Skip to main content

euv_ui/component/tree_select/view/
fn.rs

1use super::*;
2
3/// A select whose option panel is a tree.
4///
5/// Renders a `c_euv_tree_select` wrapper holding the optional group label,
6/// a `c_euv_tree_select_trigger` button that shows the selected node's
7/// label in `c_euv_tree_select_value` — or the placeholder in
8/// `c_euv_tree_select_placeholder` while no node matches — next to a
9/// trailing `c_euv_tree_select_caret` marker, and the
10/// `c_euv_tree_select_panel` the tree lives in.
11///
12/// The panel is always mounted: opening and closing it swaps the
13/// `c_euv_tree_select_panel_open` and `c_euv_tree_select_panel_closed`
14/// classes rather than removing the tree from the tree, so the nodes
15/// survive every re-render.
16///
17/// The tree itself is the [`euv_tree`] component, so the panel reuses the
18/// tree classes — `c_euv_tree`, `c_euv_tree_node`, `c_euv_tree_node_row`,
19/// `c_euv_tree_node_row_active`, the toggle markers and the children
20/// containers — instead of carrying a second set of them. The children of
21/// a node stay mounted; expanding a branch is a class swap.
22///
23/// Choosing a node writes its value into the `value` signal, reports it to
24/// the `on_select` callback and closes the panel. The open state is owned
25/// by the component itself through a hook, so the caller never has to
26/// wire it up.
27///
28/// # Arguments
29///
30/// - `VirtualNode<EuvTreeSelectProps>` - The props node holding the tree, the value signal and the select callback.
31///
32/// # Returns
33///
34/// - `VirtualNode` - The tree select virtual DOM tree.
35#[component]
36pub fn euv_tree_select(node: VirtualNode<EuvTreeSelectProps>) -> VirtualNode {
37    let EuvTreeSelectProps {
38        nodes,
39        value,
40        placeholder,
41        label: label_text,
42        on_select,
43    }: EuvTreeSelectProps = node.try_get_props().unwrap_or_default();
44    let open: Signal<bool> = HookContext::use_hook(|| Signal::create(false));
45    let selected_value: String = value.get();
46    let selected_label: Option<&'static str> = tree_select_find_label(&nodes, &selected_value);
47    let trigger_text: &'static str = selected_label.unwrap_or(placeholder);
48    let has_selection: bool = selected_label.is_some();
49    let pick: Rc<dyn Fn(&'static str)> = tree_select_pick_handler(value, on_select, open);
50    html! {
51        div {
52            class: c_euv_tree_select()
53            if !label_text.is_empty() {
54                div {
55                    class: c_form_label()
56                    label_text
57                }
58            }
59            button {
60                type: "button"
61                class: c_euv_tree_select_trigger()
62                onclick: tree_select_toggle_handler(open)
63                span {
64                    class: if has_selection {
65                        c_euv_tree_select_value()
66                    } else {
67                        c_euv_tree_select_placeholder()
68                    }
69                    trigger_text
70                }
71                span {
72                    class: c_euv_tree_select_caret()
73                    "▾"
74                }
75            }
76            div {
77                class: c_euv_tree_select_panel()
78                class: if { open } {
79                    c_euv_tree_select_panel_open()
80                } else {
81                    c_euv_tree_select_panel_closed()
82                }
83                euv_tree {
84                    nodes: nodes
85                    selected: value
86                    on_select: Some(pick)
87                    default_expanded: true
88                }
89            }
90        }
91    }
92}
93
94/// Finds the label of the node carrying the given value.
95///
96/// The search walks the tree depth first, so a nested node is found by
97/// its value just like a root one.
98///
99/// # Arguments
100///
101/// - `&[EuvTreeNode]` - The nodes to walk.
102/// - `&str` - The value of the node to find.
103///
104/// # Returns
105///
106/// - `Option<&'static str>` - The label of the matching node, when there is one.
107pub fn tree_select_find_label(nodes: &[EuvTreeNode], value: &str) -> Option<&'static str> {
108    for node in nodes.iter() {
109        if node.value == value {
110            return Some(node.label);
111        }
112        if let Some(found) = tree_select_find_label(&node.children, value) {
113            return Some(found);
114        }
115    }
116    None
117}
118
119/// Builds the click handler that opens or closes the panel.
120///
121/// # Arguments
122///
123/// - `Signal<bool>` - The panel open state owned by the component.
124///
125/// # Returns
126///
127/// - `Option<Rc<dyn Fn(E)>>` - The toggle click handler.
128pub fn tree_select_toggle_handler<E: 'static>(open: Signal<bool>) -> Option<Rc<dyn Fn(E)>> {
129    Some(Rc::new(move |_: E| {
130        let is_open: bool = open.get();
131        open.set(!is_open);
132    }))
133}
134
135/// Builds the handler that picks a node of the tree.
136///
137/// # Arguments
138///
139/// - `Signal<String>` - The value signal owned by the caller.
140/// - `Option<Rc<dyn Fn(&'static str)>>` - The select callback.
141/// - `Signal<bool>` - The panel open state owned by the component.
142///
143/// # Returns
144///
145/// - `Rc<dyn Fn(&'static str)>` - The pick handler.
146fn tree_select_pick_handler(
147    value: Signal<String>,
148    on_select: Option<Rc<dyn Fn(&'static str)>>,
149    open: Signal<bool>,
150) -> Rc<dyn Fn(&'static str)> {
151    Rc::new(move |node_value: &'static str| {
152        tree_select_pick_node(value, on_select.clone(), open, node_value);
153    })
154}
155
156/// Picks a node: writes its value, reports it and closes the panel.
157///
158/// The value lands in the caller's signal first, so the callback and the
159/// closing of the panel both see the new selection.
160///
161/// # Arguments
162///
163/// - `Signal<String>` - The value signal owned by the caller.
164/// - `Option<Rc<dyn Fn(&'static str)>>` - The select callback.
165/// - `Signal<bool>` - The panel open state owned by the component.
166/// - `&'static str` - The value of the picked node.
167pub fn tree_select_pick_node(
168    value: Signal<String>,
169    on_select: Option<Rc<dyn Fn(&'static str)>>,
170    open: Signal<bool>,
171    node_value: &'static str,
172) {
173    value.set(String::from(node_value));
174    if let Some(callback) = &on_select {
175        callback(node_value);
176    }
177    open.set(false);
178}