Skip to main content

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}