1use crate::drag_drop::{DragDropEffects, DropProposal};
2use crate::file::{register_browser_file, BrowserFile};
3use crate::node::NodeRef;
4use std::cell::RefCell;
5#[cfg(feature = "native-runtime")]
6use std::path::Path;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub(crate) enum ExternalDragEventType {
10 Enter = 1,
11 Over = 2,
12 Leave = 3,
13 Drop = 4,
14 Unknown,
15}
16
17impl ExternalDragEventType {
18 pub(crate) fn from_raw(value: u32) -> Self {
19 match value {
20 1 => Self::Enter,
21 2 => Self::Over,
22 3 => Self::Leave,
23 4 => Self::Drop,
24 _ => Self::Unknown,
25 }
26 }
27}
28
29#[repr(u32)]
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum ExternalDropItemKind {
32 File = 1,
33 Text = 2,
34 Uri = 3,
35 Unknown(u32),
36}
37
38impl ExternalDropItemKind {
39 pub(crate) fn from_raw(value: u32) -> Self {
40 match value {
41 1 => Self::File,
42 2 => Self::Text,
43 3 => Self::Uri,
44 _ => Self::Unknown(value),
45 }
46 }
47}
48
49#[derive(Clone, Debug)]
50pub struct ExternalDropItemInfo {
51 pub id: String,
52 pub kind: ExternalDropItemKind,
53 pub name: String,
54 pub mime_type: Option<String>,
55 pub size_bytes: f64,
56 pub file: Option<BrowserFile>,
57}
58
59impl ExternalDropItemInfo {
60 pub fn new(
61 id: impl Into<String>,
62 kind: ExternalDropItemKind,
63 name: impl Into<String>,
64 mime_type: Option<String>,
65 size_bytes: f64,
66 file: Option<BrowserFile>,
67 ) -> Self {
68 Self {
69 id: id.into(),
70 kind,
71 name: name.into(),
72 mime_type,
73 size_bytes,
74 file,
75 }
76 }
77
78 #[cfg(feature = "native-runtime")]
79 pub fn native_path(&self) -> Option<&Path> {
80 (self.kind == ExternalDropItemKind::File).then(|| Path::new(&self.id))
81 }
82}
83
84#[derive(Clone, Debug)]
85pub struct ExternalDropEventArgs {
86 pub x: f32,
87 pub y: f32,
88 pub modifiers: u32,
89 pub items: Vec<ExternalDropItemInfo>,
90}
91
92impl ExternalDropEventArgs {
93 pub fn new(x: f32, y: f32, modifiers: u32, items: Vec<ExternalDropItemInfo>) -> Self {
94 Self {
95 x,
96 y,
97 modifiers,
98 items,
99 }
100 }
101}
102
103#[derive(Default)]
104struct ExternalDropState {
105 active_target: Option<NodeRef>,
106 active_effect: DragDropEffects,
107}
108
109thread_local! {
110 static STATE: RefCell<ExternalDropState> = RefCell::new(ExternalDropState::default());
111}
112
113fn is_default_proposal(proposal: DropProposal) -> bool {
114 proposal.effect == DragDropEffects::None && !proposal.show_insertion_marker
115}
116
117fn normalize_effect(candidate: DragDropEffects) -> DragDropEffects {
118 let masked = (candidate as u32)
119 & ((DragDropEffects::Copy as u32)
120 | (DragDropEffects::Move as u32)
121 | (DragDropEffects::Link as u32));
122 if masked == DragDropEffects::None as u32 {
123 return DragDropEffects::None;
124 }
125 if (masked & DragDropEffects::Move as u32) != 0 {
126 return DragDropEffects::Move;
127 }
128 if (masked & DragDropEffects::Copy as u32) != 0 {
129 return DragDropEffects::Copy;
130 }
131 if (masked & DragDropEffects::Link as u32) != 0 {
132 return DragDropEffects::Link;
133 }
134 DragDropEffects::None
135}
136
137fn resolve_drop_target(pointed_node: Option<NodeRef>) -> Option<NodeRef> {
138 let mut current = pointed_node;
139 while let Some(node) = current {
140 if node.allows_external_drop() {
141 return Some(node);
142 }
143 current = node.parent();
144 }
145 None
146}
147
148fn finish(
149 state: &mut ExternalDropState,
150 x: f32,
151 y: f32,
152 modifiers: u32,
153 items: &[ExternalDropItemInfo],
154 notify_target_leave: bool,
155) {
156 let target = state.active_target.take();
157 state.active_effect = DragDropEffects::None;
158 if notify_target_leave {
159 if let Some(target) = target {
160 target.handle_external_drag_leave(ExternalDropEventArgs::new(
161 x,
162 y,
163 modifiers,
164 items.to_vec(),
165 ));
166 }
167 }
168}
169
170pub(crate) fn handle_event(
171 pointed_node: Option<NodeRef>,
172 event_type: ExternalDragEventType,
173 x: f32,
174 y: f32,
175 modifiers: u32,
176 items: Vec<ExternalDropItemInfo>,
177) -> DragDropEffects {
178 STATE.with(|slot| {
179 let mut state = slot.borrow_mut();
180 if event_type == ExternalDragEventType::Leave {
181 finish(&mut state, x, y, modifiers, &items, true);
182 return DragDropEffects::None;
183 }
184
185 let target = resolve_drop_target(pointed_node);
186 let args = ExternalDropEventArgs::new(x, y, modifiers, items.clone());
187 let mut proposal = DropProposal::none();
188 let target_changed = match (&target, &state.active_target) {
189 (Some(target), Some(active)) => target.handle() != active.handle(),
190 (None, None) => false,
191 _ => true,
192 };
193 if target_changed {
194 if let Some(previous_target) = state.active_target.take() {
195 previous_target.handle_external_drag_leave(args.clone());
196 }
197 state.active_target = target.clone();
198 state.active_effect = DragDropEffects::None;
199 if let Some(target) = target.as_ref() {
200 if target.has_external_drag_enter_handler() {
201 proposal = target.handle_external_drag_enter(args.clone());
202 }
203 }
204 }
205
206 let Some(target) = target else {
207 state.active_effect = DragDropEffects::None;
208 return DragDropEffects::None;
209 };
210
211 if target.has_external_drag_over_handler() {
212 proposal = target.handle_external_drag_over(args.clone());
213 } else if is_default_proposal(proposal) {
214 proposal = DropProposal::new(state.active_effect, false);
215 }
216
217 let effect = normalize_effect(proposal.effect);
218 state.active_effect = effect;
219 if event_type == ExternalDragEventType::Drop {
220 if effect != DragDropEffects::None {
221 target.handle_external_drop_event(args);
222 }
223 finish(&mut state, x, y, modifiers, &items, true);
224 }
225 effect
226 })
227}
228
229pub(crate) fn handle_node_destroyed(node: NodeRef) {
230 STATE.with(|slot| {
231 let mut state = slot.borrow_mut();
232 let Some(active_target) = state.active_target.as_ref() else {
233 return;
234 };
235 if active_target.handle() == node.handle() {
236 state.active_target = None;
237 state.active_effect = DragDropEffects::None;
238 }
239 });
240}
241
242pub(crate) fn reset() {
243 STATE.with(|slot| {
244 let mut state = slot.borrow_mut();
245 state.active_target = None;
246 state.active_effect = DragDropEffects::None;
247 });
248}
249
250pub(crate) fn decode_payload(
251 payload_ptr: *const u8,
252 payload_len: u32,
253) -> Vec<ExternalDropItemInfo> {
254 let mut items = Vec::new();
255 if payload_ptr.is_null() || payload_len == 0 {
256 return items;
257 }
258 let bytes = unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) };
259 if bytes.len() < 4 {
260 crate::logger::warn("ExternalDrop", "Malformed external drop payload header.");
261 return items;
262 }
263 let mut cursor = 0usize;
264 let item_count = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().unwrap_or([0; 4]));
265 cursor += 4;
266 for index in 0..item_count {
267 if cursor + 12 > bytes.len() {
268 crate::logger::warn(
269 "ExternalDrop",
270 &format!("Truncated external drop item header at index {}.", index),
271 );
272 return items;
273 }
274 let kind = ExternalDropItemKind::from_raw(u32::from_le_bytes(
275 bytes[cursor..cursor + 4].try_into().unwrap_or([0; 4]),
276 ));
277 cursor += 4;
278 let size_bytes = f64::from_le_bytes(bytes[cursor..cursor + 8].try_into().unwrap_or([0; 8]));
279 cursor += 8;
280
281 let Some(id) = decode_string(bytes, &mut cursor, "id", index) else {
282 return items;
283 };
284 let Some(name) = decode_string(bytes, &mut cursor, "name", index) else {
285 return items;
286 };
287 let Some(mime_type) = decode_string(bytes, &mut cursor, "mime", index) else {
288 return items;
289 };
290 let mime_type = if mime_type.is_empty() {
291 None
292 } else {
293 Some(mime_type)
294 };
295 let file = if kind == ExternalDropItemKind::File && !id.is_empty() {
296 Some(register_browser_file(
297 id.clone(),
298 name.clone(),
299 mime_type.clone(),
300 size_bytes as u64,
301 0,
302 ))
303 } else {
304 None
305 };
306 items.push(ExternalDropItemInfo::new(
307 id, kind, name, mime_type, size_bytes, file,
308 ));
309 }
310 items
311}
312
313fn decode_string(bytes: &[u8], cursor: &mut usize, label: &str, index: u32) -> Option<String> {
314 if *cursor + 4 > bytes.len() {
315 crate::logger::warn(
316 "ExternalDrop",
317 &format!(
318 "Truncated external drop item {} length at index {}.",
319 label, index
320 ),
321 );
322 return None;
323 }
324 let len = u32::from_le_bytes(bytes[*cursor..*cursor + 4].try_into().unwrap_or([0; 4])) as usize;
325 *cursor += 4;
326 if *cursor + len > bytes.len() {
327 crate::logger::warn(
328 "ExternalDrop",
329 &format!("Truncated external drop item {} at index {}.", label, index),
330 );
331 return None;
332 }
333 let value = String::from_utf8_lossy(&bytes[*cursor..*cursor + len]).into_owned();
334 *cursor += len;
335 Some(value)
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::drag_drop::DropProposal;
342 use crate::event::reset as reset_events;
343 use crate::node::{flex_box, Node};
344 use std::cell::{Cell, RefCell};
345 use std::rc::Rc;
346
347 #[test]
348 fn routes_external_drop_enter_over_leave_and_drop() {
349 reset();
350 reset_events();
351
352 let root = flex_box();
353 let target = flex_box();
354 root.child(&target);
355
356 let enter_count = Rc::new(Cell::new(0));
357 let over_count = Rc::new(Cell::new(0));
358 let leave_count = Rc::new(Cell::new(0));
359 let drop_count = Rc::new(Cell::new(0));
360 let last_name = Rc::new(RefCell::new(String::new()));
361
362 target
363 .allow_external_drop(true)
364 .on_external_drag_enter({
365 let enter_count = enter_count.clone();
366 let last_name = last_name.clone();
367 move |args| {
368 enter_count.set(enter_count.get() + 1);
369 if let Some(item) = args.items.first() {
370 last_name.replace(item.name.clone());
371 }
372 DropProposal::new(DragDropEffects::Copy, false)
373 }
374 })
375 .on_external_drag_over({
376 let over_count = over_count.clone();
377 move |_args| {
378 over_count.set(over_count.get() + 1);
379 DropProposal::new(DragDropEffects::Copy, false)
380 }
381 })
382 .on_external_drag_leave({
383 let leave_count = leave_count.clone();
384 move |_args| {
385 leave_count.set(leave_count.get() + 1);
386 }
387 })
388 .on_external_drop({
389 let drop_count = drop_count.clone();
390 move |_args| {
391 drop_count.set(drop_count.get() + 1);
392 }
393 });
394
395 let items = vec![ExternalDropItemInfo::new(
396 "external-drop-1",
397 ExternalDropItemKind::File,
398 "todo.txt",
399 Some("text/plain".to_string()),
400 10.0,
401 None,
402 )];
403
404 assert_eq!(
405 handle_event(
406 Some(target.node_ref()),
407 ExternalDragEventType::Enter,
408 12.0,
409 18.0,
410 0,
411 items.clone(),
412 ),
413 DragDropEffects::Copy
414 );
415 assert_eq!(
416 handle_event(
417 Some(target.node_ref()),
418 ExternalDragEventType::Over,
419 14.0,
420 19.0,
421 0,
422 items.clone(),
423 ),
424 DragDropEffects::Copy
425 );
426 assert_eq!(
427 handle_event(
428 Some(target.node_ref()),
429 ExternalDragEventType::Drop,
430 16.0,
431 20.0,
432 0,
433 items,
434 ),
435 DragDropEffects::Copy
436 );
437
438 assert_eq!(enter_count.get(), 1);
439 assert_eq!(over_count.get(), 3);
440 assert_eq!(leave_count.get(), 1);
441 assert_eq!(drop_count.get(), 1);
442 assert_eq!(last_name.borrow().as_str(), "todo.txt");
443 }
444
445 #[test]
446 fn decodes_unknown_external_item_kind_without_registering_file() {
447 reset();
448 reset_events();
449
450 let mut payload = Vec::new();
451 payload.extend_from_slice(&1u32.to_le_bytes());
452 payload.extend_from_slice(&99u32.to_le_bytes());
453 payload.extend_from_slice(&123.0f64.to_le_bytes());
454 for value in ["external-drop-unknown", "note.txt", "text/plain"] {
455 payload.extend_from_slice(&(value.len() as u32).to_le_bytes());
456 payload.extend_from_slice(value.as_bytes());
457 }
458
459 let items = decode_payload(payload.as_ptr(), payload.len() as u32);
460 assert_eq!(items.len(), 1);
461 assert_eq!(items[0].kind, ExternalDropItemKind::Unknown(99));
462 assert_eq!(items[0].id, "external-drop-unknown");
463 assert_eq!(items[0].name, "note.txt");
464 assert_eq!(items[0].mime_type.as_deref(), Some("text/plain"));
465 assert!(items[0].file.is_none());
466 }
467}