mktree/
tree.rs

1//! A tree structure and display with [`egui::SelectableLabel`] at every leaves.
2
3use crate::*;
4
5use crossbeam_channel::Sender;
6use egui::{collapsing_header::CollapsingState, RichText};
7use std::collections::HashSet;
8
9// ----------------------------------------------------------------------------
10#[derive(Clone, Debug, Default)]
11pub struct Node<T: Entity> {
12    /// This name will be used to represent the node in the tree.
13    name: String,
14    /// Inner content of the node.
15    content: T,
16    /// Collapse state of the `CollapsingHeader`.
17    open: bool,
18}
19
20impl<T: Entity> Node<T> {
21    fn empty() -> Self {
22        Self {
23            content: T::empty(),
24            open: true,
25            ..Default::default()
26        }
27    }
28
29    fn name(mut self, name: &str) -> Self {
30        self.name = name.to_owned();
31        self
32    }
33
34    fn match_by(&self, pattern: &str) -> bool {
35        self.name.to_lowercase().contains(pattern)
36    }
37}
38
39#[derive(Clone, Debug)]
40/// Used for display tree data with [`Node`].
41/// We're distinguishing the type of the nodes in order to perform further actions
42/// depending on the selected node type.
43pub enum TreeNode<T: Entity> {
44    Root(Node<T>),
45    SubTreeRoot(Node<T>),
46    Leaf(Node<T>),
47}
48
49impl<T: Entity> Default for TreeNode<T> {
50    /// Makes a leaf.
51    fn default() -> Self {
52        TreeNode::Leaf(Node::default())
53    }
54}
55
56impl<T: Entity> TreeNode<T> {
57    fn node_name(&self) -> &String {
58        match self {
59            Self::Root(inner) | Self::SubTreeRoot(inner) | Self::Leaf(inner) => &inner.name,
60        }
61    }
62
63    fn open(&self) -> bool {
64        match self {
65            Self::Root(inner) | Self::SubTreeRoot(inner) | Self::Leaf(inner) => inner.open,
66        }
67    }
68
69    fn open_mut(&mut self, open: bool) {
70        match self {
71            Self::Root(inner) | Self::SubTreeRoot(inner) | Self::Leaf(inner) => {
72                inner.open = open;
73            }
74        }
75    }
76
77    fn toggle_collapse_state(&mut self) {
78        match self {
79            Self::Root(inner) | Self::SubTreeRoot(inner) | Self::Leaf(inner) => {
80                inner.open = !inner.open;
81            }
82        }
83    }
84
85    fn selected(&self, selection: &HashSet<T>) -> bool {
86        match self {
87            // TODO: `SubTreeRoot`s won't be shown as selected for now.
88            Self::Root(_) | Self::SubTreeRoot(_) => false,
89            Self::Leaf(inner) => match inner.content.name() {
90                // TODO: this makes assets with the same name all show as selected
91                Some(name) => selection
92                    .iter()
93                    .filter_map(|a| a.name())
94                    .collect::<HashSet<&String>>()
95                    .contains(name),
96                None => false,
97            },
98        }
99    }
100
101    fn leaf(entity: T) -> TreeNode<T> {
102        TreeNode::Leaf(Node {
103            name: match entity.name() {
104                Some(name) => name.to_owned(),
105                None => EMPTY_NODE_NAME.to_owned(),
106            },
107            content: entity,
108            open: false,
109        })
110    }
111
112    fn subtree_root(group: &str, typ: &ProjectSource) -> TreeNode<T> {
113        TreeNode::SubTreeRoot(Node {
114            name: group.to_owned(),
115            content: T::as_group(group, typ),
116            open: true,
117        })
118    }
119}
120
121// ----------------------------------------------------------------------------
122#[derive(Clone, Debug, Default)]
123/// Recursive tree struct.
124pub struct MkTree<T: Entity>(pub TreeNode<T>, pub Vec<MkTree<T>>);
125
126impl<T: Entity> MkTree<T> {
127    /// Can be used to sort trees after construction.
128    /// Must clone to get around the borrow checker.
129    pub fn node_name(&self) -> String {
130        self.0.node_name().clone()
131    }
132
133    fn open(&self) -> bool {
134        self.0.open()
135    }
136
137    fn toggle_collapse_state(&mut self) {
138        self.0.toggle_collapse_state()
139    }
140
141    fn node_content(&self) -> &T {
142        match &self.0 {
143            TreeNode::Root(inner) | TreeNode::SubTreeRoot(inner) | TreeNode::Leaf(inner) => {
144                &inner.content
145            }
146        }
147    }
148
149    /// Recursively determines if the node should be visible, given the filter string.
150    fn match_by(&self, pattern: &str) -> bool {
151        match &self.0 {
152            TreeNode::Leaf(inner) => inner.match_by(pattern),
153            TreeNode::SubTreeRoot(_) => self
154                .1
155                .iter()
156                .filter(|n| n.match_by(pattern))
157                .next()
158                .is_some(),
159            TreeNode::Root(_) => true,
160        }
161    }
162
163    /// The given `T` will be used to make a leaf -- a tip of the tree -- all by itself,
164    /// i.e. containing empty list of trees.
165    fn single_leaf(entity: T) -> Self {
166        Self(TreeNode::leaf(entity), vec![])
167    }
168
169    /// The given group name will be made into a `SubTreeRoot` directly containing the given leaves.
170    pub fn leaf_group(group: &str, leaves: Vec<T>, typ: &ProjectSource) -> Self {
171        Self(
172            TreeNode::subtree_root(group, typ),
173            leaves
174                .into_iter()
175                .map(|leaf| Self::single_leaf(leaf))
176                .collect(),
177        )
178    }
179
180    pub fn subtree_group(group: &str, subtrees: Vec<MkTree<T>>, typ: &ProjectSource) -> Self {
181        Self(TreeNode::subtree_root(group, typ), subtrees)
182    }
183
184    /// A root and all other leaves at the same level.
185    pub fn from_node_n_leaves(entity: T, leaves: Vec<T>) -> Self {
186        Self(
187            TreeNode::leaf(entity),
188            leaves.into_iter().map(|c| Self::single_leaf(c)).collect(),
189        )
190    }
191
192    pub fn from_node_n_subtrees(entity: T, subtrees: Vec<MkTree<T>>) -> Self {
193        Self(TreeNode::leaf(entity), subtrees)
194    }
195
196    fn listen(
197        &mut self,
198        ui: &mut egui::Ui,
199        response: egui::Response,
200        selection: &mut HashSet<T>,
201        sender: &Sender<TreeNodeSignal>,
202    ) {
203        if response.clicked() {
204            if !ui.ctx().input(|i| i.modifiers).any() {
205                selection.clear();
206            };
207            match &self.0 {
208                TreeNode::Leaf(_) => {
209                    // Only signal selection change in this case.
210                    sender
211                        .send(TreeNodeSignal::LeafClicked)
212                        .expect("Channel of MkTree's selected node response has been disconnected");
213                    // Saves selected `impl Entity` in `TreeContainer::selected_nodes`
214                    selection.insert(self.node_content().clone());
215                }
216                // don't do anything when `TreeNode::SubTreeRoot` or `TreeNode::Root` gets clicked
217                _ => {}
218            };
219            // log::info!("Selected node: {:?}", &selection);
220        };
221    }
222
223    /// Recursively makes the tree UI, while collects the user selection of the leaf nodes, and sends message.
224    pub fn ui(
225        &mut self,
226        ui: &mut egui::Ui,
227        node_name: &str,
228        depth: usize,
229        filter: &str,
230        selection: &mut HashSet<T>,
231        sender: &Sender<TreeNodeSignal>,
232    ) {
233        let mut state = CollapsingState::load_with_default_open(
234            ui.ctx(),
235            ui.make_persistent_id(node_name),
236            true,
237        );
238
239        state.set_open(self.open());
240
241        let response = state
242            .show_header(ui, |ui| {
243                // NOTE: no semicolon, as we're returning the inner response!
244                ui.selectable_label(self.0.selected(selection), node_name)
245                // the `egui::Response` is returned from this `CollapsingState::show_header` method,
246                // and passed to `Self::listen` to be sent via channel
247                //
248            })
249            // then recursively draws the children content
250            .body(|ui| self.children_ui(ui, &node_name, depth, filter, selection, sender));
251
252        // toggles collapse state
253        if response.0.clicked() {
254            self.toggle_collapse_state();
255        };
256
257        // checks for clicks on the header UI -- selectable label
258        self.listen(ui, response.1.inner, selection, sender);
259    }
260
261    fn children_ui(
262        &mut self,
263        ui: &mut egui::Ui,
264        // this isn't used in this function body but in previous call it's used for the header
265        _node_name: &str,
266        depth: usize,
267        filter: &str,
268        selection: &mut HashSet<T>,
269        sender: &Sender<TreeNodeSignal>,
270    ) {
271        let mut tree = std::mem::take(self);
272        // modifies the children
273        tree.1 = tree
274            .1
275            .into_iter()
276            .map(|mut child| {
277                let node_name = child.node_name();
278                if filter.is_empty() || child.match_by(filter) {
279                    child.ui(ui, &node_name, depth + 1, filter, selection, sender);
280                };
281                child
282            })
283            .collect();
284        *self = tree
285    }
286
287    /// Recursively counts (by accumulation) the number of leaves.
288    fn leaf_len(&self, count: &mut usize) -> usize {
289        if let TreeNode::Leaf(_) = self.0 {
290            *count += 1;
291        };
292        self.1.iter().for_each(|t| {
293            t.leaf_len(count);
294        });
295        *count
296    }
297
298    fn collapse_all(&mut self, open: bool) {
299        match self.0 {
300            TreeNode::Root(_) => {
301                // do not expand nor close
302            }
303            _ => {
304                self.0.open_mut(open);
305            }
306        }
307        self.1.iter_mut().for_each(|t| t.collapse_all(open));
308    }
309}
310
311#[derive(Debug, Clone, Default)]
312/// Container for the asset tree, where first field holds the currently selected asset (via primary mouse click).
313pub struct TreeContainer<T: Entity> {
314    /// The final tree with `tree.0` of `TreeNode::Root` type.
315    tree: MkTree<T>,
316
317    root_nice_name: String,
318
319    total_leaves: usize,
320
321    filter: String,
322
323    /// This contains only non-empty ProductionAsset, but such item might not have `asset_name`, i.e.
324    /// having only `category.main_type` value.
325    selected_nodes: HashSet<T>,
326
327    /// Whether next toggle action should expand or collapse the tree recursively.
328    batch_collapse: bool,
329}
330
331impl<T: Entity> TreeContainer<T> {
332    pub fn uninitialized_root() -> Self {
333        Self {
334            tree: MkTree(
335                TreeNode::Root(Node::empty().name(TREE_ROOT_UNINITIALIZED_NAME)),
336                vec![],
337            ),
338            ..Default::default()
339        }
340    }
341
342    pub fn subtrees_mut(&mut self, subtrees: Vec<MkTree<T>>, root_name: &str, show_count: bool) {
343        self.tree = MkTree(TreeNode::Root(Node::empty().name(root_name)), subtrees);
344        self.count_leaves();
345        self.make_root_nice_name(root_name, show_count);
346    }
347
348    pub fn selected_nodes(&self) -> &HashSet<T> {
349        &self.selected_nodes
350    }
351
352    pub fn selected_nodes_mut(&mut self, selected_nodes: HashSet<T>) {
353        self.selected_nodes = selected_nodes;
354    }
355
356    // TODO: provide select all leaves?
357    pub fn clear_selection(&mut self) {
358        self.selected_nodes = HashSet::new();
359    }
360
361    pub fn filter_ui(&mut self, width: f32, ui: &mut egui::Ui) {
362        ui.horizontal(|ui| {
363            ui.label("Filter by Name:");
364            ui.add(egui::TextEdit::singleline(&mut self.filter).desired_width(width));
365            // forces lowercase conversion
366            self.filter = self.filter.to_lowercase();
367            if ui.button("x").clicked() {
368                self.filter.clear();
369            }
370        });
371    }
372
373    pub fn batch_collapse_ui(&mut self, ui: &mut egui::Ui) {
374        let text = RichText::new(if self.batch_collapse {
375            "⏷ Expand Tree"
376        } else {
377            "➖ Collapse Tree"
378        });
379        if ui.button(text).clicked() {
380            self.tree.collapse_all(self.batch_collapse);
381            self.batch_collapse = !self.batch_collapse;
382        };
383    }
384
385    pub fn tree_ui(&mut self, ui: &mut egui::Ui, sender: &Sender<TreeNodeSignal>) {
386        self.tree.ui(
387            ui,
388            &self.root_nice_name,
389            0,
390            &self.filter,
391            &mut self.selected_nodes,
392            sender,
393        )
394    }
395
396    /// Counts all the leaves.
397    fn count_leaves(&mut self) {
398        // must reset the count first
399        self.total_leaves = 0;
400        self.tree.leaf_len(&mut self.total_leaves);
401    }
402
403    /// Appends after the root name of the tree the count of leaves.
404    fn make_root_nice_name(&mut self, root_name: &str, show_count: bool) {
405        if show_count {
406            self.root_nice_name = format!("{}: total {}", root_name, self.total_leaves);
407        } else {
408            self.root_nice_name = root_name.to_owned();
409        }
410    }
411}