Skip to main content

dioxus_dnd/
external.rs

1//! Drops arriving from *outside* your app — selected text, links dragged from
2//! another tab, content from other applications — plus typed serde payloads
3//! over Dioxus 0.7's `DataTransfer` bridge for interop scenarios where the
4//! Rust-side context can't reach.
5//!
6//! For drags between elements of your own app, prefer the core context: it
7//! carries any `Clone` type with zero serialization. Reach for this module
8//! when the *browser* is the transport.
9
10use dioxus::html::HasFileData;
11use dioxus::prelude::*;
12
13use crate::core::{client_point, element_point, Point};
14
15/// Content the browser handed us from an external drag, best-effort decoded
16/// in order of specificity.
17#[derive(Debug, Clone, PartialEq)]
18pub enum ExternalPayload {
19    /// `text/uri-list` — links dragged from the URL bar, bookmarks, other tabs.
20    Url(String),
21    /// `text/html` — rich content (e.g. a selection dragged from a page).
22    Html(String),
23    /// `text/plain`.
24    Text(String),
25}
26
27/// A decoded external drop.
28#[derive(Clone, PartialEq)]
29pub struct ExternalDrop {
30    /// All representations the browser offered, most specific first.
31    pub payloads: Vec<ExternalPayload>,
32    /// Files, if the drag carried any (also see [`crate::files`]).
33    pub files: Vec<dioxus::html::FileData>,
34    pub client: Point,
35    pub element: Point,
36}
37
38impl ExternalDrop {
39    /// The most specific text-ish payload, if any.
40    pub fn best(&self) -> Option<&ExternalPayload> {
41        self.payloads.first()
42    }
43
44    /// First URL payload, parsed out of `text/uri-list` (one URL per line,
45    /// `#` lines are comments).
46    pub fn url(&self) -> Option<&str> {
47        self.payloads.iter().find_map(|p| match p {
48            ExternalPayload::Url(u) => Some(u.as_str()),
49            _ => None,
50        })
51    }
52
53    /// First plain-text payload.
54    pub fn text(&self) -> Option<&str> {
55        self.payloads.iter().find_map(|p| match p {
56            ExternalPayload::Text(t) => Some(t.as_str()),
57            _ => None,
58        })
59    }
60}
61
62/// Decode an incoming drag event's `DataTransfer` into [`ExternalPayload`]s.
63pub fn classify(evt: &DragEvent) -> Vec<ExternalPayload> {
64    let dt = evt.data_transfer();
65    let mut out = Vec::new();
66    if let Some(uris) = dt.get_data("text/uri-list") {
67        for line in uris.lines() {
68            let line = line.trim();
69            if !line.is_empty() && !line.starts_with('#') {
70                out.push(ExternalPayload::Url(line.to_string()));
71            }
72        }
73    }
74    if let Some(html) = dt.get_data("text/html") {
75        if !html.is_empty() {
76            out.push(ExternalPayload::Html(html));
77        }
78    }
79    if let Some(text) = dt.get_data("text/plain") {
80        if !text.is_empty() {
81            out.push(ExternalPayload::Text(text));
82        }
83    }
84    out
85}
86
87/// A zone accepting drops that originate outside the app.
88#[component]
89pub fn ExternalDropZone(
90    on_drop: EventHandler<ExternalDrop>,
91    /// Fired with `true`/`false` on hover enter/leave.
92    #[props(default)]
93    on_hover: Option<EventHandler<bool>>,
94    #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
95    children: Element,
96) -> Element {
97    let mut depth = use_signal(|| 0u32);
98
99    rsx! {
100        div {
101            ondragover: move |evt: DragEvent| {
102                evt.prevent_default();
103            },
104            ondragenter: move |evt: DragEvent| {
105                evt.prevent_default();
106                let d = depth() + 1;
107                depth.set(d);
108                if d == 1 {
109                    if let Some(h) = &on_hover {
110                        h.call(true);
111                    }
112                }
113            },
114            ondragleave: move |_| {
115                let d = depth().saturating_sub(1);
116                depth.set(d);
117                if d == 0 {
118                    if let Some(h) = &on_hover {
119                        h.call(false);
120                    }
121                }
122            },
123            ondrop: move |evt: DragEvent| {
124                evt.prevent_default();
125                depth.set(0);
126                if let Some(h) = &on_hover {
127                    h.call(false);
128                }
129                let payloads = classify(&evt);
130                let files = evt.files();
131                if payloads.is_empty() && files.is_empty() {
132                    return;
133                }
134                on_drop.call(ExternalDrop {
135                    payloads,
136                    files,
137                    client: client_point(&evt),
138                    element: element_point(&evt),
139                });
140            },
141            ..attributes,
142            {children}
143        }
144    }
145}
146
147/// Typed payloads over the native `DataTransfer` (JSON-encoded under
148/// `application/json`, wire-compatible with dioxus-html's own
149/// `store`/`retrieve`). Useful when the browser must carry the data — e.g.
150/// dragging between two separate Dioxus apps or windows — at the cost of
151/// requiring `Serialize`/`Deserialize`.
152#[cfg(feature = "serde")]
153pub mod typed {
154    use dioxus::prelude::*;
155
156    /// Store a typed payload on the drag's `DataTransfer`. Call in `ondragstart`.
157    pub fn store<T: serde::Serialize>(evt: &DragEvent, value: &T) -> Result<(), String> {
158        let json = serde_json::to_string(value).map_err(|e| e.to_string())?;
159        evt.data_transfer().set_data("application/json", &json)
160    }
161
162    /// Retrieve a typed payload from a drop's `DataTransfer`. Call in `ondrop`.
163    pub fn retrieve<T: for<'de> serde::Deserialize<'de>>(
164        evt: &DragEvent,
165    ) -> Result<Option<T>, String> {
166        match evt.data_transfer().get_data("application/json") {
167            Some(json) => serde_json::from_str(&json)
168                .map(Some)
169                .map_err(|e| e.to_string()),
170            None => Ok(None),
171        }
172    }
173}