azul_layout/managers/file_drop.rs
1//! **File** drag & drop management
2//!
3//! Manages hovered files (drag-and-drop).
4
5use alloc::vec::Vec;
6
7use azul_css::AzString;
8
9/// Manager for file drop state and hovered file tracking.
10///
11/// MWA-B7: stores ALL files of a drag/drop (multi-file drops were silently
12/// truncated to the first path at every OS ingress site — the manager could
13/// only hold one). The single-file accessors remain as first-element views
14/// for existing callers.
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct FileDropManager {
17 /// Files being hovered during the drag operation (empty = no hover).
18 hovered_files: Vec<AzString>,
19 /// Files that were dropped (cleared after one frame).
20 dropped_files: Vec<AzString>,
21 /// One-shot flag set when a hover ends without a drop (a non-empty →
22 /// empty transition). Read by `determine_all_events` to emit
23 /// `EventType::FileHoverCancel`, then cleared by the platform drag
24 /// handler (one-shot, mirrors the dropped-files reset).
25 hover_cancelled: bool,
26}
27
28impl FileDropManager {
29 /// Create a new file drop manager
30 #[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 /// Set ALL currently hovered files (MWA-B7). An empty vec behaves like a
39 /// drag-leave (latches the hover-cancel flag).
40 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 /// Single-file compatibility shim over [`set_hovered_files`](Self::set_hovered_files).
52 ///
53 /// Platform backends call this with `Some(path)` on drag-enter
54 /// (macOS `draggingEntered`, Windows OLE `IDropTarget::DragEnter`) and
55 /// `None` on drag-leave (`draggingExited` / `DragLeave`). A `Some` -> `None`
56 /// transition latches [`FileDropManager::hover_was_cancelled`] so the
57 /// `FileHoverCancel` event can fire.
58 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 /// Whether a hover ended without a drop since the last
66 /// [`FileDropManager::clear_hover_cancelled`] (one-shot).
67 #[must_use] pub const fn hover_was_cancelled(&self) -> bool {
68 self.hover_cancelled
69 }
70
71 /// Clear the one-shot hover-cancel flag. Called by the platform drag
72 /// handler after `determine_all_events` has emitted the `FileHoverCancel`
73 /// event (mirrors the dropped-files reset after `FileDrop`).
74 pub const fn clear_hover_cancelled(&mut self) {
75 self.hover_cancelled = false;
76 }
77
78 /// First hovered file (single-file view; use
79 /// [`get_hovered_files`](Self::get_hovered_files) for the full list).
80 #[must_use] pub fn get_hovered_file(&self) -> Option<&AzString> {
81 self.hovered_files.first()
82 }
83
84 /// ALL currently hovered files (MWA-B7).
85 #[must_use] pub fn get_hovered_files(&self) -> &[AzString] {
86 &self.hovered_files
87 }
88
89 /// First dropped file (single-file view; use
90 /// [`get_dropped_files`](Self::get_dropped_files) for the full list).
91 #[must_use] pub fn get_dropped_file(&self) -> Option<&AzString> {
92 self.dropped_files.first()
93 }
94
95 /// ALL files of the drop this frame (MWA-B7; one-shot, cleared by the
96 /// platform handler after event processing).
97 #[must_use] pub fn get_dropped_files(&self) -> &[AzString] {
98 &self.dropped_files
99 }
100
101 /// Set ALL dropped files (MWA-B7; cleared after one frame).
102 pub fn set_dropped_files(&mut self, files: Vec<AzString>) {
103 self.dropped_files = files;
104 }
105
106 /// Single-file compatibility shim over [`set_dropped_files`](Self::set_dropped_files).
107 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}