Skip to main content

iced_swdir_tree/directory_tree/
node.rs

1//! In-memory tree types: [`TreeNode`] and [`TreeCache`].
2
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use crate::Error;
7
8/// A single node in the directory tree.
9///
10/// All fields are public so downstream code (tests, custom renderers
11/// built on top of the widget's state) can inspect them, but the widget
12/// itself drives mutation through [`DirectoryTree::update`].
13///
14/// [`DirectoryTree::update`]: crate::DirectoryTree::update
15#[derive(Debug, Clone)]
16pub struct TreeNode {
17    /// Full path of the entry.
18    pub path: PathBuf,
19    /// `true` if the entry is a directory.
20    pub is_dir: bool,
21    /// `true` if the directory is currently expanded in the UI. Always
22    /// `false` for files.
23    pub is_expanded: bool,
24    /// `true` if this directory has already been scanned at least once,
25    /// even if the scan returned zero children. Distinguishes
26    /// "scanned and empty" from "not scanned yet" in the view.
27    pub is_loaded: bool,
28    /// Direct children. Empty until `is_loaded` is `true`.
29    pub children: Vec<TreeNode>,
30    /// `true` if the user has this node selected.
31    pub is_selected: bool,
32    /// Populated when a scan of *this directory* failed. For files this
33    /// is always `None`. The view layer uses this to render a greyed-out
34    /// or error-tinted row.
35    pub error: Option<Error>,
36}
37
38impl TreeNode {
39    /// Build the root node of a freshly-constructed [`DirectoryTree`].
40    ///
41    /// The root is always treated as a directory (we can't meaningfully
42    /// root a tree at a regular file).
43    ///
44    /// [`DirectoryTree`]: crate::DirectoryTree
45    pub(crate) fn new_root(path: PathBuf) -> Self {
46        Self {
47            path,
48            is_dir: true,
49            is_expanded: false,
50            is_loaded: false,
51            children: Vec::new(),
52            is_selected: false,
53            error: None,
54        }
55    }
56
57    /// Build a child node from a loaded entry.
58    pub(crate) fn from_entry(entry: &LoadedEntry) -> Self {
59        Self {
60            path: entry.path.clone(),
61            is_dir: entry.is_dir,
62            is_expanded: false,
63            is_loaded: false,
64            children: Vec::new(),
65            is_selected: false,
66            error: None,
67        }
68    }
69
70    /// Find a descendant (including `self`) by path, returning a
71    /// mutable reference.
72    ///
73    /// Uses path-prefix pruning: we only descend into subtrees that
74    /// could contain `target`, so the worst case is O(depth) not
75    /// O(total nodes).
76    pub(crate) fn find_mut(&mut self, target: &Path) -> Option<&mut TreeNode> {
77        if self.path == target {
78            return Some(self);
79        }
80        // Only descend if target lives under `self.path`. Without this
81        // check we'd walk every sibling subtree on every lookup.
82        if !target.starts_with(&self.path) {
83            return None;
84        }
85        for child in &mut self.children {
86            if let Some(hit) = child.find_mut(target) {
87                return Some(hit);
88            }
89        }
90        None
91    }
92
93    /// Clear the selection flag on every node in this subtree.
94    ///
95    /// Selection is single-select; setting a new selection is a
96    /// clear-then-set operation.
97    pub(crate) fn clear_selection(&mut self) {
98        self.is_selected = false;
99        for child in &mut self.children {
100            child.clear_selection();
101        }
102    }
103
104    /// Count nodes in this subtree (including `self`). Exposed primarily
105    /// for tests and diagnostics.
106    #[allow(dead_code)]
107    pub(crate) fn node_count(&self) -> usize {
108        1 + self.children.iter().map(Self::node_count).sum::<usize>()
109    }
110
111    /// Flat list of rows the view would render, in render order.
112    ///
113    /// Every ancestor-collapsed subtree is skipped. The returned
114    /// order is the same one the user sees on screen, so it is the
115    /// right order for keyboard navigation and Shift+click range
116    /// extension to reason about. Cost is O(visible nodes).
117    pub(crate) fn visible_rows(&self) -> Vec<VisibleRow<'_>> {
118        let mut out = Vec::new();
119        collect_visible(self, 0, &mut out);
120        out
121    }
122}
123
124/// A single visible row: the node, plus its indentation depth.
125///
126/// Crate-internal — used by the keyboard handler and by the
127/// multi-select range-extension path. Depth is cached on the row
128/// so callers don't have to re-walk from the root.
129#[derive(Debug)]
130pub(crate) struct VisibleRow<'a> {
131    pub node: &'a TreeNode,
132    #[allow(dead_code)]
133    pub depth: u32,
134}
135
136fn collect_visible<'a>(node: &'a TreeNode, depth: u32, out: &mut Vec<VisibleRow<'a>>) {
137    out.push(VisibleRow { node, depth });
138    if node.is_dir && node.is_expanded && node.is_loaded {
139        for child in &node.children {
140            collect_visible(child, depth + 1, out);
141        }
142    }
143}
144
145/// Lightweight, owned entry record produced by [`crate::walker`] and
146/// consumed by [`super::update`] to build [`TreeNode`]s.
147///
148/// We don't use `swdir::DirEntry` here directly — keeping swdir types
149/// out of the message enum means swdir can be a private dependency
150/// from the public API's point of view.
151#[derive(Debug, Clone)]
152pub struct LoadedEntry {
153    /// Full path of the entry.
154    pub path: PathBuf,
155    /// `true` if the entry is a directory. Symlinks to directories
156    /// are treated as files here (the widget never auto-follows them,
157    /// to stay robust against cycles).
158    pub is_dir: bool,
159    /// `true` if the entry itself is a symlink (regardless of target).
160    ///
161    /// Currently only used for cycle-avoidance diagnostics; kept here
162    /// so v0.4+ can render a symlink indicator without having to
163    /// re-stat every entry.
164    #[allow(dead_code)]
165    pub is_symlink: bool,
166    /// `true` if the entry is hidden per OS conventions.
167    ///
168    /// Persisted on the entry (not just consulted in the scan path)
169    /// so that a later filter change — e.g. flipping from
170    /// `FilesAndFolders` to `AllIncludingHidden` — can be applied
171    /// from the cache without another disk scan.
172    pub is_hidden: bool,
173}
174
175impl LoadedEntry {
176    /// `true` if this entry should be visible under `filter`.
177    ///
178    /// The rules mirror [`DirectoryFilter`](crate::DirectoryFilter)'s
179    /// predicates but operate on the per-entry flags we already have
180    /// in hand, keeping the decision O(1) rather than touching the
181    /// filesystem again.
182    pub(crate) fn passes(&self, filter: crate::DirectoryFilter) -> bool {
183        if filter.skips_hidden() && self.is_hidden {
184            return false;
185        }
186        if filter.skips_files() && !self.is_dir {
187            return false;
188        }
189        true
190    }
191}
192
193/// A path → children cache so that collapsing and re-expanding a folder
194/// does not re-scan the filesystem.
195///
196/// The cache stores **unfiltered** children (raw normalised entries).
197/// When the filter changes at runtime, [`DirectoryTree::set_filter`]
198/// re-derives each already-loaded directory's visible child list from
199/// its cached raw entries — no filesystem I/O, no flicker.
200///
201/// [`DirectoryTree::set_filter`]: crate::DirectoryTree::set_filter
202/// (In practice the raw entry set is small — a single directory's
203/// listing — so the extra memory cost of keeping both raw and
204/// filtered forms is not justified at this scale.)
205#[derive(Debug, Default, Clone)]
206pub struct TreeCache {
207    entries: HashMap<PathBuf, CacheEntry>,
208}
209
210/// A single cache line: the raw (unfiltered but normalized) listing
211/// of a directory plus the generation number at which it was
212/// recorded. Read by
213/// [`DirectoryTree::set_filter`](crate::DirectoryTree::set_filter)
214/// to re-derive filtered children without another scan.
215#[derive(Debug, Clone)]
216pub(crate) struct CacheEntry {
217    /// Generation this cache line was recorded with. Stale lines are
218    /// skipped rather than deleted, to avoid churn on repeat expansions.
219    #[allow(dead_code)]
220    pub generation: u64,
221    /// The raw, unfiltered children of the directory.
222    pub raw: Vec<LoadedEntry>,
223}
224
225impl TreeCache {
226    /// Insert or replace the cached entries for `dir`.
227    pub(crate) fn put(&mut self, dir: PathBuf, generation: u64, raw: Vec<LoadedEntry>) {
228        self.entries.insert(dir, CacheEntry { generation, raw });
229    }
230
231    /// Retrieve the raw entries previously recorded for `dir`, if any.
232    pub(crate) fn get(&self, dir: &Path) -> Option<&CacheEntry> {
233        self.entries.get(dir)
234    }
235
236    /// Drop every cached entry. Used when the filter changes in a way
237    /// that could affect membership (hidden → not-hidden, etc.).
238    #[allow(dead_code)]
239    pub(crate) fn clear(&mut self) {
240        self.entries.clear();
241    }
242}
243
244#[cfg(test)]
245mod tests;