1use alloc::vec::Vec;
6
7use azul_css::AzString;
8
9#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct FileDropManager {
17 hovered_files: Vec<AzString>,
19 dropped_files: Vec<AzString>,
21 hover_cancelled: bool,
26}
27
28impl FileDropManager {
29 #[must_use] pub const fn new() -> Self {
31 Self {
32 hovered_files: Vec::new(),
33 dropped_files: Vec::new(),
34 hover_cancelled: false,
35 }
36 }
37
38 pub fn set_hovered_files(&mut self, files: Vec<AzString>) {
41 if files.is_empty() {
42 if !self.hovered_files.is_empty() {
43 self.hover_cancelled = true;
44 }
45 self.hovered_files.clear();
46 } else {
47 self.hovered_files = files;
48 }
49 }
50
51 pub fn set_hovered_file(&mut self, file: Option<AzString>) {
59 match file {
60 Some(f) => self.set_hovered_files(alloc::vec![f]),
61 None => self.set_hovered_files(Vec::new()),
62 }
63 }
64
65 #[must_use] pub const fn hover_was_cancelled(&self) -> bool {
68 self.hover_cancelled
69 }
70
71 pub const fn clear_hover_cancelled(&mut self) {
75 self.hover_cancelled = false;
76 }
77
78 #[must_use] pub fn get_hovered_file(&self) -> Option<&AzString> {
81 self.hovered_files.first()
82 }
83
84 #[must_use] pub fn get_hovered_files(&self) -> &[AzString] {
86 &self.hovered_files
87 }
88
89 #[must_use] pub fn get_dropped_file(&self) -> Option<&AzString> {
92 self.dropped_files.first()
93 }
94
95 #[must_use] pub fn get_dropped_files(&self) -> &[AzString] {
98 &self.dropped_files
99 }
100
101 pub fn set_dropped_files(&mut self, files: Vec<AzString>) {
103 self.dropped_files = files;
104 }
105
106 pub fn set_dropped_file(&mut self, file: Option<AzString>) {
108 match file {
109 Some(f) => self.dropped_files = alloc::vec![f],
110 None => self.dropped_files.clear(),
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn s(v: &str) -> AzString {
120 AzString::from(v.to_string())
121 }
122
123 #[test]
124 fn multi_file_drop_keeps_every_path() {
125 let mut m = FileDropManager::new();
126 m.set_dropped_files(vec![s("/a"), s("/b"), s("/c")]);
127 assert_eq!(m.get_dropped_files().len(), 3);
128 assert_eq!(m.get_dropped_file().map(AzString::as_str), Some("/a"));
129 m.set_dropped_file(None);
130 assert!(m.get_dropped_files().is_empty());
131 }
132
133 #[test]
134 fn hover_cancel_latches_on_empty_transition() {
135 let mut m = FileDropManager::new();
136 m.set_hovered_files(vec![s("/a"), s("/b")]);
137 assert_eq!(m.get_hovered_files().len(), 2);
138 assert!(!m.hover_was_cancelled());
139 m.set_hovered_files(Vec::new());
140 assert!(m.hover_was_cancelled());
141 m.clear_hover_cancelled();
142 assert!(!m.hover_was_cancelled());
143 }
144}
145
146#[cfg(test)]
147mod autotest_generated {
148 use super::*;
149
150 fn s(v: &str) -> AzString {
151 AzString::from(v.to_string())
152 }
153
154 fn assert_invariants(m: &FileDropManager) {
157 assert_eq!(m.get_hovered_file(), m.get_hovered_files().first());
158 assert_eq!(m.get_dropped_file(), m.get_dropped_files().first());
159 assert_eq!(
160 m.get_hovered_file().is_none(),
161 m.get_hovered_files().is_empty()
162 );
163 assert_eq!(
164 m.get_dropped_file().is_none(),
165 m.get_dropped_files().is_empty()
166 );
167 }
168
169 #[test]
174 fn new_is_empty_and_matches_default() {
175 let m = FileDropManager::new();
176 assert!(m.get_hovered_files().is_empty());
177 assert!(m.get_dropped_files().is_empty());
178 assert_eq!(m.get_hovered_file(), None);
179 assert_eq!(m.get_dropped_file(), None);
180 assert!(!m.hover_was_cancelled());
181 assert_eq!(m, FileDropManager::default());
182 assert_invariants(&m);
183 }
184
185 #[test]
186 fn new_is_usable_in_const_context() {
187 const M: FileDropManager = FileDropManager::new();
188 assert!(!M.hover_was_cancelled());
189 assert!(M.get_hovered_files().is_empty());
190 assert!(M.get_dropped_files().is_empty());
191 }
192
193 #[test]
194 fn getters_on_default_instance_do_not_panic() {
195 let m = FileDropManager::default();
196 assert_invariants(&m);
197 assert!(!m.hover_was_cancelled());
199 assert!(!m.hover_was_cancelled());
200 }
201
202 #[test]
207 fn empty_hover_set_on_fresh_manager_does_not_latch_cancel() {
208 let mut m = FileDropManager::new();
210 m.set_hovered_files(Vec::new());
211 assert!(!m.hover_was_cancelled());
212 m.set_hovered_files(Vec::new());
213 assert!(!m.hover_was_cancelled());
214 m.set_hovered_file(None);
215 assert!(!m.hover_was_cancelled());
216 assert_invariants(&m);
217 }
218
219 #[test]
220 fn second_empty_hover_set_after_a_latch_keeps_the_flag() {
221 let mut m = FileDropManager::new();
222 m.set_hovered_files(vec![s("/a")]);
223 m.set_hovered_files(Vec::new()); assert!(m.hover_was_cancelled());
225 m.set_hovered_files(Vec::new());
228 assert!(m.hover_was_cancelled());
229 m.clear_hover_cancelled();
230 assert!(!m.hover_was_cancelled());
231 }
232
233 #[test]
234 fn nonempty_to_nonempty_hover_never_latches_cancel() {
235 let mut m = FileDropManager::new();
236 for i in 0..64 {
237 m.set_hovered_files(vec![s(&format!("/f{i}"))]);
238 assert!(!m.hover_was_cancelled());
239 }
240 assert_eq!(m.get_hovered_files().len(), 1, "sets replace, never append");
241 assert_eq!(m.get_hovered_file().map(AzString::as_str), Some("/f63"));
242 }
243
244 #[test]
245 fn re_entering_hover_does_not_clear_a_pending_cancel() {
246 let mut m = FileDropManager::new();
250 m.set_hovered_file(Some(s("/a")));
251 m.set_hovered_file(None);
252 assert!(m.hover_was_cancelled());
253 m.set_hovered_file(Some(s("/b")));
254 assert!(m.hover_was_cancelled());
255 assert_eq!(m.get_hovered_file().map(AzString::as_str), Some("/b"));
256 }
257
258 #[test]
259 fn clear_hover_cancelled_is_idempotent_and_leaves_files_alone() {
260 let mut m = FileDropManager::new();
261 m.set_hovered_files(vec![s("/a"), s("/b")]);
262 m.set_dropped_files(vec![s("/d")]);
263 m.clear_hover_cancelled();
264 m.clear_hover_cancelled();
265 assert!(!m.hover_was_cancelled());
266 assert_eq!(m.get_hovered_files().len(), 2);
267 assert_eq!(m.get_dropped_files().len(), 1);
268 assert_invariants(&m);
269 }
270
271 #[test]
272 fn dropped_file_setters_never_touch_the_hover_cancel_flag() {
273 let mut m = FileDropManager::new();
274 m.set_dropped_files(vec![s("/a")]);
275 m.set_dropped_files(Vec::new());
276 m.set_dropped_file(Some(s("/b")));
277 m.set_dropped_file(None);
278 assert!(!m.hover_was_cancelled());
279 m.set_hovered_files(vec![s("/h")]);
281 m.set_hovered_files(Vec::new());
282 assert!(m.hover_was_cancelled());
283 m.set_dropped_files(vec![s("/c")]);
284 m.set_dropped_file(None);
285 assert!(m.hover_was_cancelled());
286 }
287
288 #[test]
289 fn drop_does_not_implicitly_end_the_hover() {
290 let mut m = FileDropManager::new();
294 m.set_hovered_files(vec![s("/a")]);
295 m.set_dropped_files(vec![s("/a")]);
296 assert_eq!(m.get_hovered_files().len(), 1);
297 assert!(!m.hover_was_cancelled());
298 m.set_hovered_files(Vec::new());
299 assert!(m.hover_was_cancelled());
300 assert!(m.get_hovered_files().is_empty());
301 assert_eq!(m.get_dropped_files().len(), 1, "drop survives the exit");
302 }
303
304 #[test]
309 fn hovered_round_trip_preserves_order_and_duplicates() {
310 let files = vec![s("/z"), s("/a"), s("/z"), s(""), s("/a")];
311 let mut m = FileDropManager::new();
312 m.set_hovered_files(files.clone());
313 assert_eq!(m.get_hovered_files(), &files[..]);
314 assert_eq!(m.get_hovered_file(), Some(&files[0]));
315 assert_invariants(&m);
316 }
317
318 #[test]
319 fn dropped_round_trip_preserves_order_and_duplicates() {
320 let files = vec![s("/z"), s("/a"), s("/z"), s(""), s("/a")];
321 let mut m = FileDropManager::new();
322 m.set_dropped_files(files.clone());
323 assert_eq!(m.get_dropped_files(), &files[..]);
324 assert_eq!(m.get_dropped_file(), Some(&files[0]));
325 assert_invariants(&m);
326 }
327
328 #[test]
329 fn single_file_shims_agree_with_the_multi_file_setters() {
330 let mut shim = FileDropManager::new();
331 let mut multi = FileDropManager::new();
332
333 shim.set_hovered_file(Some(s("/p")));
334 multi.set_hovered_files(vec![s("/p")]);
335 shim.set_dropped_file(Some(s("/q")));
336 multi.set_dropped_files(vec![s("/q")]);
337 assert_eq!(shim, multi);
338
339 shim.set_hovered_file(None);
340 multi.set_hovered_files(Vec::new());
341 shim.set_dropped_file(None);
342 multi.set_dropped_files(Vec::new());
343 assert_eq!(shim, multi);
344 assert!(shim.hover_was_cancelled() && multi.hover_was_cancelled());
345 }
346
347 #[test]
348 fn hovered_and_dropped_state_are_independent() {
349 let mut m = FileDropManager::new();
350 m.set_hovered_files(vec![s("/h1"), s("/h2")]);
351 m.set_dropped_files(vec![s("/d1")]);
352 assert_eq!(m.get_hovered_files().len(), 2);
353 assert_eq!(m.get_dropped_files().len(), 1);
354
355 m.set_dropped_file(None); assert!(m.get_dropped_files().is_empty());
357 assert_eq!(m.get_hovered_files().len(), 2, "hover untouched by reset");
358 assert_invariants(&m);
359 }
360
361 #[test]
366 fn a_single_empty_path_is_a_hover_not_a_drag_leave() {
367 let mut m = FileDropManager::new();
370 m.set_hovered_files(vec![s("")]);
371 assert!(!m.hover_was_cancelled());
372 assert_eq!(m.get_hovered_files().len(), 1);
373 assert_eq!(m.get_hovered_file().map(AzString::as_str), Some(""));
374
375 let mut shim = FileDropManager::new();
376 shim.set_hovered_file(Some(s("")));
377 assert!(!shim.hover_was_cancelled());
378 assert_eq!(shim.get_hovered_file().map(AzString::as_str), Some(""));
379 assert_eq!(m, shim);
380 }
381
382 #[test]
383 fn unicode_paths_round_trip_byte_exact() {
384 let paths = [
385 "/tmp/файл.txt", "/tmp/文件/資料.csv", "/tmp/🦀/emoji 💾.bin", "/tmp/\u{202E}gpj.exe", "/tmp/e\u{0301}\u{0301}\u{0301}.txt", "/tmp/zero\u{200B}width.txt", "/tmp/\u{FFFD}replacement.txt", " /tmp/ leading and trailing ", "/tmp/newline\nand\ttab.txt", ];
395 let files: Vec<AzString> = paths.iter().map(|p| s(p)).collect();
396
397 let mut m = FileDropManager::new();
398 m.set_hovered_files(files.clone());
399 m.set_dropped_files(files.clone());
400 for (i, p) in paths.iter().enumerate() {
401 assert_eq!(m.get_hovered_files()[i].as_str(), *p);
402 assert_eq!(m.get_dropped_files()[i].as_str(), *p);
403 assert_eq!(m.get_hovered_files()[i].as_str().len(), p.len());
404 }
405 assert_invariants(&m);
406 }
407
408 #[test]
409 fn path_with_interior_nul_byte_is_preserved() {
410 let path = "/tmp/a\0b/c\0.txt";
413 let mut m = FileDropManager::new();
414 m.set_dropped_file(Some(s(path)));
415 let got = m.get_dropped_file().expect("dropped file present");
416 assert_eq!(got.as_str(), path);
417 assert_eq!(got.as_str().len(), path.len());
418 assert_eq!(got.as_str().matches('\0').count(), 2);
419 }
420
421 #[test]
422 fn huge_path_round_trips_without_truncation() {
423 let mut path = String::from("/tmp/");
425 for _ in 0..32_768 {
426 path.push_str("aä🦀/"); }
428 path.push_str("end.txt");
429 let byte_len = path.len();
430 assert!(byte_len > 256 * 1024);
431
432 let mut m = FileDropManager::new();
433 m.set_hovered_file(Some(AzString::from(path.clone())));
434 let got = m.get_hovered_file().expect("hovered file present");
435 assert_eq!(got.as_str().len(), byte_len);
436 assert!(got.as_str().ends_with("end.txt"));
437 assert_eq!(got.as_str(), path.as_str());
438 assert!(!m.hover_was_cancelled());
439 }
440
441 #[test]
442 fn ten_thousand_dropped_files_keep_length_and_order() {
443 let n = 10_000usize;
444 let files: Vec<AzString> = (0..n).map(|i| s(&format!("/f/{i}"))).collect();
445 let mut m = FileDropManager::new();
446 m.set_dropped_files(files);
447
448 assert_eq!(m.get_dropped_files().len(), n);
449 assert_eq!(m.get_dropped_file().map(AzString::as_str), Some("/f/0"));
450 assert_eq!(m.get_dropped_files()[n - 1].as_str(), "/f/9999");
451 assert_invariants(&m);
452
453 m.set_dropped_files(Vec::new());
454 assert!(m.get_dropped_files().is_empty());
455 assert_eq!(m.get_dropped_file(), None);
456 assert!(!m.hover_was_cancelled(), "drops never latch hover-cancel");
457 }
458
459 #[test]
460 fn many_hover_enter_leave_cycles_stay_consistent() {
461 let mut m = FileDropManager::new();
462 for i in 0..1_000 {
463 m.set_hovered_files(vec![s(&format!("/f{i}")), s("/other")]);
464 assert_eq!(m.get_hovered_files().len(), 2, "no accumulation");
465 assert!(!m.hover_was_cancelled(), "cleared at the end of each cycle");
466 m.set_hovered_files(Vec::new());
467 assert!(m.hover_was_cancelled());
468 assert!(m.get_hovered_files().is_empty());
469 assert_eq!(m.get_hovered_file(), None);
470 m.clear_hover_cancelled();
471 }
472 assert_eq!(m, FileDropManager::new());
473 }
474
475 #[test]
480 fn clone_equals_original_and_is_independent() {
481 let mut m = FileDropManager::new();
482 m.set_hovered_files(vec![s("/a"), s("/b")]);
483 m.set_dropped_files(vec![s("/c")]);
484 m.set_hovered_files(Vec::new()); let c = m.clone();
487 assert_eq!(c, m);
488
489 let mut c2 = c.clone();
491 c2.clear_hover_cancelled();
492 c2.set_dropped_files(Vec::new());
493 assert!(m.hover_was_cancelled());
494 assert_eq!(m.get_dropped_files().len(), 1);
495 assert_ne!(c2, m);
496 }
497
498 #[test]
499 fn equality_accounts_for_the_hover_cancel_flag() {
500 let mut latched = FileDropManager::new();
501 latched.set_hovered_files(vec![s("/a")]);
502 latched.set_hovered_files(Vec::new());
503
504 let fresh = FileDropManager::new();
505 assert_eq!(latched.get_hovered_files(), fresh.get_hovered_files());
507 assert_eq!(latched.get_dropped_files(), fresh.get_dropped_files());
508 assert_ne!(latched, fresh);
509
510 let mut cleared = latched.clone();
511 cleared.clear_hover_cancelled();
512 assert_eq!(cleared, fresh);
513 }
514
515 #[test]
516 fn debug_output_contains_the_paths() {
517 let mut m = FileDropManager::new();
518 m.set_hovered_files(vec![s("/tmp/🦀.txt")]);
519 let dbg = alloc::format!("{m:?}");
520 assert!(dbg.contains("/tmp/🦀.txt"), "unexpected Debug output: {dbg}");
521 assert!(dbg.contains("hover_cancelled"));
522 }
523}