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
DirectoryTree::new— build with a root path.- Optionally chain
DirectoryTree::with_filterand/orDirectoryTree::with_max_depthto configure. - Call
DirectoryTree::viewfrom yourviewfunction. - Route emitted
DirectoryTreeEvents through your app’s message system and pass them toDirectoryTree::update, which returns aniced::Taskthe parent should.map(..)back into its own message type.
Implementations§
Source§impl DirectoryTree
impl DirectoryTree
Sourcepub fn handle_key(
&self,
key: &Key,
modifiers: Modifiers,
) -> Option<DirectoryTreeEvent>
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?
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
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 }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
impl DirectoryTree
Sourcepub fn update(&mut self, msg: DirectoryTreeEvent) -> Task<DirectoryTreeEvent>
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?
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
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 }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 }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 }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 }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
impl DirectoryTree
Sourcepub fn view<'a, Message, F>(&'a self, on_event: F) -> Element<'a, Message>
pub fn view<'a, Message, F>(&'a self, on_event: F) -> Element<'a, Message>
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?
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
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 }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 }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 }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 }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
impl DirectoryTree
Sourcepub fn new(root: PathBuf) -> Self
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?
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
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 }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 }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 }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 }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 }Sourcepub fn with_filter(self, filter: DirectoryFilter) -> Self
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?
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
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 }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 }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 }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 }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 }Sourcepub fn with_max_depth(self, depth: u32) -> Self
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.
Sourcepub fn with_prefetch_limit(self, limit: usize) -> Self
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: 5–25,
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 scansPrefetch 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?
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 }Sourcepub fn with_prefetch_skip<I, S>(self, names: I) -> Self
pub fn with_prefetch_skip<I, S>(self, names: I) -> Self
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.
Sourcepub fn with_executor(self, executor: Arc<dyn ScanExecutor>) -> Self
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.
Sourcepub fn with_icon_theme(self, theme: Arc<dyn IconTheme>) -> Self
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 theiconsfeature is disabled.LucideTheme— available with theiconsfeature, renders real lucide vector glyphs. Default wheniconsis 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?
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 }Sourcepub fn set_filter(&mut self, filter: DirectoryFilter)
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?
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
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 }Sourcepub fn root_path(&self) -> &Path
pub fn root_path(&self) -> &Path
Return the root path.
Examples found in repository?
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}Sourcepub fn filter(&self) -> DirectoryFilter
pub fn filter(&self) -> DirectoryFilter
Return the current filter.
Examples found in repository?
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
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 }Sourcepub fn selected_path(&self) -> Option<&Path>
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.
Sourcepub fn selected_paths(&self) -> &[PathBuf]
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?
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 }Sourcepub fn anchor_path(&self) -> Option<&Path>
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?
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 }Sourcepub fn is_selected(&self, path: &Path) -> bool
pub fn is_selected(&self, path: &Path) -> bool
true if path is in the selected set. O(n) in the set size.
Sourcepub fn is_dragging(&self) -> bool
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?
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 }Sourcepub fn drop_target(&self) -> Option<&Path>
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?
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 }Sourcepub fn drag_sources(&self) -> &[PathBuf]
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?
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 }Sourcepub fn set_search_query(&mut self, query: impl Into<String>)
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?
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 }Sourcepub fn clear_search(&mut self)
pub fn clear_search(&mut self)
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).
Sourcepub fn search_query(&self) -> Option<&str>
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.
Sourcepub fn is_searching(&self) -> bool
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?
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 }Sourcepub fn search_match_count(&self) -> usize
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?
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§
impl Freeze for DirectoryTree
impl !RefUnwindSafe for DirectoryTree
impl Send for DirectoryTree
impl Sync for DirectoryTree
impl Unpin for DirectoryTree
impl UnsafeUnpin for DirectoryTree
impl !UnwindSafe for DirectoryTree
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&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
impl<T> DowncastSync for T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<State, Message> IntoBoot<State, Message> for State
impl<State, Message> IntoBoot<State, Message> for State
Source§fn into_boot(self) -> (State, Task<Message>)
fn into_boot(self) -> (State, Task<Message>)
Application.Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
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