Skip to main content

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}