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}