Skip to main content

DirectoryTree

Struct DirectoryTree 

Source
pub struct DirectoryTree { /* private fields */ }
Expand description

A directory tree widget state.

Hold one DirectoryTree per visible tree in your application state. The widget is cheap to construct: DirectoryTree::new creates only the root node — child folders are scanned lazily when the user expands them.

§Lifecycle

  1. DirectoryTree::new — build with a root path.
  2. Optionally chain DirectoryTree::with_filter and/or DirectoryTree::with_max_depth to configure.
  3. Call DirectoryTree::view from your view function.
  4. Route emitted DirectoryTreeEvents through your app’s message system and pass them to DirectoryTree::update, which returns an iced::Task the parent should .map(..) back into its own message type.

Implementations§

Source§

impl DirectoryTree

Source

pub fn handle_key( &self, key: &Key, modifiers: Modifiers, ) -> Option<DirectoryTreeEvent>

Translate a key press into the event that keyboard navigation should produce.

Returns None when the key has no binding in the current state (e.g. Right on a file, or Up when no row is selected and the tree is empty). Callers can safely ignore the None case.

This method is &self — it never mutates the tree. The returned event, if any, must be fed back through DirectoryTree::update like any other event so the existing state-machine (selection set, cache, generation counter) stays authoritative.

§Example
use iced::keyboard;
// ...in your iced subscription function:
fn subscription(app: &App) -> iced::Subscription<Message> {
    keyboard::listen().map(|event| match event {
        keyboard::Event::KeyPressed { key, modifiers, .. } =>
            Message::TreeKey(key, modifiers),
        _ => Message::Noop,
    })
}

// ...in your update:
Message::TreeKey(key, mods) => {
    if let Some(event) = app.tree.handle_key(&key, mods) {
        return app.tree.update(event).map(Message::Tree);
    }
    Task::none()
}
Examples found in repository?
examples/keyboard_nav.rs (line 70)
57    fn update(&mut self, message: Message) -> Task<Message> {
58        match message {
59            Message::Tree(event) => {
60                if let DirectoryTreeEvent::Selected(p, _, _) = &event {
61                    self.last_selected = Some(p.clone());
62                }
63                self.tree.update(event).map(Message::Tree)
64            }
65            Message::Key(key, mods) => {
66                // handle_key is `&self` — it only *produces* an
67                // event. We still have to route the returned event
68                // back through update so state transitions (cursor
69                // move, expand/collapse) actually happen.
70                if let Some(event) = self.tree.handle_key(&key, mods) {
71                    if let DirectoryTreeEvent::Selected(p, _, _) = &event {
72                        self.last_selected = Some(p.clone());
73                    }
74                    return self.tree.update(event).map(Message::Tree);
75                }
76                Task::none()
77            }
78        }
79    }
More examples
Hide additional examples
examples/multi_select.rs (line 85)
68    fn update(&mut self, message: Message) -> Task<Message> {
69        match message {
70            // Intercept plain-click `Selected` events and rewrite the
71            // mode based on the current modifier state. The built-in
72            // view always produces Replace; keyboard events produce
73            // the right mode already (handled by handle_key).
74            Message::Tree(DirectoryTreeEvent::Selected(path, is_dir, _from_view)) => {
75                let mode = SelectionMode::from_modifiers(self.modifiers);
76                let event = DirectoryTreeEvent::Selected(path, is_dir, mode);
77                self.tree.update(event).map(Message::Tree)
78            }
79            Message::Tree(event) => self.tree.update(event).map(Message::Tree),
80            Message::ModifiersChanged(m) => {
81                self.modifiers = m;
82                Task::none()
83            }
84            Message::Key(key, mods) => {
85                if let Some(event) = self.tree.handle_key(&key, mods) {
86                    return self.tree.update(event).map(Message::Tree);
87                }
88                Task::none()
89            }
90        }
91    }
examples/drag_drop.rs (line 129)
77    fn update(&mut self, message: Message) -> Task<Message> {
78        match message {
79            // As in the multi-select example, rewrite the built-in
80            // view's `Replace`-only `Selected` events using the
81            // current modifier state. Keyboard events come through
82            // `handle_key` with the correct mode already.
83            Message::Tree(DirectoryTreeEvent::Selected(path, is_dir, _)) => {
84                let mode = SelectionMode::from_modifiers(self.modifiers);
85                let event = DirectoryTreeEvent::Selected(path, is_dir, mode);
86                self.tree.update(event).map(Message::Tree)
87            }
88            // The headline case: the user released the mouse over a
89            // valid drop target. Perform the actual filesystem
90            // operation, then refresh affected folders so the tree
91            // view reflects the new layout.
92            Message::Tree(DirectoryTreeEvent::DragCompleted {
93                sources,
94                destination,
95            }) => {
96                let outcome = move_paths(&sources, &destination);
97                self.status = outcome.summary();
98                // The set of folders that need re-scanning: the
99                // destination (for the newly-arrived entries) and
100                // every source's parent (for the departed entries).
101                let mut to_refresh: HashSet<PathBuf> = HashSet::new();
102                to_refresh.insert(destination);
103                for s in &sources {
104                    if let Some(parent) = s.parent() {
105                        to_refresh.insert(parent.to_path_buf());
106                    }
107                }
108                // Issue a collapse+expand for each affected folder.
109                // A collapse followed by a `Toggled` on the same
110                // path is the simplest way in v0.4 to invalidate
111                // the cached children and re-scan from scratch.
112                let tasks: Vec<Task<Message>> = to_refresh
113                    .into_iter()
114                    .flat_map(|p| {
115                        [
116                            Task::done(Message::Tree(DirectoryTreeEvent::Toggled(p.clone()))),
117                            Task::done(Message::Tree(DirectoryTreeEvent::Toggled(p))),
118                        ]
119                    })
120                    .collect();
121                Task::batch(tasks)
122            }
123            Message::Tree(event) => self.tree.update(event).map(Message::Tree),
124            Message::ModifiersChanged(m) => {
125                self.modifiers = m;
126                Task::none()
127            }
128            Message::Key(key, mods) => {
129                if let Some(event) = self.tree.handle_key(&key, mods) {
130                    return self.tree.update(event).map(Message::Tree);
131                }
132                Task::none()
133            }
134        }
135    }
Source§

impl DirectoryTree

Source

pub fn update(&mut self, msg: DirectoryTreeEvent) -> Task<DirectoryTreeEvent>

Feed an event into the widget.

Returns an iced::Task the parent should .map(..) back into its own message type. For Selected this is always Task::none(); for Toggled on an unloaded folder it carries the pending async scan; for Loaded it is again Task::none().

Parent apps typically route every tree-related message here unconditionally:

fn update(&mut self, msg: MyMessage) -> Task<MyMessage> {
    match msg {
        MyMessage::Tree(e) => self.tree.update(e).map(MyMessage::Tree),
    }
}
Examples found in repository?
examples/with_icons.rs (line 53)
47    fn update(&mut self, message: Message) -> Task<Message> {
48        match message {
49            Message::Tree(event) => {
50                if let DirectoryTreeEvent::Selected(p, _, _) = &event {
51                    self.last_selected = Some(p.clone());
52                }
53                self.tree.update(event).map(Message::Tree)
54            }
55            Message::SetFilter(filter) => {
56                self.tree.set_filter(filter);
57                Task::none()
58            }
59        }
60    }
More examples
Hide additional examples
examples/icon_theme.rs (line 130)
118    fn new() -> (Self, Task<Message>) {
119        let root = resolve_root();
120        let tree = DirectoryTree::new(root.clone())
121            .with_filter(DirectoryFilter::FilesAndFolders)
122            .with_icon_theme(ThemeChoice::Unicode.to_theme());
123        let mut app = App {
124            tree,
125            choice: ThemeChoice::Unicode,
126            root: root.clone(),
127        };
128        let task = app
129            .tree
130            .update(DirectoryTreeEvent::Toggled(root))
131            .map(Message::Tree);
132        (app, task)
133    }
134
135    fn update(&mut self, msg: Message) -> Task<Message> {
136        match msg {
137            Message::Tree(ev) => self.tree.update(ev).map(Message::Tree),
138            Message::ThemePicked(choice) => {
139                // Swapping a theme today requires rebuilding the
140                // tree (it's set at construction via the builder).
141                // We preserve nothing across the swap — a real app
142                // would carry selection/expansion forward, but
143                // this is a demo so a clean rebuild keeps it
144                // short.
145                self.choice = choice;
146                self.tree = DirectoryTree::new(self.root.clone())
147                    .with_filter(DirectoryFilter::FilesAndFolders)
148                    .with_icon_theme(choice.to_theme());
149                self.tree
150                    .update(DirectoryTreeEvent::Toggled(self.root.clone()))
151                    .map(Message::Tree)
152            }
153        }
154    }
examples/basic.rs (line 52)
44    fn update(&mut self, message: Message) -> Task<Message> {
45        match message {
46            Message::Tree(event) => {
47                // Side-effect: remember the last selection so we can
48                // show it in the status bar.
49                if let DirectoryTreeEvent::Selected(p, _, _) = &event {
50                    self.last_selected = Some(p.clone());
51                }
52                self.tree.update(event).map(Message::Tree)
53            }
54            Message::SetFilter(filter) => {
55                self.tree.set_filter(filter);
56                Task::none()
57            }
58        }
59    }
examples/search.rs (line 65)
51    fn new() -> (Self, Task<Message>) {
52        let root = resolve_root();
53        let tree = DirectoryTree::new(root.clone())
54            .with_filter(DirectoryFilter::FilesAndFolders)
55            // Prefetch one level helps search cover more ground
56            // without the user expanding everything manually.
57            .with_prefetch_limit(20);
58        let mut app = App {
59            tree,
60            query: String::new(),
61        };
62        // Kick off the initial scan of the root.
63        let task = app
64            .tree
65            .update(DirectoryTreeEvent::Toggled(root))
66            .map(Message::Tree);
67        (app, task)
68    }
69
70    fn update(&mut self, msg: Message) -> Task<Message> {
71        match msg {
72            Message::Tree(ev) => self.tree.update(ev).map(Message::Tree),
73            Message::SearchChanged(q) => {
74                self.query = q.clone();
75                self.tree.set_search_query(q);
76                Task::none()
77            }
78            Message::ExpandAll => {
79                // Toggle every loaded folder that isn't already
80                // expanded. The widget's on_loaded handler will
81                // cascade more scans via prefetch.
82                let mut tasks = Vec::new();
83                let to_expand = collect_collapsed_folders(&self.tree);
84                for p in to_expand {
85                    tasks.push(
86                        self.tree
87                            .update(DirectoryTreeEvent::Toggled(p))
88                            .map(Message::Tree),
89                    );
90                }
91                Task::batch(tasks)
92            }
93        }
94    }
examples/keyboard_nav.rs (line 63)
57    fn update(&mut self, message: Message) -> Task<Message> {
58        match message {
59            Message::Tree(event) => {
60                if let DirectoryTreeEvent::Selected(p, _, _) = &event {
61                    self.last_selected = Some(p.clone());
62                }
63                self.tree.update(event).map(Message::Tree)
64            }
65            Message::Key(key, mods) => {
66                // handle_key is `&self` — it only *produces* an
67                // event. We still have to route the returned event
68                // back through update so state transitions (cursor
69                // move, expand/collapse) actually happen.
70                if let Some(event) = self.tree.handle_key(&key, mods) {
71                    if let DirectoryTreeEvent::Selected(p, _, _) = &event {
72                        self.last_selected = Some(p.clone());
73                    }
74                    return self.tree.update(event).map(Message::Tree);
75                }
76                Task::none()
77            }
78        }
79    }
examples/multi_select.rs (line 77)
68    fn update(&mut self, message: Message) -> Task<Message> {
69        match message {
70            // Intercept plain-click `Selected` events and rewrite the
71            // mode based on the current modifier state. The built-in
72            // view always produces Replace; keyboard events produce
73            // the right mode already (handled by handle_key).
74            Message::Tree(DirectoryTreeEvent::Selected(path, is_dir, _from_view)) => {
75                let mode = SelectionMode::from_modifiers(self.modifiers);
76                let event = DirectoryTreeEvent::Selected(path, is_dir, mode);
77                self.tree.update(event).map(Message::Tree)
78            }
79            Message::Tree(event) => self.tree.update(event).map(Message::Tree),
80            Message::ModifiersChanged(m) => {
81                self.modifiers = m;
82                Task::none()
83            }
84            Message::Key(key, mods) => {
85                if let Some(event) = self.tree.handle_key(&key, mods) {
86                    return self.tree.update(event).map(Message::Tree);
87                }
88                Task::none()
89            }
90        }
91    }
Source§

impl DirectoryTree

Source

pub fn view<'a, Message, F>(&'a self, on_event: F) -> Element<'a, Message>
where Message: Clone + 'a, F: Fn(DirectoryTreeEvent) -> Message + Copy + 'a,

Build an iced::Element that renders this tree.

on_event is the closure that maps the widget’s internal DirectoryTreeEvents into the parent application’s own message type. See the crate-level docs for a worked example.

Examples found in repository?
examples/keyboard_nav.rs (line 115)
104    fn view(&self) -> Element<'_, Message> {
105        let status = text(match &self.last_selected {
106            Some(p) => format!(
107                "Selected: {}  |  Try ↑ ↓ ← →, Enter, Space, Home, End.",
108                p.display()
109            ),
110            None => "Press ↓ to select the first row.".into(),
111        })
112        .size(12);
113
114        container(
115            column![self.tree.view(Message::Tree), status]
116                .spacing(8.0)
117                .padding(8.0),
118        )
119        .width(Length::Fill)
120        .height(Length::Fill)
121        .into()
122    }
More examples
Hide additional examples
examples/icon_theme.rs (line 166)
156    fn view(&self) -> Element<'_, Message> {
157        let picker = pick_list(
158            &ThemeChoice::ALL[..],
159            Some(self.choice),
160            Message::ThemePicked,
161        );
162        let header = row![text("Theme:").size(13), picker].spacing(8);
163
164        column![
165            header,
166            container(self.tree.view(Message::Tree))
167                .width(Length::Fill)
168                .height(Length::Fill),
169            text(
170                "Switch themes to see the icon trait in action. \
171                 The 'Label' and 'Ascii' themes are defined in \
172                 this example file; 'Unicode' is shipped with \
173                 the crate."
174            )
175            .size(12),
176        ]
177        .spacing(8)
178        .padding(10)
179        .into()
180    }
examples/drag_drop.rs (line 171)
148    fn view(&self) -> Element<'_, Message> {
149        // While a drag is in progress, override the static status
150        // line with a live preview of where the drop will land.
151        let live_status = if self.tree.is_dragging() {
152            match self.tree.drop_target() {
153                Some(dest) => format!(
154                    "Drop {} onto {}?",
155                    describe_sources(self.tree.drag_sources()),
156                    short_name(dest),
157                ),
158                None => format!(
159                    "Dragging {} — hover over a folder",
160                    describe_sources(self.tree.drag_sources()),
161                ),
162            }
163        } else {
164            self.status.clone()
165        };
166
167        let status = Column::new().push(text(live_status).size(13)).spacing(2);
168
169        container(
170            column![
171                scrollable(self.tree.view(Message::Tree)).height(Length::Fill),
172                status,
173            ]
174            .spacing(8.0)
175            .padding(8.0),
176        )
177        .width(Length::Fill)
178        .height(Length::Fill)
179        .into()
180    }
examples/search.rs (line 126)
96    fn view(&self) -> Element<'_, Message> {
97        let search_bar = text_input("Search filenames...", &self.query)
98            .on_input(Message::SearchChanged)
99            .padding(6);
100
101        let status_text = if self.tree.is_searching() {
102            format!(
103                "{} match{} for \"{}\"",
104                self.tree.search_match_count(),
105                if self.tree.search_match_count() == 1 {
106                    ""
107                } else {
108                    "es"
109                },
110                self.query,
111            )
112        } else {
113            "Type above to filter. Press Expand-all to load deeper \
114             folders for broader coverage."
115                .into()
116        };
117
118        let controls = row![
119            search_bar,
120            button(text("Expand all")).on_press(Message::ExpandAll),
121        ]
122        .spacing(8);
123
124        column![
125            controls,
126            container(self.tree.view(Message::Tree))
127                .width(Length::Fill)
128                .height(Length::Fill),
129            text(status_text).size(13),
130        ]
131        .spacing(8)
132        .padding(10)
133        .into()
134    }
examples/with_icons.rs (line 99)
62    fn view(&self) -> Element<'_, Message> {
63        use iced::widget::{button, column, container, row, text};
64
65        let filter_row = row![
66            text("Filter:"),
67            button("Folders only")
68                .on_press(Message::SetFilter(DirectoryFilter::FoldersOnly))
69                .style(if self.tree.filter() == DirectoryFilter::FoldersOnly {
70                    button::primary
71                } else {
72                    button::secondary
73                }),
74            button("Files + folders")
75                .on_press(Message::SetFilter(DirectoryFilter::FilesAndFolders))
76                .style(if self.tree.filter() == DirectoryFilter::FilesAndFolders {
77                    button::primary
78                } else {
79                    button::secondary
80                }),
81            button("All (w/ hidden)")
82                .on_press(Message::SetFilter(DirectoryFilter::AllIncludingHidden))
83                .style(
84                    if self.tree.filter() == DirectoryFilter::AllIncludingHidden {
85                        button::primary
86                    } else {
87                        button::secondary
88                    },
89                ),
90        ]
91        .spacing(8.0);
92
93        let status = text(match &self.last_selected {
94            Some(p) => format!("Selected: {}", p.display()),
95            None => "No selection yet. Click any row to select; click folders to expand.".into(),
96        });
97
98        container(
99            column![filter_row, self.tree.view(Message::Tree), status]
100                .spacing(8.0)
101                .padding(8.0),
102        )
103        .width(Length::Fill)
104        .height(Length::Fill)
105        .into()
106    }
examples/basic.rs (line 100)
61    fn view(&self) -> Element<'_, Message> {
62        use iced::widget::{button, column, container, row, text};
63
64        // Three plain buttons for filter selection. Keeps the example
65        // dependency-free of `Display` impls or pick_list complexity.
66        let filter_row = row![
67            text("Filter:"),
68            button("Folders only")
69                .on_press(Message::SetFilter(DirectoryFilter::FoldersOnly))
70                .style(if self.tree.filter() == DirectoryFilter::FoldersOnly {
71                    button::primary
72                } else {
73                    button::secondary
74                }),
75            button("Files + folders")
76                .on_press(Message::SetFilter(DirectoryFilter::FilesAndFolders))
77                .style(if self.tree.filter() == DirectoryFilter::FilesAndFolders {
78                    button::primary
79                } else {
80                    button::secondary
81                }),
82            button("All (w/ hidden)")
83                .on_press(Message::SetFilter(DirectoryFilter::AllIncludingHidden))
84                .style(
85                    if self.tree.filter() == DirectoryFilter::AllIncludingHidden {
86                        button::primary
87                    } else {
88                        button::secondary
89                    },
90                ),
91        ]
92        .spacing(8.0);
93
94        let status = text(match &self.last_selected {
95            Some(p) => format!("Selected: {}", p.display()),
96            None => "No selection yet. Click any row to select; click folders to expand.".into(),
97        });
98
99        container(
100            column![filter_row, self.tree.view(Message::Tree), status]
101                .spacing(8.0)
102                .padding(8.0),
103        )
104        .width(Length::Fill)
105        .height(Length::Fill)
106        .into()
107    }
Source§

impl DirectoryTree

Source

pub fn new(root: PathBuf) -> Self

Create a new tree rooted at root.

Only the root node is created eagerly; the first level of children is scanned when the user first expands the root (or, for convenience, when you call DirectoryTree::update with a Toggled(root) event yourself).

Defaults: DirectoryFilter::FilesAndFolders, no depth limit.

Examples found in repository?
examples/basic.rs (line 34)
29    fn new() -> (Self, Task<Message>) {
30        let root = std::env::args()
31            .nth(1)
32            .map(PathBuf::from)
33            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
34        let tree = DirectoryTree::new(root).with_filter(DirectoryFilter::FilesAndFolders);
35        (
36            Self {
37                tree,
38                last_selected: None,
39            },
40            Task::none(),
41        )
42    }
More examples
Hide additional examples
examples/with_icons.rs (line 37)
32    fn new() -> (Self, Task<Message>) {
33        let root = std::env::args()
34            .nth(1)
35            .map(PathBuf::from)
36            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
37        let tree = DirectoryTree::new(root).with_filter(DirectoryFilter::FilesAndFolders);
38        (
39            Self {
40                tree,
41                last_selected: None,
42            },
43            Task::none(),
44        )
45    }
examples/icon_theme.rs (line 120)
118    fn new() -> (Self, Task<Message>) {
119        let root = resolve_root();
120        let tree = DirectoryTree::new(root.clone())
121            .with_filter(DirectoryFilter::FilesAndFolders)
122            .with_icon_theme(ThemeChoice::Unicode.to_theme());
123        let mut app = App {
124            tree,
125            choice: ThemeChoice::Unicode,
126            root: root.clone(),
127        };
128        let task = app
129            .tree
130            .update(DirectoryTreeEvent::Toggled(root))
131            .map(Message::Tree);
132        (app, task)
133    }
134
135    fn update(&mut self, msg: Message) -> Task<Message> {
136        match msg {
137            Message::Tree(ev) => self.tree.update(ev).map(Message::Tree),
138            Message::ThemePicked(choice) => {
139                // Swapping a theme today requires rebuilding the
140                // tree (it's set at construction via the builder).
141                // We preserve nothing across the swap — a real app
142                // would carry selection/expansion forward, but
143                // this is a demo so a clean rebuild keeps it
144                // short.
145                self.choice = choice;
146                self.tree = DirectoryTree::new(self.root.clone())
147                    .with_filter(DirectoryFilter::FilesAndFolders)
148                    .with_icon_theme(choice.to_theme());
149                self.tree
150                    .update(DirectoryTreeEvent::Toggled(self.root.clone()))
151                    .map(Message::Tree)
152            }
153        }
154    }
examples/drag_drop.rs (line 63)
60    fn new() -> (Self, Task<Message>) {
61        let root = resolve_root();
62        let root_for_task = root.clone();
63        let tree = DirectoryTree::new(root).with_filter(DirectoryFilter::FilesAndFolders);
64        (
65            Self {
66                tree,
67                modifiers: Modifiers::default(),
68                status: "Drag a row onto a folder to move it. \
69                         Shift/Ctrl-click for multi-select. \
70                         Esc cancels an in-flight drag."
71                    .to_string(),
72            },
73            Task::done(Message::Tree(DirectoryTreeEvent::Toggled(root_for_task))),
74        )
75    }
examples/multi_select.rs (line 57)
51    fn new() -> (Self, Task<Message>) {
52        let root = std::env::args()
53            .nth(1)
54            .map(PathBuf::from)
55            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
56        let root_for_task = root.clone();
57        let tree = DirectoryTree::new(root).with_filter(DirectoryFilter::FilesAndFolders);
58        (
59            Self {
60                tree,
61                modifiers: Modifiers::default(),
62            },
63            // Kick off the first expansion so the user sees content.
64            Task::done(Message::Tree(DirectoryTreeEvent::Toggled(root_for_task))),
65        )
66    }
examples/search.rs (line 53)
51    fn new() -> (Self, Task<Message>) {
52        let root = resolve_root();
53        let tree = DirectoryTree::new(root.clone())
54            .with_filter(DirectoryFilter::FilesAndFolders)
55            // Prefetch one level helps search cover more ground
56            // without the user expanding everything manually.
57            .with_prefetch_limit(20);
58        let mut app = App {
59            tree,
60            query: String::new(),
61        };
62        // Kick off the initial scan of the root.
63        let task = app
64            .tree
65            .update(DirectoryTreeEvent::Toggled(root))
66            .map(Message::Tree);
67        (app, task)
68    }
Source

pub fn with_filter(self, filter: DirectoryFilter) -> Self

Set the display filter.

This is the builder form used at construction. For runtime filter changes call DirectoryTree::set_filter — or use this method with std::mem::replace / std::mem::take-style moves if that fits the shape of your state better. Either route re-derives visible children from the cache, so the tree updates instantly without re-scanning the filesystem.

Examples found in repository?
examples/basic.rs (line 34)
29    fn new() -> (Self, Task<Message>) {
30        let root = std::env::args()
31            .nth(1)
32            .map(PathBuf::from)
33            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
34        let tree = DirectoryTree::new(root).with_filter(DirectoryFilter::FilesAndFolders);
35        (
36            Self {
37                tree,
38                last_selected: None,
39            },
40            Task::none(),
41        )
42    }
More examples
Hide additional examples
examples/with_icons.rs (line 37)
32    fn new() -> (Self, Task<Message>) {
33        let root = std::env::args()
34            .nth(1)
35            .map(PathBuf::from)
36            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
37        let tree = DirectoryTree::new(root).with_filter(DirectoryFilter::FilesAndFolders);
38        (
39            Self {
40                tree,
41                last_selected: None,
42            },
43            Task::none(),
44        )
45    }
examples/icon_theme.rs (line 121)
118    fn new() -> (Self, Task<Message>) {
119        let root = resolve_root();
120        let tree = DirectoryTree::new(root.clone())
121            .with_filter(DirectoryFilter::FilesAndFolders)
122            .with_icon_theme(ThemeChoice::Unicode.to_theme());
123        let mut app = App {
124            tree,
125            choice: ThemeChoice::Unicode,
126            root: root.clone(),
127        };
128        let task = app
129            .tree
130            .update(DirectoryTreeEvent::Toggled(root))
131            .map(Message::Tree);
132        (app, task)
133    }
134
135    fn update(&mut self, msg: Message) -> Task<Message> {
136        match msg {
137            Message::Tree(ev) => self.tree.update(ev).map(Message::Tree),
138            Message::ThemePicked(choice) => {
139                // Swapping a theme today requires rebuilding the
140                // tree (it's set at construction via the builder).
141                // We preserve nothing across the swap — a real app
142                // would carry selection/expansion forward, but
143                // this is a demo so a clean rebuild keeps it
144                // short.
145                self.choice = choice;
146                self.tree = DirectoryTree::new(self.root.clone())
147                    .with_filter(DirectoryFilter::FilesAndFolders)
148                    .with_icon_theme(choice.to_theme());
149                self.tree
150                    .update(DirectoryTreeEvent::Toggled(self.root.clone()))
151                    .map(Message::Tree)
152            }
153        }
154    }
examples/drag_drop.rs (line 63)
60    fn new() -> (Self, Task<Message>) {
61        let root = resolve_root();
62        let root_for_task = root.clone();
63        let tree = DirectoryTree::new(root).with_filter(DirectoryFilter::FilesAndFolders);
64        (
65            Self {
66                tree,
67                modifiers: Modifiers::default(),
68                status: "Drag a row onto a folder to move it. \
69                         Shift/Ctrl-click for multi-select. \
70                         Esc cancels an in-flight drag."
71                    .to_string(),
72            },
73            Task::done(Message::Tree(DirectoryTreeEvent::Toggled(root_for_task))),
74        )
75    }
examples/multi_select.rs (line 57)
51    fn new() -> (Self, Task<Message>) {
52        let root = std::env::args()
53            .nth(1)
54            .map(PathBuf::from)
55            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
56        let root_for_task = root.clone();
57        let tree = DirectoryTree::new(root).with_filter(DirectoryFilter::FilesAndFolders);
58        (
59            Self {
60                tree,
61                modifiers: Modifiers::default(),
62            },
63            // Kick off the first expansion so the user sees content.
64            Task::done(Message::Tree(DirectoryTreeEvent::Toggled(root_for_task))),
65        )
66    }
examples/search.rs (line 54)
51    fn new() -> (Self, Task<Message>) {
52        let root = resolve_root();
53        let tree = DirectoryTree::new(root.clone())
54            .with_filter(DirectoryFilter::FilesAndFolders)
55            // Prefetch one level helps search cover more ground
56            // without the user expanding everything manually.
57            .with_prefetch_limit(20);
58        let mut app = App {
59            tree,
60            query: String::new(),
61        };
62        // Kick off the initial scan of the root.
63        let task = app
64            .tree
65            .update(DirectoryTreeEvent::Toggled(root))
66            .map(Message::Tree);
67        (app, task)
68    }
Source

pub fn with_max_depth(self, depth: u32) -> Self

Limit how deep the widget will load. depth == 0 means only the root’s direct children are ever loaded; depth == 1 allows one more level of descent; and so on. No limit by default.

Source

pub fn with_prefetch_limit(self, limit: usize) -> Self

v0.5: configure parallel pre-expansion of visible descendants.

When a user-initiated expansion finishes loading a folder, the widget will eagerly issue background scans for up to limit of that folder’s direct children-that-are-folders, in parallel via the widget’s ScanExecutor. Those children’s data is loaded into the in-memory cache (is_loaded = true) but they are not automatically expanded in the UI — the user still controls what’s drawn. When they click to expand a prefetched child, no I/O happens: the expansion is instant.

Passing 0 (the default) disables prefetch and restores v0.1–0.4 behaviour exactly. Typical app values: 525, sized to the number of folder-children a user plausibly targets with their next click. A very high value effectively means “prefetch every child folder” — the crate doesn’t cap it, because apps with fast executors (tokio / rayon / smol) can legitimately want that.

let tree = DirectoryTree::new(root)
    .with_executor(my_tokio_executor)   // fast pool
    .with_prefetch_limit(20);           // up to 20 parallel scans

Prefetch is one level deep only: a folder that loaded via prefetch does not itself trigger further prefetches. This avoids the exponential limit ^ depth cascade that would otherwise paper-over I/O costs the user didn’t ask for.

Prefetch respects with_max_depth the same way user-initiated scans do — a prefetch target past the cap is skipped, not scanned.

See TreeConfig::prefetch_per_parent for the full contract.

Examples found in repository?
examples/search.rs (line 57)
51    fn new() -> (Self, Task<Message>) {
52        let root = resolve_root();
53        let tree = DirectoryTree::new(root.clone())
54            .with_filter(DirectoryFilter::FilesAndFolders)
55            // Prefetch one level helps search cover more ground
56            // without the user expanding everything manually.
57            .with_prefetch_limit(20);
58        let mut app = App {
59            tree,
60            query: String::new(),
61        };
62        // Kick off the initial scan of the root.
63        let task = app
64            .tree
65            .update(DirectoryTreeEvent::Toggled(root))
66            .map(Message::Tree);
67        (app, task)
68    }
Source

pub fn with_prefetch_skip<I, S>(self, names: I) -> Self
where I: IntoIterator<Item = S>, S: Into<String>,

v0.6.1: replace the prefetch skip list.

The list holds basenames that with_prefetch_limit- driven scans will refuse to enter. Match is exact-basename, ASCII case-insensitive"target" skips target/ and Target/ but not my-target-files/. The list applies only to automatic prefetch; a user click on a skipped folder still expands it normally.

Replacing the list drops the default entries (see DEFAULT_PREFETCH_SKIP). To add entries while keeping the defaults, read them and append:

use iced_swdir_tree::{DirectoryTree, DEFAULT_PREFETCH_SKIP};

let mut skip: Vec<String> = DEFAULT_PREFETCH_SKIP
    .iter()
    .map(|&s| s.to_string())
    .collect();
skip.push("huge_media_library".into());

let tree = DirectoryTree::new(root)
    .with_prefetch_limit(10)
    .with_prefetch_skip(skip);

To disable skipping entirely (dangerous — means .git/ and node_modules/ will be prefetched), pass an empty list:

let tree = DirectoryTree::new(root)
    .with_prefetch_limit(10)
    .with_prefetch_skip(Vec::<String>::new());

See DEFAULT_PREFETCH_SKIP for the set populated by default.

Source

pub fn with_executor(self, executor: Arc<dyn ScanExecutor>) -> Self

Route blocking scan_dir calls through a custom executor.

By default the widget spawns a fresh std::thread per expansion via ThreadExecutor. Apps that already manage a blocking-task pool (tokio, smol, rayon, …) can implement ScanExecutor and swap it in here:

use std::sync::Arc;
let tree = DirectoryTree::new(root).with_executor(Arc::new(MyTokioExecutor));

Calling this mid-session is allowed (the next scan will use the new executor); in-flight scans initiated under the old executor still complete through it.

Source

pub fn with_icon_theme(self, theme: Arc<dyn IconTheme>) -> Self

v0.7: replace the icon theme.

Install an IconTheme implementation to control which glyph, font, and size the view uses for each IconRole (folder-closed / folder-open / file / caret-right / caret-down / error).

The crate ships two stock themes:

  • UnicodeTheme — always available, renders short Unicode symbols (📁 📂 📄 ⚠ ▸ ▾). Default when the icons feature is disabled.
  • LucideTheme — available with the icons feature, renders real lucide vector glyphs. Default when icons is enabled.

You don’t need to call this if you’re happy with the stock default — DirectoryTree::new picks the right one for your feature configuration automatically.

Custom themes implement the IconTheme trait. A minimal example:

use std::sync::Arc;
use iced_swdir_tree::{
    DirectoryTree, IconRole, IconSpec, IconTheme,
};

#[derive(Debug)]
struct LabelTheme;

impl IconTheme for LabelTheme {
    fn glyph(&self, role: IconRole) -> IconSpec {
        let label: &'static str = match role {
            IconRole::FolderClosed => "[D]",
            IconRole::FolderOpen => "[O]",
            IconRole::File => "[F]",
            IconRole::Error => "[!]",
            IconRole::CaretRight => ">",
            IconRole::CaretDown => "v",
            _ => "?",
        };
        IconSpec::new(label)
    }
}

let tree = DirectoryTree::new(".".into())
    .with_icon_theme(Arc::new(LabelTheme));

Note the _ => fallback: IconRole is #[non_exhaustive] so new variants may be added in future minor releases.

Examples found in repository?
examples/icon_theme.rs (line 122)
118    fn new() -> (Self, Task<Message>) {
119        let root = resolve_root();
120        let tree = DirectoryTree::new(root.clone())
121            .with_filter(DirectoryFilter::FilesAndFolders)
122            .with_icon_theme(ThemeChoice::Unicode.to_theme());
123        let mut app = App {
124            tree,
125            choice: ThemeChoice::Unicode,
126            root: root.clone(),
127        };
128        let task = app
129            .tree
130            .update(DirectoryTreeEvent::Toggled(root))
131            .map(Message::Tree);
132        (app, task)
133    }
134
135    fn update(&mut self, msg: Message) -> Task<Message> {
136        match msg {
137            Message::Tree(ev) => self.tree.update(ev).map(Message::Tree),
138            Message::ThemePicked(choice) => {
139                // Swapping a theme today requires rebuilding the
140                // tree (it's set at construction via the builder).
141                // We preserve nothing across the swap — a real app
142                // would carry selection/expansion forward, but
143                // this is a demo so a clean rebuild keeps it
144                // short.
145                self.choice = choice;
146                self.tree = DirectoryTree::new(self.root.clone())
147                    .with_filter(DirectoryFilter::FilesAndFolders)
148                    .with_icon_theme(choice.to_theme());
149                self.tree
150                    .update(DirectoryTreeEvent::Toggled(self.root.clone()))
151                    .map(Message::Tree)
152            }
153        }
154    }
Source

pub fn set_filter(&mut self, filter: DirectoryFilter)

Change the display filter at runtime. The tree re-derives its visible children from the unfiltered cache, so the change is instant — no re-scan, no blocking the UI.

Selection is preserved. Selection is kept by path on the widget, not on the TreeNodes that this call rebuilds, so every selected path survives the filter swap. Paths that become invisible under the new filter are not lost — flipping the filter back re-reveals them unchanged. This is true for both single and multi-select.

Expansion state is preserved too. rebuild_from_cache copies the whole child subtree from the old node into its freshly-built replacement, so directories the user had opened stay open.

Examples found in repository?
examples/with_icons.rs (line 56)
47    fn update(&mut self, message: Message) -> Task<Message> {
48        match message {
49            Message::Tree(event) => {
50                if let DirectoryTreeEvent::Selected(p, _, _) = &event {
51                    self.last_selected = Some(p.clone());
52                }
53                self.tree.update(event).map(Message::Tree)
54            }
55            Message::SetFilter(filter) => {
56                self.tree.set_filter(filter);
57                Task::none()
58            }
59        }
60    }
More examples
Hide additional examples
examples/basic.rs (line 55)
44    fn update(&mut self, message: Message) -> Task<Message> {
45        match message {
46            Message::Tree(event) => {
47                // Side-effect: remember the last selection so we can
48                // show it in the status bar.
49                if let DirectoryTreeEvent::Selected(p, _, _) = &event {
50                    self.last_selected = Some(p.clone());
51                }
52                self.tree.update(event).map(Message::Tree)
53            }
54            Message::SetFilter(filter) => {
55                self.tree.set_filter(filter);
56                Task::none()
57            }
58        }
59    }
Source

pub fn root_path(&self) -> &Path

Return the root path.

Examples found in repository?
examples/search.rs (line 161)
140fn collect_collapsed_folders(tree: &DirectoryTree) -> Vec<PathBuf> {
141    // There's no public "walk every node" API, so we do a BFS by
142    // repeatedly querying visible_rows() of the tree's internal
143    // view - but that requires crate-internal access. Instead, we
144    // use the public root_path and do our own filesystem walk of
145    // directories only.
146    let mut out = Vec::new();
147    fn recurse(p: &std::path::Path, out: &mut Vec<PathBuf>) {
148        if !p.is_dir() {
149            return;
150        }
151        out.push(p.to_path_buf());
152        if let Ok(read) = fs::read_dir(p) {
153            for entry in read.flatten() {
154                let ep = entry.path();
155                if ep.is_dir() {
156                    recurse(&ep, out);
157                }
158            }
159        }
160    }
161    recurse(tree.root_path(), &mut out);
162    out
163}
Source

pub fn filter(&self) -> DirectoryFilter

Return the current filter.

Examples found in repository?
examples/with_icons.rs (line 69)
62    fn view(&self) -> Element<'_, Message> {
63        use iced::widget::{button, column, container, row, text};
64
65        let filter_row = row![
66            text("Filter:"),
67            button("Folders only")
68                .on_press(Message::SetFilter(DirectoryFilter::FoldersOnly))
69                .style(if self.tree.filter() == DirectoryFilter::FoldersOnly {
70                    button::primary
71                } else {
72                    button::secondary
73                }),
74            button("Files + folders")
75                .on_press(Message::SetFilter(DirectoryFilter::FilesAndFolders))
76                .style(if self.tree.filter() == DirectoryFilter::FilesAndFolders {
77                    button::primary
78                } else {
79                    button::secondary
80                }),
81            button("All (w/ hidden)")
82                .on_press(Message::SetFilter(DirectoryFilter::AllIncludingHidden))
83                .style(
84                    if self.tree.filter() == DirectoryFilter::AllIncludingHidden {
85                        button::primary
86                    } else {
87                        button::secondary
88                    },
89                ),
90        ]
91        .spacing(8.0);
92
93        let status = text(match &self.last_selected {
94            Some(p) => format!("Selected: {}", p.display()),
95            None => "No selection yet. Click any row to select; click folders to expand.".into(),
96        });
97
98        container(
99            column![filter_row, self.tree.view(Message::Tree), status]
100                .spacing(8.0)
101                .padding(8.0),
102        )
103        .width(Length::Fill)
104        .height(Length::Fill)
105        .into()
106    }
More examples
Hide additional examples
examples/basic.rs (line 70)
61    fn view(&self) -> Element<'_, Message> {
62        use iced::widget::{button, column, container, row, text};
63
64        // Three plain buttons for filter selection. Keeps the example
65        // dependency-free of `Display` impls or pick_list complexity.
66        let filter_row = row![
67            text("Filter:"),
68            button("Folders only")
69                .on_press(Message::SetFilter(DirectoryFilter::FoldersOnly))
70                .style(if self.tree.filter() == DirectoryFilter::FoldersOnly {
71                    button::primary
72                } else {
73                    button::secondary
74                }),
75            button("Files + folders")
76                .on_press(Message::SetFilter(DirectoryFilter::FilesAndFolders))
77                .style(if self.tree.filter() == DirectoryFilter::FilesAndFolders {
78                    button::primary
79                } else {
80                    button::secondary
81                }),
82            button("All (w/ hidden)")
83                .on_press(Message::SetFilter(DirectoryFilter::AllIncludingHidden))
84                .style(
85                    if self.tree.filter() == DirectoryFilter::AllIncludingHidden {
86                        button::primary
87                    } else {
88                        button::secondary
89                    },
90                ),
91        ]
92        .spacing(8.0);
93
94        let status = text(match &self.last_selected {
95            Some(p) => format!("Selected: {}", p.display()),
96            None => "No selection yet. Click any row to select; click folders to expand.".into(),
97        });
98
99        container(
100            column![filter_row, self.tree.view(Message::Tree), status]
101                .spacing(8.0)
102                .padding(8.0),
103        )
104        .width(Length::Fill)
105        .height(Length::Fill)
106        .into()
107    }
Source

pub fn max_depth(&self) -> Option<u32>

Return the current max depth, if any.

Source

pub fn selected_path(&self) -> Option<&Path>

Return a reference to the currently-active selected path, if any.

The active path is the path the user most recently acted on — the last row clicked, the last Space-toggled, the last target of a Shift-range, etc. For single-select applications this is exactly the one selected path and matches v0.2 semantics.

For multi-select, use DirectoryTree::selected_paths to see the whole set and DirectoryTree::anchor_path to read the pivot for range extension.

The returned path may point to a node that is currently invisible (because an ancestor is collapsed, or because the active filter hides it); the view layer handles that gracefully.

Source

pub fn selected_paths(&self) -> &[PathBuf]

All currently-selected paths.

Order is not semantically meaningful — treat the slice as a set. The slice is empty iff nothing is selected. Runs in O(1) (returns a reference to the internal Vec).

Examples found in repository?
examples/multi_select.rs (line 110)
109    fn view(&self) -> Element<'_, Message> {
110        let selected = self.tree.selected_paths();
111        let count = selected.len();
112
113        // Human-readable summary of currently-selected rows.
114        let summary_text = if count == 0 {
115            "No selection. Click to select, Shift+click for range, \
116             Ctrl/Cmd+click to toggle."
117                .to_string()
118        } else {
119            format!(
120                "{count} selected (anchor: {})",
121                self.tree
122                    .anchor_path()
123                    .map(short_name)
124                    .unwrap_or_else(|| "-".into())
125            )
126        };
127
128        // Compact list of selected basenames. Capped to avoid the
129        // status bar eating the screen when the user ranges over
130        // a huge folder.
131        const MAX_SHOWN: usize = 10;
132        let names: HashSet<String> = selected.iter().map(|p| short_name(p)).collect();
133        let mut names_sorted: Vec<String> = names.into_iter().collect();
134        names_sorted.sort();
135        let shown: String = if names_sorted.len() <= MAX_SHOWN {
136            names_sorted.join(", ")
137        } else {
138            format!(
139                "{}, +{} more",
140                names_sorted
141                    .iter()
142                    .take(MAX_SHOWN)
143                    .cloned()
144                    .collect::<Vec<_>>()
145                    .join(", "),
146                names_sorted.len() - MAX_SHOWN
147            )
148        };
149
150        let status = Column::new()
151            .push(text(summary_text).size(13))
152            .push(text(shown).size(11))
153            .spacing(2);
154
155        container(
156            column![
157                scrollable(self.tree.view(Message::Tree)).height(Length::Fill),
158                status,
159            ]
160            .spacing(8.0)
161            .padding(8.0),
162        )
163        .width(Length::Fill)
164        .height(Length::Fill)
165        .into()
166    }
Source

pub fn anchor_path(&self) -> Option<&Path>

The anchor used as the pivot for SelectionMode::ExtendRange.

The anchor is set by Replace and Toggle selections, and is not moved by ExtendRange — so two successive Shift+clicks from the same starting point select different ranges with the same origin.

Returns None before the first selection.

Examples found in repository?
examples/multi_select.rs (line 122)
109    fn view(&self) -> Element<'_, Message> {
110        let selected = self.tree.selected_paths();
111        let count = selected.len();
112
113        // Human-readable summary of currently-selected rows.
114        let summary_text = if count == 0 {
115            "No selection. Click to select, Shift+click for range, \
116             Ctrl/Cmd+click to toggle."
117                .to_string()
118        } else {
119            format!(
120                "{count} selected (anchor: {})",
121                self.tree
122                    .anchor_path()
123                    .map(short_name)
124                    .unwrap_or_else(|| "-".into())
125            )
126        };
127
128        // Compact list of selected basenames. Capped to avoid the
129        // status bar eating the screen when the user ranges over
130        // a huge folder.
131        const MAX_SHOWN: usize = 10;
132        let names: HashSet<String> = selected.iter().map(|p| short_name(p)).collect();
133        let mut names_sorted: Vec<String> = names.into_iter().collect();
134        names_sorted.sort();
135        let shown: String = if names_sorted.len() <= MAX_SHOWN {
136            names_sorted.join(", ")
137        } else {
138            format!(
139                "{}, +{} more",
140                names_sorted
141                    .iter()
142                    .take(MAX_SHOWN)
143                    .cloned()
144                    .collect::<Vec<_>>()
145                    .join(", "),
146                names_sorted.len() - MAX_SHOWN
147            )
148        };
149
150        let status = Column::new()
151            .push(text(summary_text).size(13))
152            .push(text(shown).size(11))
153            .spacing(2);
154
155        container(
156            column![
157                scrollable(self.tree.view(Message::Tree)).height(Length::Fill),
158                status,
159            ]
160            .spacing(8.0)
161            .padding(8.0),
162        )
163        .width(Length::Fill)
164        .height(Length::Fill)
165        .into()
166    }
Source

pub fn is_selected(&self, path: &Path) -> bool

true if path is in the selected set. O(n) in the set size.

Source

pub fn is_dragging(&self) -> bool

true when a drag gesture is in progress.

Apps can use this to dim unrelated UI or change cursors, but the widget’s own rendering already reflects drag state via the drop-target highlight.

Examples found in repository?
examples/drag_drop.rs (line 151)
148    fn view(&self) -> Element<'_, Message> {
149        // While a drag is in progress, override the static status
150        // line with a live preview of where the drop will land.
151        let live_status = if self.tree.is_dragging() {
152            match self.tree.drop_target() {
153                Some(dest) => format!(
154                    "Drop {} onto {}?",
155                    describe_sources(self.tree.drag_sources()),
156                    short_name(dest),
157                ),
158                None => format!(
159                    "Dragging {} — hover over a folder",
160                    describe_sources(self.tree.drag_sources()),
161                ),
162            }
163        } else {
164            self.status.clone()
165        };
166
167        let status = Column::new().push(text(live_status).size(13)).spacing(2);
168
169        container(
170            column![
171                scrollable(self.tree.view(Message::Tree)).height(Length::Fill),
172                status,
173            ]
174            .spacing(8.0)
175            .padding(8.0),
176        )
177        .width(Length::Fill)
178        .height(Length::Fill)
179        .into()
180    }
Source

pub fn drop_target(&self) -> Option<&Path>

Read-only view of the currently-hovered drop target, iff a drag is in progress and the cursor is over a valid folder.

Returns None when there is no drag, or when the cursor is over an invalid target (a file, one of the sources, a descendant of a source, or empty space).

Examples found in repository?
examples/drag_drop.rs (line 152)
148    fn view(&self) -> Element<'_, Message> {
149        // While a drag is in progress, override the static status
150        // line with a live preview of where the drop will land.
151        let live_status = if self.tree.is_dragging() {
152            match self.tree.drop_target() {
153                Some(dest) => format!(
154                    "Drop {} onto {}?",
155                    describe_sources(self.tree.drag_sources()),
156                    short_name(dest),
157                ),
158                None => format!(
159                    "Dragging {} — hover over a folder",
160                    describe_sources(self.tree.drag_sources()),
161                ),
162            }
163        } else {
164            self.status.clone()
165        };
166
167        let status = Column::new().push(text(live_status).size(13)).spacing(2);
168
169        container(
170            column![
171                scrollable(self.tree.view(Message::Tree)).height(Length::Fill),
172                status,
173            ]
174            .spacing(8.0)
175            .padding(8.0),
176        )
177        .width(Length::Fill)
178        .height(Length::Fill)
179        .into()
180    }
Source

pub fn drag_sources(&self) -> &[PathBuf]

Read-only view of the paths being dragged, iff a drag is in progress. Empty slice if there’s no drag.

Examples found in repository?
examples/drag_drop.rs (line 155)
148    fn view(&self) -> Element<'_, Message> {
149        // While a drag is in progress, override the static status
150        // line with a live preview of where the drop will land.
151        let live_status = if self.tree.is_dragging() {
152            match self.tree.drop_target() {
153                Some(dest) => format!(
154                    "Drop {} onto {}?",
155                    describe_sources(self.tree.drag_sources()),
156                    short_name(dest),
157                ),
158                None => format!(
159                    "Dragging {} — hover over a folder",
160                    describe_sources(self.tree.drag_sources()),
161                ),
162            }
163        } else {
164            self.status.clone()
165        };
166
167        let status = Column::new().push(text(live_status).size(13)).spacing(2);
168
169        container(
170            column![
171                scrollable(self.tree.view(Message::Tree)).height(Length::Fill),
172                status,
173            ]
174            .spacing(8.0)
175            .padding(8.0),
176        )
177        .width(Length::Fill)
178        .height(Length::Fill)
179        .into()
180    }
Source

pub fn set_search_query(&mut self, query: impl Into<String>)

v0.6: set or update the incremental search query.

Apps typically call this from their TextInput’s on_input callback. The widget narrows its visible rows to those whose basename matches the query as a case-insensitive substring — plus every ancestor of every match, so the user sees the tree context leading to their matches.

// In your update handler:
Message::SearchChanged(q) => {
    self.tree.set_search_query(q);
    Task::none()
}

An empty string clears the search — equivalent to clear_search. This is a deliberate simplification: having three states (none / empty-string / non-empty-string) tends to produce surprising UI where clearing the text box leaves the widget in a visually identical-but-semantically-distinct “searching for nothing” mode. With this contract there are only two states.

Search operates on already-loaded nodes only. Matches inside unloaded folders don’t appear until the folder loads (by user expansion or v0.5 prefetch). It does descend into loaded-but-collapsed folders, though — collapsed state doesn’t hide content from search.

Selection (including multi-selection) is orthogonal to search and is fully preserved: a selected row hidden by the query stays selected, and reappears when the query clears.

See the crate-internal search module for the full contract (visible in the source tree at src/directory_tree/search.rs).

Examples found in repository?
examples/search.rs (line 75)
70    fn update(&mut self, msg: Message) -> Task<Message> {
71        match msg {
72            Message::Tree(ev) => self.tree.update(ev).map(Message::Tree),
73            Message::SearchChanged(q) => {
74                self.query = q.clone();
75                self.tree.set_search_query(q);
76                Task::none()
77            }
78            Message::ExpandAll => {
79                // Toggle every loaded folder that isn't already
80                // expanded. The widget's on_loaded handler will
81                // cascade more scans via prefetch.
82                let mut tasks = Vec::new();
83                let to_expand = collect_collapsed_folders(&self.tree);
84                for p in to_expand {
85                    tasks.push(
86                        self.tree
87                            .update(DirectoryTreeEvent::Toggled(p))
88                            .map(Message::Tree),
89                    );
90                }
91                Task::batch(tasks)
92            }
93        }
94    }

Clear the active search query, if any. No-op if there is no active search.

After this call is_searching returns false, search_query returns None, and the widget returns to its normal view where rows are hidden only by is_expanded chain (plus the ordinary DirectoryFilter).

Source

pub fn search_query(&self) -> Option<&str>

The current search query as the application set it (preserving the app’s original case), or None when search is inactive.

Source

pub fn is_searching(&self) -> bool

true iff a search query is currently active.

Convenience wrapper around search_query; apps can use either depending on taste.

Examples found in repository?
examples/search.rs (line 101)
96    fn view(&self) -> Element<'_, Message> {
97        let search_bar = text_input("Search filenames...", &self.query)
98            .on_input(Message::SearchChanged)
99            .padding(6);
100
101        let status_text = if self.tree.is_searching() {
102            format!(
103                "{} match{} for \"{}\"",
104                self.tree.search_match_count(),
105                if self.tree.search_match_count() == 1 {
106                    ""
107                } else {
108                    "es"
109                },
110                self.query,
111            )
112        } else {
113            "Type above to filter. Press Expand-all to load deeper \
114             folders for broader coverage."
115                .into()
116        };
117
118        let controls = row![
119            search_bar,
120            button(text("Expand all")).on_press(Message::ExpandAll),
121        ]
122        .spacing(8);
123
124        column![
125            controls,
126            container(self.tree.view(Message::Tree))
127                .width(Length::Fill)
128                .height(Length::Fill),
129            text(status_text).size(13),
130        ]
131        .spacing(8)
132        .padding(10)
133        .into()
134    }
Source

pub fn search_match_count(&self) -> usize

Count of nodes that directly match the current search query.

Returns 0 when no search is active. This is distinct from “visible rows” — the visible set also includes ancestor breadcrumbs leading down to matches, which are typically not what the user wants counted in their UI’s “X results” display.

Examples found in repository?
examples/search.rs (line 104)
96    fn view(&self) -> Element<'_, Message> {
97        let search_bar = text_input("Search filenames...", &self.query)
98            .on_input(Message::SearchChanged)
99            .padding(6);
100
101        let status_text = if self.tree.is_searching() {
102            format!(
103                "{} match{} for \"{}\"",
104                self.tree.search_match_count(),
105                if self.tree.search_match_count() == 1 {
106                    ""
107                } else {
108                    "es"
109                },
110                self.query,
111            )
112        } else {
113            "Type above to filter. Press Expand-all to load deeper \
114             folders for broader coverage."
115                .into()
116        };
117
118        let controls = row![
119            search_bar,
120            button(text("Expand all")).on_press(Message::ExpandAll),
121        ]
122        .spacing(8);
123
124        column![
125            controls,
126            container(self.tree.view(Message::Tree))
127                .width(Length::Fill)
128                .height(Length::Fill),
129            text(status_text).size(13),
130        ]
131        .spacing(8)
132        .padding(10)
133        .into()
134    }

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<State, Message> IntoBoot<State, Message> for State

Source§

fn into_boot(self) -> (State, Task<Message>)

Turns some type into the initial state of some Application.
Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

fn upcast(&self) -> Option<&T>

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

impl<T> MaybeClone for T

Source§

impl<T> MaybeDebug for T

Source§

impl<T> MaybeSend for T
where T: Send,

Source§

impl<T> MaybeSync for T
where T: Sync,

Source§

impl<T> WasmNotSend for T
where T: Send,

Source§

impl<T> WasmNotSendSync for T

Source§

impl<T> WasmNotSync for T
where T: Sync,