Skip to main content

euv_ui/component/tree/view/
fn.rs

1use super::*;
2
3/// A hierarchical tree with selectable rows and collapsible branches.
4///
5/// Renders a `c_euv_tree` column holding one `c_euv_tree_node` per node.
6/// Each node holds a clickable row — `c_euv_tree_node_row`, or
7/// `c_euv_tree_node_row_active` for the selected node — carrying a toggle
8/// marker and a `c_euv_tree_node_label`. The marker is
9/// `c_euv_tree_node_toggle` while collapsed, `c_euv_tree_node_toggle_open`
10/// while expanded, and `c_euv_tree_node_toggle_leaf` for a node without
11/// children, which renders it invisible so the labels stay aligned.
12///
13/// The children of a branch stay mounted: the branch container carries
14/// `c_euv_tree_node_children` plus `c_euv_tree_node_children_open` or
15/// `c_euv_tree_node_children_closed`, so collapsing a branch is a class
16/// swap and never a reactive `if` that would unmount the subtree.
17///
18/// The selection is read once per render and handed down the recursion,
19/// and the expansion set is read once per render as well, so a deep tree
20/// never re-reads a signal per node.
21///
22/// # Arguments
23///
24/// - `VirtualNode<EuvTreeProps>` - The props node containing the nodes, the selection and the expansion seed.
25///
26/// # Returns
27///
28/// - `VirtualNode` - The tree virtual DOM tree.
29#[component]
30pub fn euv_tree(node: VirtualNode<EuvTreeProps>) -> VirtualNode {
31    let EuvTreeProps {
32        nodes,
33        selected,
34        on_select,
35        default_expanded,
36    }: EuvTreeProps = node.try_get_props().unwrap_or_default();
37    let expanded: Signal<Vec<String>> = use_euv_tree_expanded(nodes.as_slice(), default_expanded);
38    let selected_value: String = selected.get();
39    let expanded_values: Vec<String> = expanded.get();
40    let tree_nodes: Vec<VirtualNode> = nodes
41        .into_iter()
42        .map(|tree_node: EuvTreeNode| {
43            tree_node_view(
44                tree_node,
45                selected,
46                &selected_value,
47                expanded,
48                &expanded_values,
49                on_select.clone(),
50            )
51        })
52        .collect();
53    html! {
54        div {
55            class: c_euv_tree()
56            tree_nodes
57        }
58    }
59}
60
61/// Obtains the expansion state handle registered against the current hook
62/// context slot.
63///
64/// Behaves like [`HookContext::use_hook`] — the same handle is returned on
65/// every render at the same hook index, so the expanded set survives
66/// re-renders. The seed is built once, when the slot is first written:
67/// an empty set when the tree starts collapsed, and the value of every
68/// node in the tree when it starts expanded.
69///
70/// # Arguments
71///
72/// - `&[EuvTreeNode]` - The root nodes used to seed the expanded set.
73/// - `bool` - Whether the tree starts with every node expanded.
74///
75/// # Returns
76///
77/// - `Signal<Vec<String>>` - The signal holding the expanded node values.
78pub fn use_euv_tree_expanded(nodes: &[EuvTreeNode], default_expanded: bool) -> Signal<Vec<String>> {
79    let seed: Vec<String> = if default_expanded {
80        tree_node_values(nodes)
81    } else {
82        Vec::new()
83    };
84    HookContext::use_hook(move || Signal::create(seed))
85}
86
87/// Collects the value of every node in a tree, depth first.
88///
89/// # Arguments
90///
91/// - `&[EuvTreeNode]` - The nodes to walk.
92///
93/// # Returns
94///
95/// - `Vec<String>` - The value of every node, parents before children.
96fn tree_node_values(nodes: &[EuvTreeNode]) -> Vec<String> {
97    let mut values: Vec<String> = Vec::with_capacity(nodes.len());
98    collect_tree_node_values(nodes, &mut values);
99    values
100}
101
102/// Appends the value of every node in a tree to an accumulator.
103///
104/// # Arguments
105///
106/// - `&[EuvTreeNode]` - The nodes to walk.
107/// - `&mut Vec<String>` - The accumulator the node values are appended to.
108fn collect_tree_node_values(nodes: &[EuvTreeNode], values: &mut Vec<String>) {
109    for node in nodes {
110        values.push(node.value.to_string());
111        collect_tree_node_values(&node.children, values);
112    }
113}
114
115/// Renders one node: its row, and the container holding its children.
116///
117/// The child list is built with a single `.map(...).collect()` before the
118/// `html!` block: a `for` loop inside `html!` over a local vector fails
119/// the `'static` bound the reactive attribute closure needs.
120///
121/// # Arguments
122///
123/// - `EuvTreeNode` - The node to render, consumed.
124/// - `Signal<String>` - The selected-value signal.
125/// - `&str` - The selected value, read once per render.
126/// - `Signal<Vec<String>>` - The expanded-values signal.
127/// - `&[String]` - The expanded values, read once per render.
128/// - `Option<Rc<dyn Fn(&'static str)>>` - The select callback.
129///
130/// # Returns
131///
132/// - `VirtualNode` - The tree node virtual DOM tree.
133fn tree_node_view(
134    node: EuvTreeNode,
135    selected: Signal<String>,
136    selected_value: &str,
137    expanded: Signal<Vec<String>>,
138    expanded_values: &[String],
139    on_select: Option<Rc<dyn Fn(&'static str)>>,
140) -> VirtualNode {
141    let value: &'static str = node.value;
142    let label: &'static str = node.label;
143    let has_children: bool = !node.children.is_empty();
144    let is_selected: bool = value == selected_value;
145    let is_expanded: bool = expanded_values.iter().any(|entry: &String| entry == value);
146    let child_nodes: Vec<VirtualNode> = node
147        .children
148        .into_iter()
149        .map(|child: EuvTreeNode| {
150            tree_node_view(
151                child,
152                selected,
153                selected_value,
154                expanded,
155                expanded_values,
156                on_select.clone(),
157            )
158        })
159        .collect();
160    html! {
161        div {
162            key: value
163            class: c_euv_tree_node()
164            div {
165                class: if is_selected {
166                    c_euv_tree_node_row_active()
167                } else {
168                    c_euv_tree_node_row()
169                }
170                onclick: on_tree_select(selected, on_select.clone(), value)
171                span {
172                    class: if !has_children {
173                        c_euv_tree_node_toggle_leaf()
174                    } else if is_expanded {
175                        c_euv_tree_node_toggle_open()
176                    } else {
177                        c_euv_tree_node_toggle()
178                    }
179                    onclick: on_tree_toggle(expanded, value)
180                    "▸"
181                }
182                span {
183                    class: c_euv_tree_node_label()
184                    label
185                }
186            }
187            if has_children {
188                div {
189                    class: c_euv_tree_node_children()
190                    class: if is_expanded {
191                        c_euv_tree_node_children_open()
192                    } else {
193                        c_euv_tree_node_children_closed()
194                    }
195                    child_nodes
196                }
197            }
198        }
199    }
200}
201
202/// Selects a node and reports the selection to the caller.
203///
204/// The node's value is written into the caller-owned `selected` signal and
205/// the `on_select` callback is called with it, so a caller can react to
206/// the selection without subscribing to the signal itself.
207///
208/// # Arguments
209///
210/// - `Signal<String>` - The selected-value signal.
211/// - `Option<Rc<dyn Fn(&'static str)>>` - The select callback.
212/// - `&'static str` - The selected node's value.
213pub fn select_tree_node(
214    selected: Signal<String>,
215    on_select: Option<Rc<dyn Fn(&'static str)>>,
216    value: &'static str,
217) {
218    selected.set(value.to_string());
219    if let Some(callback) = &on_select {
220        callback(value);
221    }
222}
223
224/// Adds a node's value to the expanded set, or removes it when it is there.
225///
226/// # Arguments
227///
228/// - `&mut Vec<String>` - The expanded values, mutated in place.
229/// - `&str` - The toggled node's value.
230pub fn toggle_expanded_value(values: &mut Vec<String>, value: &str) {
231    if let Some(index) = values.iter().position(|entry: &String| entry == value) {
232        values.remove(index);
233    } else {
234        values.push(value.to_string());
235    }
236}
237
238/// Builds the click handler that selects a node.
239///
240/// # Arguments
241///
242/// - `Signal<String>` - The selected-value signal.
243/// - `Option<Rc<dyn Fn(&'static str)>>` - The select callback.
244/// - `&'static str` - The clicked node's value.
245///
246/// # Returns
247///
248/// - `Option<Rc<dyn Fn(Event)>>` - The click handler.
249fn on_tree_select(
250    selected: Signal<String>,
251    on_select: Option<Rc<dyn Fn(&'static str)>>,
252    value: &'static str,
253) -> Option<Rc<dyn Fn(Event)>> {
254    Some(Rc::new(move |_: Event| {
255        select_tree_node(selected, on_select.clone(), value);
256    }))
257}
258
259/// Builds the click handler that expands or collapses a branch.
260///
261/// The event is stopped so the click does not bubble into the enclosing
262/// row, whose handler would otherwise select the node as well: the toggle
263/// only ever folds a branch, it never changes the selection.
264///
265/// # Arguments
266///
267/// - `Signal<Vec<String>>` - The expanded-values signal.
268/// - `&'static str` - The toggled node's value.
269///
270/// # Returns
271///
272/// - `Option<Rc<dyn Fn(Event)>>` - The click handler.
273fn on_tree_toggle(expanded: Signal<Vec<String>>, value: &'static str) -> Option<Rc<dyn Fn(Event)>> {
274    Some(Rc::new(move |event: Event| {
275        event.stop_propagation();
276        let mut values: Vec<String> = expanded.get();
277        toggle_expanded_value(&mut values, value);
278        expanded.set(values);
279    }))
280}