iced_swdir_tree/directory_tree/update.rs
1//! State machine for [`DirectoryTree::update`].
2//!
3//! The dispatcher lives here; each event variant is handled by a
4//! dedicated submodule so that any one handler can grow independently
5//! without the file crossing the "too long to read" threshold again:
6//!
7//! | Event | Handler module |
8//! |-----------------------------------------|--------------------------|
9//! | [`DirectoryTreeEvent::Toggled`] | [`on_toggled`] |
10//! | [`DirectoryTreeEvent::Selected`] | [`on_selected`] |
11//! | [`DirectoryTreeEvent::Drag`] | [`on_drag`] |
12//! | [`DirectoryTreeEvent::Loaded`] | [`on_loaded`] |
13//! | [`DirectoryTreeEvent::DragCompleted`] | inline no-op |
14//!
15//! `DragCompleted` is a broadcast event: the widget's state machine in
16//! [`on_drag`] has already cleared its state by the time the message
17//! reaches the dispatcher, so the dispatcher's job is just to route it
18//! back through the app's message plumbing.
19//!
20//! The returned [`iced::Task`] carries any follow-up the widget needs
21//! to emit — a `Loaded` for an in-flight scan, a delayed `Selected`
22//! or `DragCompleted` for a completed drag gesture, etc.
23
24use iced::Task;
25
26use super::DirectoryTree;
27use super::message::DirectoryTreeEvent;
28
29mod on_drag;
30mod on_loaded;
31mod on_selected;
32mod on_toggled;
33
34impl DirectoryTree {
35 /// Feed an event into the widget.
36 ///
37 /// Returns an `iced::Task` the parent should `.map(..)` back into
38 /// its own message type. For `Selected` this is always
39 /// [`Task::none()`]; for `Toggled` on an unloaded folder it carries
40 /// the pending async scan; for `Loaded` it is again
41 /// [`Task::none()`].
42 ///
43 /// Parent apps typically route every tree-related message here
44 /// unconditionally:
45 ///
46 /// ```ignore
47 /// fn update(&mut self, msg: MyMessage) -> Task<MyMessage> {
48 /// match msg {
49 /// MyMessage::Tree(e) => self.tree.update(e).map(MyMessage::Tree),
50 /// }
51 /// }
52 /// ```
53 pub fn update(&mut self, msg: DirectoryTreeEvent) -> Task<DirectoryTreeEvent> {
54 match msg {
55 DirectoryTreeEvent::Toggled(path) => self.on_toggled(path),
56 DirectoryTreeEvent::Selected(path, is_dir, mode) => {
57 self.on_selected(path, is_dir, mode);
58 Task::none()
59 }
60 DirectoryTreeEvent::Drag(msg) => self.on_drag(msg),
61 DirectoryTreeEvent::DragCompleted { .. } => Task::none(),
62 DirectoryTreeEvent::Loaded(payload) => {
63 // v0.5: `on_loaded` returns the paths (possibly empty)
64 // that the prefetch layer wants scanned next. The
65 // dispatcher is the layer that knows about the
66 // executor, so converting paths → scan Tasks happens
67 // here, not inside the handler.
68 let targets = self.on_loaded(payload);
69 self.issue_prefetch_scans(targets)
70 }
71 }
72 }
73
74 /// Issue background scans for a batch of prefetch targets.
75 ///
76 /// Each target is tracked in `prefetching_paths` so that when the
77 /// scan result arrives, [`on_loaded`](Self::on_loaded) knows to
78 /// drain the flag rather than triggering another cascade of
79 /// prefetches. Returns [`Task::none()`] if the input is empty —
80 /// the common case when prefetch is disabled or the user is
81 /// expanding a folder with no folder-children.
82 fn issue_prefetch_scans(
83 &mut self,
84 targets: Vec<std::path::PathBuf>,
85 ) -> Task<DirectoryTreeEvent> {
86 if targets.is_empty() {
87 return Task::none();
88 }
89 let tasks: Vec<Task<DirectoryTreeEvent>> = targets
90 .into_iter()
91 .map(|p| {
92 // Each prefetch gets its own generation so a later
93 // collapse-and-rescan of the same path invalidates
94 // exactly this stale result.
95 self.generation = self.generation.wrapping_add(1);
96 self.prefetching_paths.insert(p.clone());
97 let depth = depth_of(&self.config.root_path, &p);
98 super::walker::scan(self.executor.clone(), p, self.generation, depth)
99 })
100 .collect();
101 Task::batch(tasks)
102 }
103}
104
105/// Compute the depth of `path` relative to `root`. Returns `0` if they
106/// are equal, `1` for an immediate child, etc. If `path` does not
107/// start with `root` (shouldn't happen in practice — every known node
108/// descends from the root) we return `u32::MAX` so any depth limit
109/// will trivially exclude it.
110///
111/// Shared between [`on_toggled`] (for the max-depth guard) and
112/// [`DirectoryTree::__test_expand_blocking`] below. `pub(super)` so
113/// submodules can import it via `use super::depth_of`.
114pub(super) fn depth_of(root: &std::path::Path, path: &std::path::Path) -> u32 {
115 let Ok(rel) = path.strip_prefix(root) else {
116 return u32::MAX;
117 };
118 rel.components().count() as u32
119}
120
121impl DirectoryTree {
122 /// Synchronously scan `path` and merge the result.
123 ///
124 /// **Test/helper API.** This duplicates the async `Toggled → scan →
125 /// Loaded` round-trip but blocks on the scan, which is what
126 /// integration tests need — iced's `Task` runtime machinery is
127 /// private (see `iced_runtime::task::into_stream`), so driving a
128 /// Task to completion from outside iced requires either standing
129 /// up a window (overkill for unit-level tests) or bypassing the
130 /// Task. This method does the latter.
131 ///
132 /// v0.5: if `config.prefetch_per_parent > 0`, this helper also
133 /// drains the prefetch wave synchronously — the scans that the
134 /// real dispatcher would have dispatched to the executor are
135 /// instead run on this thread in sequence. That's slower than
136 /// production (serial rather than parallel) but gives tests
137 /// deterministic state to assert against without spinning up an
138 /// iced runtime.
139 ///
140 /// Real applications should not call this on the main thread —
141 /// `scan_dir` blocks on `readdir` — and should route events
142 /// through [`DirectoryTree::update`] instead, which delegates the
143 /// scan to a worker thread.
144 #[doc(hidden)]
145 pub fn __test_expand_blocking(&mut self, path: std::path::PathBuf) {
146 // User-initiated leg: scan path, merge, flip is_expanded.
147 self.__expand_blocking_impl(path.clone(), /* flip_expanded= */ true);
148
149 // v0.5 prefetch leg: drain any prefetch targets synchronously.
150 // We look at `select_prefetch_targets` *after* the merge, at
151 // which point the children are populated and the set is
152 // well-defined. Each target gets its `prefetching_paths` flag
153 // set first (so when its `on_loaded` runs, it correctly
154 // identifies itself as a prefetch result and won't cascade).
155 let targets = self.select_prefetch_targets(&path);
156 for t in targets {
157 self.prefetching_paths.insert(t.clone());
158 self.__expand_blocking_impl(t, /* flip_expanded= */ false);
159 }
160 }
161
162 /// Shared core of [`__test_expand_blocking`]: scan, build a
163 /// `LoadPayload`, optionally flip `is_expanded`, feed to
164 /// `on_loaded`. Does **not** look at prefetch — that's one level
165 /// up, in `__test_expand_blocking` proper.
166 fn __expand_blocking_impl(&mut self, path: std::path::PathBuf, flip_expanded: bool) {
167 use super::message::LoadPayload;
168 use std::sync::Arc;
169
170 let depth = depth_of(&self.config.root_path, &path);
171 // Skip the walker::scan Task entirely: call the blocking
172 // primitive directly and hand-assemble the Loaded payload.
173 let result = swdir::scan_dir(&path)
174 .as_ref()
175 .map(|e| super::walker::normalize_entries(e))
176 .map_err(crate::Error::from);
177
178 // Make sure generation matches — bump first, then attach.
179 self.generation = self.generation.wrapping_add(1);
180 let payload = LoadPayload {
181 path: path.clone(),
182 generation: self.generation,
183 depth,
184 result: Arc::new(result),
185 };
186
187 if flip_expanded
188 && let Some(node) = self.root.find_mut(&path)
189 && node.is_dir
190 {
191 node.is_expanded = true;
192 }
193 // The return value (prefetch targets) is intentionally
194 // discarded here: the caller — `__test_expand_blocking` —
195 // computes targets itself from the post-merge tree state.
196 let _targets = self.on_loaded(payload);
197 }
198}
199
200#[cfg(test)]
201mod tests;