iced-swdir-tree 0.7.0

iced widget for file tree powered by swdir, supporting selection, lazy loading and filtering.
Documentation

iced-swdir-tree

crates.io License Documentation Dependency Status

A batteries-included directory-tree widget for iced — lazy-loading, async, multi-select, drag-and-drop, live search.

Overview

A reusable iced widget for displaying a directory tree with selection, lazy loading, filtering, and asynchronous traversal. Built on swdir's scan_dir for single-level, non-recursive directory listings — ideal for GUI trees that expand one folder at a time.

The widget never blocks the UI thread on disk I/O; it never touches the filesystem beyond reading directory listings; and it ships its full event surface (selection, drag-drop, keyboard, search) behind a small, typed API that composes with the iced Task / Subscription model.

When to use it

Reach for this crate when your iced app needs any of:

  • A file/folder picker with multi-select.
  • A project-navigator pane (code editor, asset browser, file-manager side panel).
  • Drag-and-drop between folders — you react to a DragCompleted { sources, destination } event and perform the move/copy/upload yourself.
  • A searchable directory view with real-time type-ahead filtering.

If you only need a one-shot "pick a file" dialog, the OS-native file-chooser is almost certainly a better fit.

Quick start

[dependencies]
iced = "0.14"
iced-swdir-tree = "0.7"
use std::path::PathBuf;
use iced::{Element, Task};
use iced_swdir_tree::{DirectoryFilter, DirectoryTree, DirectoryTreeEvent};

#[derive(Debug, Clone)]
enum Message {
    Tree(DirectoryTreeEvent),
}

struct App {
    tree: DirectoryTree,
}

impl App {
    fn new() -> (Self, Task<Message>) {
        let tree = DirectoryTree::new(PathBuf::from("."))
            .with_filter(DirectoryFilter::FilesAndFolders);
        (Self { tree }, Task::none())
    }

    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::Tree(event) => self.tree.update(event).map(Message::Tree),
        }
    }

    fn view(&self) -> Element<'_, Message> {
        self.tree.view(Message::Tree)
    }
}

fn main() -> iced::Result {
    iced::application(App::new, App::update, App::view).run()
}

For real lucide glyphs instead of the Unicode-symbol fallback, enable the icons feature and register the bundled font:

iced-swdir-tree = { version = "0.7", features = ["icons"] }
iced::application(App::new, App::update, App::view)
    .font(iced_swdir_tree::LUCIDE_FONT_BYTES)
    .run()

To plug in your own icon set (Material, Heroicons, custom labels, …) implement IconTheme and pass it to with_icon_theme — the icons feature can stay off in that case, shaving the lucide TTF out of your binary.

Working apps live in examples/: keyboard_nav, multi_select, drag_drop, search, icon_theme. Run them with cargo run --example <name>.

Design notes

  • Lazy, async, cache-backed. Only the root is eagerly created. Each expansion dispatches a scan through a pluggable ScanExecutor and merges the result back into a generation-tagged cache. Filter changes and search re-derive from the cache — no re-scan.
  • Every feature is orthogonal. Selection survives filter flips, subtree reloads, collapse/re-expand cycles, and search-hidden rows. Drag state is separate from selection state. Search doesn't mutate expansion. These invariants are tested (the crate ships 140+ tests).
  • The widget owns UI state, not filesystem state. It never renames, deletes, moves, or writes. Drag-and-drop produces a DragCompleted event for your app to handle — the widget's job ends at "here's what the user asked for."
  • Safety valves where defaults matter. Prefetch (with_prefetch_limit) won't enter .git, node_modules, target, or other common "don't scan this" directories out of the box; the skip list is configurable. max_depth caps recursion. Generation tags drop stale scan results that returned after a collapse.

Documentation

📚 Full reference in docs/, organized by intent: