1use std::collections::HashSet;
35use std::fs;
36use std::path::{Path, PathBuf};
37
38use iced::keyboard::{self, Modifiers};
39use iced::widget::{Column, column, container, scrollable, text};
40use iced::{Element, Length, Subscription, Task};
41use iced_swdir_tree::{DirectoryFilter, DirectoryTree, DirectoryTreeEvent, SelectionMode};
42
43#[derive(Debug, Clone)]
44enum Message {
45 Tree(DirectoryTreeEvent),
46 ModifiersChanged(Modifiers),
47 Key(keyboard::Key, Modifiers),
48}
49
50struct App {
51 tree: DirectoryTree,
52 modifiers: Modifiers,
53 status: String,
57}
58
59impl App {
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 }
76
77 fn update(&mut self, message: Message) -> Task<Message> {
78 match message {
79 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 Message::Tree(DirectoryTreeEvent::DragCompleted {
93 sources,
94 destination,
95 }) => {
96 let outcome = move_paths(&sources, &destination);
97 self.status = outcome.summary();
98 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 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 }
136
137 fn subscription(&self) -> Subscription<Message> {
138 keyboard::listen().map(|event| match event {
139 keyboard::Event::KeyPressed { key, modifiers, .. } => Message::Key(key, modifiers),
140 keyboard::Event::ModifiersChanged(modifiers) => Message::ModifiersChanged(modifiers),
141 _ => Message::Key(
142 keyboard::Key::Named(keyboard::key::Named::F35),
143 Modifiers::default(),
144 ),
145 })
146 }
147
148 fn view(&self) -> Element<'_, Message> {
149 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 }
181}
182
183fn resolve_root() -> PathBuf {
191 if let Some(arg) = std::env::args().nth(1) {
192 return PathBuf::from(arg);
193 }
194 let scratch = std::env::temp_dir().join("iced-swdir-tree-drag-demo");
195 let _ = fs::create_dir_all(&scratch);
196 for folder in &["inbox", "archive", "drafts"] {
199 let _ = fs::create_dir_all(scratch.join(folder));
200 }
201 for (name, body) in &[
202 ("notes.txt", "drop me somewhere"),
203 ("todo.md", "- try dragging this into `inbox`\n"),
204 ("ideas.txt", "multi-select me with Ctrl or Shift"),
205 ] {
206 let p = scratch.join(name);
207 if !p.exists() {
208 let _ = fs::write(p, body);
209 }
210 }
211 scratch
212}
213
214struct MoveOutcome {
217 moved: usize,
218 failed: Vec<(PathBuf, std::io::Error)>,
219 dest: PathBuf,
220}
221
222impl MoveOutcome {
223 fn summary(&self) -> String {
224 match (self.moved, self.failed.len()) {
225 (n, 0) => format!(
226 "Moved {} item{} into {}",
227 n,
228 plural(n),
229 short_name(&self.dest)
230 ),
231 (0, f) => format!(
232 "Failed to move {} item{} into {}: {}",
233 f,
234 plural(f),
235 short_name(&self.dest),
236 self.failed[0].1,
237 ),
238 (n, f) => format!(
239 "Moved {} into {}, {} failed (e.g. {}: {})",
240 n,
241 short_name(&self.dest),
242 f,
243 short_name(&self.failed[0].0),
244 self.failed[0].1,
245 ),
246 }
247 }
248}
249
250fn move_paths(sources: &[PathBuf], dest: &Path) -> MoveOutcome {
257 let mut moved = 0;
258 let mut failed = Vec::new();
259 for src in sources {
260 let Some(name) = src.file_name() else {
261 continue;
262 };
263 let target = dest.join(name);
264 if target.exists() {
266 failed.push((
267 src.clone(),
268 std::io::Error::new(
269 std::io::ErrorKind::AlreadyExists,
270 "destination already has an entry with that name",
271 ),
272 ));
273 continue;
274 }
275 match fs::rename(src, &target) {
276 Ok(()) => moved += 1,
277 Err(e) => failed.push((src.clone(), e)),
278 }
279 }
280 MoveOutcome {
281 moved,
282 failed,
283 dest: dest.to_path_buf(),
284 }
285}
286
287fn describe_sources(sources: &[PathBuf]) -> String {
290 match sources {
291 [] => "nothing".into(),
292 [p] => short_name(p),
293 _ => format!("{} items", sources.len()),
294 }
295}
296
297fn short_name(p: &Path) -> String {
298 p.file_name()
299 .map(|s| s.to_string_lossy().into_owned())
300 .unwrap_or_else(|| p.display().to_string())
301}
302
303fn plural(n: usize) -> &'static str {
304 if n == 1 { "" } else { "s" }
305}
306
307fn main() -> iced::Result {
308 iced::application(App::new, App::update, App::view)
309 .subscription(App::subscription)
310 .title("iced-swdir-tree · drag-and-drop example")
311 .run()
312}