iced_swdir_tree/lib.rs
1//! # iced-swdir-tree
2//!
3//! A reusable [`iced`] widget for displaying a directory tree with lazy
4//! loading, filtering, selection, and asynchronous traversal.
5//!
6//! Built on top of [`swdir`]'s `scan_dir` for single-level, non-recursive
7//! directory listings — perfect for GUI trees that expand one folder at a
8//! time.
9//!
10//! ## Minimal example
11//!
12//! ```no_run
13//! use std::path::PathBuf;
14//! use iced::{Element, Task};
15//! use iced_swdir_tree::{DirectoryFilter, DirectoryTree, DirectoryTreeEvent};
16//!
17//! #[derive(Debug, Clone)]
18//! enum Message {
19//! Tree(DirectoryTreeEvent),
20//! }
21//!
22//! struct App {
23//! tree: DirectoryTree,
24//! }
25//!
26//! impl App {
27//! fn new() -> (Self, Task<Message>) {
28//! let tree = DirectoryTree::new(PathBuf::from("."))
29//! .with_filter(DirectoryFilter::FilesAndFolders);
30//! (Self { tree }, Task::none())
31//! }
32//!
33//! fn update(&mut self, message: Message) -> Task<Message> {
34//! match message {
35//! Message::Tree(event) => {
36//! // Observe app-level side effects BEFORE passing to the widget.
37//! // The third field is the `SelectionMode` (Replace/Toggle/ExtendRange);
38//! // match it with `_` when you only care about which path was clicked.
39//! if let DirectoryTreeEvent::Selected(path, is_dir, _) = &event {
40//! println!("selected {:?} (dir={})", path, is_dir);
41//! }
42//! self.tree.update(event).map(Message::Tree)
43//! }
44//! }
45//! }
46//!
47//! fn view(&self) -> Element<'_, Message> {
48//! self.tree.view(Message::Tree)
49//! }
50//! }
51//! # fn main() {}
52//! ```
53//!
54//! ## Multi-select
55//!
56//! Shift/Ctrl-click support is covered in `examples/multi_select.rs`
57//! — the built-in view always emits `SelectionMode::Replace` because
58//! iced 0.14's button `on_press` can't observe modifier keys.
59//! Applications that want multi-select track modifier state via a
60//! keyboard subscription and rewrite the mode in their own update
61//! handler using [`SelectionMode::from_modifiers`].
62//!
63//! ## Drag-and-drop
64//!
65//! The widget tracks drag gestures internally and emits a
66//! [`DragCompleted`](DirectoryTreeEvent::DragCompleted)`{ sources,
67//! destination }` event when the user releases the mouse over a
68//! valid folder row. The widget performs **no** filesystem
69//! operation — the app reacts to `DragCompleted` and does move /
70//! copy / symlink / upload however it likes, then re-scans the
71//! affected folders by emitting a `Toggled` collapse+expand to
72//! refresh. See `examples/drag_drop.rs` for a worked example that
73//! performs `fs::rename` on drop.
74//!
75//! ## Feature flags
76//!
77//! * **`icons`** (off by default) — when enabled, uses [`lucide-icons`] for
78//! folder/file graphics. When disabled, icons fall back to short text
79//! labels (`▸ `, `▾ `, etc.). The public API is identical either way.
80//!
81//! [`iced`]: https://docs.rs/iced
82//! [`swdir`]: https://docs.rs/swdir
83//! [`lucide-icons`]: https://docs.rs/lucide-icons
84
85#![warn(missing_docs)]
86#![cfg_attr(docsrs, feature(doc_cfg))]
87
88mod directory_tree;
89
90pub use crate::directory_tree::{
91 DirectoryTree,
92 config::{DEFAULT_PREFETCH_SKIP, DirectoryFilter, TreeConfig},
93 drag::DragMsg,
94 error::Error,
95 executor::{ScanExecutor, ScanFuture, ScanJob, ThreadExecutor},
96 icon::{IconRole, IconSpec, IconTheme, UnicodeTheme},
97 message::{DirectoryTreeEvent, LoadPayload},
98 node::TreeNode,
99 selection::SelectionMode,
100};
101
102#[cfg(feature = "icons")]
103#[cfg_attr(docsrs, doc(cfg(feature = "icons")))]
104pub use crate::directory_tree::icon::LucideTheme;
105
106#[cfg(feature = "icons")]
107#[cfg_attr(docsrs, doc(cfg(feature = "icons")))]
108pub use lucide_icons::LUCIDE_FONT_BYTES;
109
110/// **Not part of the public API.** Shim exposed for the crate's own
111/// integration tests in `tests/`.
112///
113/// Integration tests need to drive the state machine without running
114/// an iced executor, which means they have to build `Loaded` payloads
115/// whose fields are crate-private. This module exposes a tiny set of
116/// operations that let tests poke at the otherwise-private surface.
117/// It is `#[doc(hidden)]` and not covered by SemVer; downstream
118/// crates must not depend on it.
119#[doc(hidden)]
120pub mod __testing {
121 use std::path::PathBuf;
122 use std::sync::Arc;
123
124 use crate::directory_tree::message::LoadPayload;
125 use crate::directory_tree::node::TreeNode;
126 use crate::directory_tree::walker;
127 use crate::{DirectoryTree, DirectoryTreeEvent};
128
129 /// Toggle `dir` expanded (bumping the generation), synchronously
130 /// scan it through `swdir::scan_dir`, and feed the resulting
131 /// `Loaded` event back into the tree — exactly like the async
132 /// scan task does in production, minus the thread hop.
133 ///
134 /// The filter stored on the tree is applied during the `Loaded`
135 /// event's `update` handling (same as in production), not here.
136 pub fn scan_and_feed(tree: &mut DirectoryTree, dir: PathBuf) {
137 let _ = tree.update(DirectoryTreeEvent::Toggled(dir.clone()));
138 let raw = swdir::scan_dir(&dir);
139 let depth = dir
140 .strip_prefix(tree.root_path())
141 .map(|rel| rel.components().count() as u32)
142 .unwrap_or(u32::MAX);
143 let result = raw
144 .as_ref()
145 .map(|entries| walker::normalize_entries(entries))
146 .map_err(crate::Error::from);
147 let payload = LoadPayload {
148 path: dir,
149 generation: tree.generation,
150 depth,
151 result: Arc::new(result),
152 };
153 let _ = tree.update(DirectoryTreeEvent::Loaded(payload));
154 }
155
156 /// Access the root node for read-only inspection in integration
157 /// tests. Not useful in production code — the crate's public API
158 /// offers everything parent applications need.
159 pub fn root(tree: &DirectoryTree) -> &TreeNode {
160 &tree.root
161 }
162}