Skip to main content

fui/
external_drop.rs

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}