iced-swdir-tree
A reusable iced widget for displaying a directory tree with selection, lazy loading, filtering and asynchronous traversal.
Built on top of swdir's scan_dir for
single-level, non-recursive directory listings — ideal for GUI trees that
expand one folder at a time.
Features
- Multi-select with Shift/Ctrl-click and Shift-arrow range extension. A per-path authoritative set survives filter changes and subtree reloads; see Multi-select.
- Drag-and-drop between nodes. Drag one or more selected paths
onto another folder; the widget emits a
DragCompleted { sources, destination }event and the app performs the actual move/copy/ upload/whatever. The widget performs no filesystem operations itself. See Drag-and-drop. - Lazy loading. Only the root is created eagerly; child folders are scanned on first expand.
- Non-blocking. Directory traversal runs on a worker thread through
iced::Task::perform; the UI thread never stalls on disk I/O. Plug in your own executor (tokio,smol, etc.) viawith_executorif you don't want the per-expansion thread-spawn default. - Three display filters.
FoldersOnly,FilesAndFolders(default),AllIncludingHidden. Filter changes are applied from an in-memory cache, so switching is instant — no re-scan. Expansion state and selection survive the swap. - Keyboard navigation. Arrow keys,
Home/End,Enter,Space,←/→, plus Shift-modified variants for range extension andEscapeto cancel a drag — see Keyboard navigation. - Stale-result handling. Every scan carries a generation counter, so a collapse/re-expand cycle safely discards in-flight results from the cancelled round-trip.
- Error tolerance. Permission denials, missing paths, and symlink cycles are surfaced as per-node errors that the view greys out — no panics, no UI freezes.
- Optional lucide icons. Disabled by default; enable the
iconsfeature to pull in real vector glyphs. The public API is identical in both modes. - Cross-platform. Hidden-file detection follows OS conventions: dotfile
on Unix,
HIDDENattribute plus dotfile fallback on Windows, dotfile elsewhere.
Installation
[]
= "0.14"
= "0.4"
To use real lucide icons instead of the Unicode-symbol fallback:
[]
= "0.14"
= { = "0.4", = ["icons"] }
The crate works without your application adding swdir directly — the
widget internally wraps it and exposes the pieces you need through its own
API.
Quick start
use PathBuf;
use ;
use ;
For a working example with a filter picker and a selection status bar, see
examples/basic.rs. For the lucide-icons version, see
examples/with_icons.rs.
Using the icons feature
When icons is enabled, register the bundled lucide TTF with iced at
startup:
use LUCIDE_FONT_BYTES;
Without this registration the icon widgets still render, but as tofu squares — the default system font doesn't have the lucide glyphs.
Configuration
# use PathBuf;
# use ;
let tree = new
.with_filter
.with_max_depth;
| Method | Purpose |
|---|---|
new(root) |
Build a tree rooted at root. Only the root is eagerly created. |
with_filter(f) |
Builder form of set_filter. |
with_max_depth(d) |
Refuse to load below depth d (0 = root children only). |
with_executor(e) |
Route blocking scans through a custom ScanExecutor. |
set_filter(f) |
Change the filter at runtime. Re-derives from cache; no I/O. |
handle_key(k, m) |
Translate a keyboard event into a DirectoryTreeEvent — see Keyboard navigation. |
filter(), max_depth(), root_path() |
Config accessors. |
selected_path() |
Most recently touched path (v0.2 single-select accessor). |
selected_paths() |
The full selected set (v0.3 multi-select). |
anchor_path() |
Pivot for SelectionMode::ExtendRange. |
is_selected(path) |
Membership check. |
Events
The widget emits DirectoryTreeEvent:
Toggled(PathBuf)— the user clicked the caret on a folder.Selected(PathBuf, bool, SelectionMode)— the user selected a row;boolistruefor directories,falsefor files, and theSelectionModecontrols how the event composes with any existing selection.Loaded(LoadPayload)— internal; a pending scan completed. Parent applications route it straight back intoupdate()without inspecting it.
Multi-select
The widget keeps a full selected set, a "most-recent-action" active
path, and an anchor path for Shift-range extension.
[SelectionMode] — exported from the crate root — controls how each
click composes:
| Mode | Effect |
|---|---|
Replace |
Clear the set; the new path becomes the only selection. Updates both active and anchor. |
Toggle |
Add if absent, remove if present. Updates both active and anchor. |
ExtendRange |
Replace the set with the visible rows between anchor and target, inclusive. Only active moves. Falls back to Replace if no anchor is set. |
View-level click behaviour
iced 0.14's button::on_press cannot observe modifier keys, so the
widget's built-in view always emits SelectionMode::Replace on
click. Applications that want real multi-select track modifier
state separately and rewrite the event in their own update handler:
use ;
use ;
// In your update:
Tree =>
ModifiersChanged =>
// In your subscription:
See examples/multi_select.rs for a
complete working app with a live selection-count status bar.
Drag-and-drop
The widget tracks drag gestures internally and emits a
DragCompleted { sources, destination } event when the user
releases over a valid folder. The widget does not touch the
filesystem — your app reacts to DragCompleted and performs the
actual move / copy / upload / whatever, then re-scans affected
folders so the view reflects the new layout.
match message
Pressing the mouse on a row that's already in the selection drags
the whole selected set; pressing on an unselected row drags only
that row. Escape cancels an in-flight drag. If the mouse is
released outside the tree (or over empty space), the drag stays
active until Escape or an app-initiated cancel — deliberately
chosen to match native file-browser behaviour.
Three read-only accessors let your UI reflect drag state:
tree.is_dragging; // bool
tree.drag_sources; // &[PathBuf]
tree.drop_target; // Option<&Path> — hovered valid folder
See examples/drag_drop.rs for a complete
working app with fs::rename on drop, post-move refresh, and a
live drag-preview status bar.
Keyboard navigation
DirectoryTree::handle_key(&Key, Modifiers) -> Option<DirectoryTreeEvent>
translates a key press into the right event. The widget stays
focus-neutral — you decide when the tree has focus and subscribe
to the key stream yourself:
use keyboard;
// ...in update:
TreeKey =>
| Key | Behaviour |
|---|---|
↑ / ↓ |
Move selection to previous / next visible row. |
Shift + ↑ / ↓ |
Extend the selected range toward the previous / next row. |
Home / End |
Jump to first / last visible row. |
Shift + Home / End |
Extend the range to the first / last row. |
Enter |
Toggle the selected directory (no-op on files). |
Space / Ctrl + Space |
Toggle the active path in or out of the selected set. |
← |
Collapse selected directory, or move to parent. |
→ |
Expand selected directory, or move to first child. |
Esc |
Cancel an in-flight drag (only bound during drag, so apps can still use Esc for their own UI otherwise). |
See examples/keyboard_nav.rs for a
single-select navigation demo and
examples/multi_select.rs for
multi-select with Shift/Ctrl-click.
Custom scan executor
By default the widget spawns one std::thread per folder expansion
via ThreadExecutor. Apps that already run a blocking-task pool
(tokio, smol, rayon, ...) can route through it by implementing
ScanExecutor:
use Arc;
use Future;
use Pin;
use ;
;
let tree = new
.with_executor;
The default behaviour is unchanged if you don't call
with_executor — existing v0.1 code keeps working as-is.
Architecture
See ARCHITECTURE.md.