Skip to main content

promkit_widgets/tree/
node.rs

1use std::{fs, path};
2
3/// Represents the kind of a node in a tree structure.
4///
5/// This enum is used to distinguish between nodes that are currently
6/// visible in their "folded" state and those that are "unfolded" to reveal
7/// their children, if any.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum Kind {
10    /// Represents a node that is folded (i.e., its children are not currently visible).
11    /// - `id`: A unique identifier for the node.
12    /// - `path`: The path from the root to this node, represented as a sequence of indices.
13    Folded { id: String, path: Path },
14
15    /// Represents a node that is unfolded (i.e., its children are currently visible).
16    /// - `id`: A unique identifier for the node.
17    /// - `path`: The path from the root to this node, represented as a sequence of indices.
18    Unfolded { id: String, path: Path },
19}
20
21/// A type alias for a path in the tree, represented as a sequence of indices.
22pub type Path = Vec<usize>;
23
24/// Represents a node within a tree structure.
25///
26/// A node can either be a `NonLeaf`, containing children and a visibility flag,
27/// or a `Leaf`, representing an end node without children.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub enum Node {
30    /// Represents a non-leaf node, which can contain child nodes.
31    /// - `id`: A unique identifier for the node.
32    /// - `children`: A vector of child nodes.
33    /// - `children_visible`: A boolean indicating if the children of this node are visible.
34    NonLeaf {
35        id: String,
36        children: Vec<Node>,
37        children_visible: bool,
38    },
39
40    /// Represents a leaf node, which does not contain any child nodes.
41    /// - `id`: A unique identifier for the leaf node.
42    Leaf(String),
43}
44
45impl TryFrom<&path::PathBuf> for Node {
46    /// Attempts to create a `Node` from a given directory path.
47    ///
48    /// This method constructs a `Node::NonLeaf` representing the directory specified by `dir_path`.
49    /// It recursively explores the directory, converting subdirectories into `Node::NonLeaf` instances
50    /// and files into `Node::Leaf` instances. Directories and files are kept in separate lists initially,
51    /// then combined with all directories first, followed by files. Both lists are sorted alphabetically
52    /// before merging. The resulting tree structure reflects the hierarchy of files and directories within
53    /// `dir_path`, with directories listed before files, both in alphabetical order.
54    ///
55    /// # Parameters
56    /// - `dir_path`: A reference to a `PathBuf` representing the directory to be converted into a `Node`.
57    ///
58    /// # Returns
59    /// A `Result` containing the root `Node` of the constructed tree if successful, or an `Error` if the
60    /// directory cannot be read or if any file name cannot be converted to a string.
61    ///
62    /// # Errors
63    /// This method returns an `Error` if:
64    /// - The path does not exist or is not a directory.
65    /// - There is an error reading the directory contents.
66    /// - A file name cannot be converted to a UTF-8 string.
67    type Error = anyhow::Error;
68
69    fn try_from(dir_path: &path::PathBuf) -> anyhow::Result<Self> {
70        let mut directories = Vec::new();
71        let mut files = Vec::new();
72
73        if dir_path.is_dir() {
74            for entry in fs::read_dir(dir_path)? {
75                let path = entry?.path();
76                if path.is_dir() {
77                    directories.push(Node::try_from(&path)?);
78                } else if path.is_file() {
79                    files.push(Node::Leaf(
80                        path.file_name()
81                            .and_then(|name| name.to_str())
82                            .ok_or_else(|| {
83                                std::io::Error::other("Failed to convert file name to string")
84                            })?
85                            .to_string(),
86                    ));
87                }
88            }
89        }
90
91        directories.sort_by(|a, b| a.id().cmp(b.id()));
92        files.sort_by(|a, b| a.id().cmp(b.id()));
93
94        let mut children = directories;
95        children.extend(files);
96
97        Ok(Node::NonLeaf {
98            id: dir_path
99                .file_name()
100                .and_then(|name| name.to_str())
101                .ok_or_else(|| std::io::Error::other("Failed to convert directory name to string"))?
102                .to_string(),
103            children,
104            children_visible: false,
105        })
106    }
107}
108
109impl Node {
110    fn id(&self) -> &String {
111        match self {
112            Node::NonLeaf { id, .. } => id,
113            Node::Leaf(id) => id,
114        }
115    }
116
117    /// Flattens the tree structure into a vector of `Kind`, including only visible nodes.
118    ///
119    /// This method performs a depth-first search (DFS) to traverse the tree and collect
120    /// nodes into a vector. Each node is represented as either `Kind::Folded` or `Kind::Unfolded`
121    /// based on its visibility and whether it has children.
122    ///
123    /// Returns:
124    /// - Vec<Kind>: A vector of `Kind` representing the visible nodes in the tree.
125    pub fn flatten_visibles(&self) -> Vec<Kind> {
126        fn dfs(node: &Node, path: Path, ret: &mut Vec<Kind>) {
127            match node {
128                Node::NonLeaf {
129                    id,
130                    children,
131                    children_visible,
132                } => {
133                    if *children_visible {
134                        ret.push(Kind::Unfolded {
135                            id: id.clone(),
136                            path: path.clone(),
137                        });
138                        for (index, child) in children.iter().enumerate() {
139                            let mut new_path = path.clone();
140                            new_path.push(index);
141                            dfs(child, new_path, ret);
142                        }
143                    } else {
144                        ret.push(Kind::Folded {
145                            id: id.clone(),
146                            path: path.clone(),
147                        });
148                    }
149                }
150                Node::Leaf(item) => {
151                    ret.push(Kind::Folded {
152                        id: item.clone(),
153                        path: path.clone(),
154                    });
155                }
156            }
157        }
158
159        let mut ret = Vec::new();
160        dfs(self, Vec::new(), &mut ret);
161        ret
162    }
163
164    /// Toggles the visibility of the children of the node specified by the given path.
165    ///
166    /// Parameters:
167    /// - path: &Path - A reference to a vector of usize, representing the path to the target node.
168    ///
169    /// This method modifies the tree in-place. If the target node is found and is a `NonLeaf`,
170    /// its `children_visible` field is toggled.
171    pub fn toggle(&mut self, path: &Path) {
172        if let Some(Node::NonLeaf {
173            children_visible, ..
174        }) = self.get_mut(path)
175        {
176            *children_visible = !*children_visible;
177        }
178    }
179
180    /// Retrieves the IDs of all nodes along the path to a specified node.
181    ///
182    /// Parameters:
183    /// - path: &Path - A reference to a vector of usize, representing the path to the target node.
184    ///
185    /// Returns:
186    /// - Vec<String>: A vector of String IDs representing the nodes along the path to the target node.
187    pub fn get_waypoints(&self, path: &Path) -> Vec<String> {
188        let mut ids = Vec::new();
189        let mut node = self;
190        for &index in path {
191            match node {
192                Node::NonLeaf { id, children, .. } => {
193                    ids.push(id.clone());
194                    if let Some(child) = children.get(index) {
195                        node = child;
196                    } else {
197                        break;
198                    }
199                }
200                Node::Leaf(id) => {
201                    ids.push(id.clone());
202                    break;
203                }
204            }
205        }
206        ids
207    }
208
209    /// Retrieves a reference to the node specified by the given path.
210    ///
211    /// Parameters:
212    /// - path: &Path - A reference to a vector of usize, representing the path to the target node.
213    ///
214    /// Returns:
215    /// - Option<&Node>: An option containing a reference to the target node if found, or None otherwise.
216    pub fn get(&self, path: &Path) -> Option<&Node> {
217        let mut node = self;
218        for seg in path {
219            match node {
220                Node::NonLeaf {
221                    id: _,
222                    children,
223                    children_visible: _,
224                } => {
225                    if let Some(next_node) = children.get(*seg) {
226                        node = next_node;
227                    } else {
228                        return None;
229                    }
230                }
231                Node::Leaf(_) => {
232                    return None;
233                }
234            }
235        }
236        Some(node)
237    }
238
239    /// Retrieves a mutable reference to the node specified by the given path.
240    ///
241    /// Parameters:
242    /// - path: &Path - A reference to a vector of usize, representing the path to the target node.
243    ///
244    /// Returns:
245    /// - Option<&mut Node>: An option containing a mutable reference to the target node if found, or None otherwise.
246    pub fn get_mut(&mut self, path: &Path) -> Option<&mut Node> {
247        let mut node = self;
248        for seg in path {
249            match node {
250                Node::NonLeaf {
251                    id: _,
252                    children,
253                    children_visible: _,
254                } => {
255                    if let Some(next_node) = children.get_mut(*seg) {
256                        node = next_node;
257                    } else {
258                        return None;
259                    }
260                }
261                Node::Leaf(_) => {
262                    return None;
263                }
264            }
265        }
266        Some(node)
267    }
268}
269
270#[cfg(test)]
271mod test {
272    use super::*;
273
274    fn create_test_node() -> Node {
275        Node::NonLeaf {
276            id: "root".into(),
277            children: vec![
278                Node::NonLeaf {
279                    id: "a".into(),
280                    children: vec![Node::Leaf("aa".into()), Node::Leaf("ab".into())],
281                    children_visible: true,
282                },
283                Node::Leaf("b".into()),
284                Node::Leaf("c".into()),
285            ],
286            children_visible: true,
287        }
288    }
289
290    fn as_nonleaf(node: &Node) -> Option<(&String, &Vec<Node>, bool)> {
291        match node {
292            Node::NonLeaf {
293                id,
294                children,
295                children_visible,
296            } => Some((id, children, *children_visible)),
297            _ => None,
298        }
299    }
300
301    mod toggle {
302        use super::*;
303
304        #[test]
305        fn test() {
306            let mut node = create_test_node();
307            node.toggle(&vec![]);
308            assert!(!as_nonleaf(node.get(&vec![]).unwrap()).unwrap().2);
309        }
310    }
311
312    mod flatten_visibles {
313        use super::*;
314
315        #[test]
316        fn test() {
317            let node = create_test_node();
318            assert_eq!(
319                vec![
320                    Kind::Unfolded {
321                        id: "root".into(),
322                        path: vec![],
323                    },
324                    Kind::Unfolded {
325                        id: "a".into(),
326                        path: vec![0],
327                    },
328                    Kind::Folded {
329                        id: "aa".into(),
330                        path: vec![0, 0],
331                    },
332                    Kind::Folded {
333                        id: "ab".into(),
334                        path: vec![0, 1],
335                    },
336                    Kind::Folded {
337                        id: "b".into(),
338                        path: vec![1],
339                    },
340                    Kind::Folded {
341                        id: "c".into(),
342                        path: vec![2],
343                    },
344                ],
345                node.flatten_visibles(),
346            );
347        }
348
349        #[test]
350        fn test_after_toggle() {
351            let mut node = create_test_node();
352            node.toggle(&vec![]);
353            assert_eq!(
354                vec![Kind::Folded {
355                    id: "root".into(),
356                    path: vec![],
357                },],
358                node.flatten_visibles(),
359            );
360        }
361    }
362}