dioxus_swdir_tree_core/node.rs
1//! The recursive node value making up the in-memory tree.
2//!
3//! Invariants (upheld by [`crate::DirectoryTree`] transitions, asserted
4//! by the test suite):
5//!
6//! 1. `is_expanded` ⟹ `is_dir` — files are never expanded.
7//! 2. `is_expanded` and `is_loaded` are independent: a node may be
8//! loaded-but-collapsed, or briefly expanded-but-not-loaded while a
9//! scan is in flight.
10//! 3. `children` is empty iff `!is_loaded`, except for genuinely empty
11//! (or fully filtered) directories, which are loaded with no children.
12//! 4. `error.is_some()` ⟹ `is_loaded` ∧ `children` empty.
13//!
14//! Nodes are **ephemeral**: rebuilt when filters change or fresh scan
15//! data merges. Anything that must survive rebuilds (the selection set,
16//! from RFC 004 on) lives on the tree root keyed by path, never on nodes.
17
18use std::ffi::OsStr;
19use std::path::{Path, PathBuf};
20
21use crate::entry::LoadedEntry;
22use crate::error::ScanIssue;
23
24/// One directory entry in the tree.
25#[derive(Debug, Clone, PartialEq)]
26pub struct TreeNode {
27 /// Absolute path.
28 pub path: PathBuf,
29 /// `true` for directories.
30 pub is_dir: bool,
31 /// `true` if the node's children are drawn on screen.
32 pub is_expanded: bool,
33 /// `true` once a scan has populated (or errored) this node.
34 pub is_loaded: bool,
35 /// Derived view hint: `true` iff this path is in the tree's
36 /// [`crate::DirectoryTree::selected_paths`] set.
37 ///
38 /// Re-synced after every selection mutation; never authoritative —
39 /// paths remain selected while their node is unloaded, filtered out,
40 /// or temporarily absent.
41 pub is_selected: bool,
42 /// Filtered children; empty until loaded.
43 pub children: Vec<TreeNode>,
44 /// Set when the scan for this directory failed.
45 pub error: Option<ScanIssue>,
46}
47
48impl TreeNode {
49 /// The eagerly-created root node. Always a directory, initially
50 /// neither loaded nor expanded.
51 pub(crate) fn new_root(path: PathBuf) -> Self {
52 Self {
53 path,
54 is_dir: true,
55 is_expanded: false,
56 is_loaded: false,
57 is_selected: false,
58 children: Vec::new(),
59 error: None,
60 }
61 }
62
63 /// A fresh, unloaded node for a scanned entry.
64 pub(crate) fn from_entry(entry: &LoadedEntry) -> Self {
65 Self {
66 path: entry.path.clone(),
67 is_dir: entry.is_dir,
68 is_expanded: false,
69 is_loaded: false,
70 is_selected: false,
71 children: Vec::new(),
72 error: None,
73 }
74 }
75
76 /// Final path component, for use as a row label.
77 pub fn file_name(&self) -> &OsStr {
78 self.path.file_name().unwrap_or(OsStr::new(""))
79 }
80
81 /// Find the node for `path` in this subtree.
82 ///
83 /// Descends only into the child whose path is a component-wise
84 /// prefix of `path`, so lookup cost is proportional to depth, not
85 /// to tree size.
86 pub fn find(&self, path: &Path) -> Option<&TreeNode> {
87 if self.path == path {
88 return Some(self);
89 }
90 self.children
91 .iter()
92 .find(|c| path.starts_with(&c.path))
93 .and_then(|c| c.find(path))
94 }
95
96 /// Mutable variant of [`TreeNode::find`].
97 pub(crate) fn find_mut(&mut self, path: &Path) -> Option<&mut TreeNode> {
98 if self.path == path {
99 return Some(self);
100 }
101 self.children
102 .iter_mut()
103 .find(|c| path.starts_with(&c.path))
104 .and_then(|c| c.find_mut(path))
105 }
106}