1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//! **File** drag & drop management
//!
//! Manages hovered files (drag-and-drop).
use alloc::vec::Vec;
use azul_css::AzString;
/// Manager for file drop state and hovered file tracking.
///
/// MWA-B7: stores ALL files of a drag/drop (multi-file drops were silently
/// truncated to the first path at every OS ingress site — the manager could
/// only hold one). The single-file accessors remain as first-element views
/// for existing callers.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FileDropManager {
/// Files being hovered during the drag operation (empty = no hover).
hovered_files: Vec<AzString>,
/// Files that were dropped (cleared after one frame).
dropped_files: Vec<AzString>,
/// One-shot flag set when a hover ends without a drop (a non-empty →
/// empty transition). Read by `determine_all_events` to emit
/// `EventType::FileHoverCancel`, then cleared by the platform drag
/// handler (one-shot, mirrors the dropped-files reset).
hover_cancelled: bool,
}
impl FileDropManager {
/// Create a new file drop manager
#[must_use] pub const fn new() -> Self {
Self {
hovered_files: Vec::new(),
dropped_files: Vec::new(),
hover_cancelled: false,
}
}
/// Set ALL currently hovered files (MWA-B7). An empty vec behaves like a
/// drag-leave (latches the hover-cancel flag).
pub fn set_hovered_files(&mut self, files: Vec<AzString>) {
if files.is_empty() {
if !self.hovered_files.is_empty() {
self.hover_cancelled = true;
}
self.hovered_files.clear();
} else {
self.hovered_files = files;
}
}
/// Single-file compatibility shim over [`set_hovered_files`](Self::set_hovered_files).
///
/// Platform backends call this with `Some(path)` on drag-enter
/// (macOS `draggingEntered`, Windows OLE `IDropTarget::DragEnter`) and
/// `None` on drag-leave (`draggingExited` / `DragLeave`). A `Some` -> `None`
/// transition latches [`FileDropManager::hover_was_cancelled`] so the
/// `FileHoverCancel` event can fire.
pub fn set_hovered_file(&mut self, file: Option<AzString>) {
match file {
Some(f) => self.set_hovered_files(alloc::vec![f]),
None => self.set_hovered_files(Vec::new()),
}
}
/// Whether a hover ended without a drop since the last
/// [`FileDropManager::clear_hover_cancelled`] (one-shot).
#[must_use] pub const fn hover_was_cancelled(&self) -> bool {
self.hover_cancelled
}
/// Clear the one-shot hover-cancel flag. Called by the platform drag
/// handler after `determine_all_events` has emitted the `FileHoverCancel`
/// event (mirrors the dropped-files reset after `FileDrop`).
pub const fn clear_hover_cancelled(&mut self) {
self.hover_cancelled = false;
}
/// First hovered file (single-file view; use
/// [`get_hovered_files`](Self::get_hovered_files) for the full list).
#[must_use] pub fn get_hovered_file(&self) -> Option<&AzString> {
self.hovered_files.first()
}
/// ALL currently hovered files (MWA-B7).
#[must_use] pub fn get_hovered_files(&self) -> &[AzString] {
&self.hovered_files
}
/// First dropped file (single-file view; use
/// [`get_dropped_files`](Self::get_dropped_files) for the full list).
#[must_use] pub fn get_dropped_file(&self) -> Option<&AzString> {
self.dropped_files.first()
}
/// ALL files of the drop this frame (MWA-B7; one-shot, cleared by the
/// platform handler after event processing).
#[must_use] pub fn get_dropped_files(&self) -> &[AzString] {
&self.dropped_files
}
/// Set ALL dropped files (MWA-B7; cleared after one frame).
pub fn set_dropped_files(&mut self, files: Vec<AzString>) {
self.dropped_files = files;
}
/// Single-file compatibility shim over [`set_dropped_files`](Self::set_dropped_files).
pub fn set_dropped_file(&mut self, file: Option<AzString>) {
match file {
Some(f) => self.dropped_files = alloc::vec![f],
None => self.dropped_files.clear(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &str) -> AzString {
AzString::from(v.to_string())
}
#[test]
fn multi_file_drop_keeps_every_path() {
let mut m = FileDropManager::new();
m.set_dropped_files(vec![s("/a"), s("/b"), s("/c")]);
assert_eq!(m.get_dropped_files().len(), 3);
assert_eq!(m.get_dropped_file().map(AzString::as_str), Some("/a"));
m.set_dropped_file(None);
assert!(m.get_dropped_files().is_empty());
}
#[test]
fn hover_cancel_latches_on_empty_transition() {
let mut m = FileDropManager::new();
m.set_hovered_files(vec![s("/a"), s("/b")]);
assert_eq!(m.get_hovered_files().len(), 2);
assert!(!m.hover_was_cancelled());
m.set_hovered_files(Vec::new());
assert!(m.hover_was_cancelled());
m.clear_hover_cancelled();
assert!(!m.hover_was_cancelled());
}
}