Skip to main content

search/
search.rs

1//! Incremental search demo — type-ahead filter over the tree.
2//!
3//! Run with:
4//!
5//! ```sh
6//! cargo run --example search -- /path/to/scratch
7//! ```
8//!
9//! Without an argument, defaults to a scratch directory under the OS
10//! temp dir, populated with nested folders and files so there's
11//! something to search through.
12//!
13//! # What this demonstrates
14//!
15//! * A plain [`iced::widget::text_input`] above the tree, wired into
16//!   the widget via [`DirectoryTree::set_search_query`].
17//! * Live-update of a "N matches" counter below, via
18//!   [`DirectoryTree::search_match_count`].
19//! * A small expand-all button that loads every subdirectory so
20//!   search coverage is broader. In a real app you'd typically pair
21//!   search with [`DirectoryTree::with_prefetch_limit`] (v0.5) for
22//!   the same effect without the explicit button.
23//!
24//! # Known limitation (v0.6)
25//!
26//! Clicking to expand a folder while a search is active does NOT
27//! escape the filter — the widget stays narrowed to matches-and-
28//! ancestors. To explore outside the current search, clear the
29//! query first.
30
31use std::fs;
32use std::path::PathBuf;
33
34use iced::widget::{button, column, container, row, text, text_input};
35use iced::{Element, Length, Task};
36use iced_swdir_tree::{DirectoryFilter, DirectoryTree, DirectoryTreeEvent};
37
38#[derive(Debug, Clone)]
39enum Message {
40    Tree(DirectoryTreeEvent),
41    SearchChanged(String),
42    ExpandAll,
43}
44
45struct App {
46    tree: DirectoryTree,
47    query: String,
48}
49
50impl App {
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    }
95
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    }
135}
136
137/// Walk the tree and return every loaded folder that is currently
138/// collapsed. The app issues `Toggled` events for each to "expand
139/// all" in one button press (depth-first, best effort).
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}
164
165fn resolve_root() -> PathBuf {
166    if let Some(arg) = std::env::args().nth(1) {
167        return PathBuf::from(arg);
168    }
169    let scratch = std::env::temp_dir().join("iced-swdir-tree-search-demo");
170    let _ = fs::create_dir_all(&scratch);
171    // Create a miniature project layout to search through.
172    for dir in &[
173        "project",
174        "project/src",
175        "project/src/lib",
176        "project/tests",
177        "notes",
178        "notes/ideas",
179    ] {
180        let _ = fs::create_dir_all(scratch.join(dir));
181    }
182    for (path, body) in &[
183        ("project/README.md", "# Project\n"),
184        ("project/src/main.rs", "fn main() {}\n"),
185        ("project/src/lib/config.rs", ""),
186        ("project/src/lib/parser.rs", ""),
187        ("project/tests/integration.rs", ""),
188        ("project/tests/readme.md", "test notes\n"),
189        ("notes/todo.md", "- buy milk\n"),
190        ("notes/ideas/app_idea.md", ""),
191        ("notes/ideas/README.md", ""),
192        ("scratch_note.txt", ""),
193    ] {
194        let p = scratch.join(path);
195        if !p.exists() {
196            let _ = fs::write(p, body);
197        }
198    }
199    scratch
200}
201
202fn main() -> iced::Result {
203    iced::application(App::new, App::update, App::view)
204        .title("iced-swdir-tree · search example")
205        .run()
206}