Skip to main content

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}