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}