Skip to content

Commit 0aa7351

Browse files
authored
datatransfer object, drag_and_drop example, fix drag-and-drop (#4789)
* wip: implement datatransfer * Editable kanban contents * desktop sorta works without crashing * lint
1 parent 7856679 commit 0aa7351

File tree

13 files changed

+539
-99
lines changed

13 files changed

+539
-99
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,11 @@ name = "websocket"
790790
path = "examples/07-fullstack/websocket.rs"
791791
doc-scrape-examples = true
792792

793+
[[example]]
794+
name = "drag_and_drop"
795+
path = "examples/08-apis/drag_and_drop.rs"
796+
doc-scrape-examples = true
797+
793798
[[example]]
794799
name = "control_focus"
795800
path = "examples/08-apis/control_focus.rs"

examples/08-apis/drag_and_drop.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//! This example shows how to implement a simple drag-and-drop kanban board using Dioxus.
2+
//! You can drag items between different categories and edit their contents.
3+
//!
4+
//! This example uses the `.data_transfer()` API to handle drag-and-drop events. When an item is dragged,
5+
//! its ID is stored in the data transfer object. When the item is dropped into a new category, its ID is retrieved
6+
//! from the data transfer object and used to update the item's category.
7+
//!
8+
//! Note that in a real-world application, you'll want more sophisticated drop handling, such as visual
9+
//! feedback during dragging, and better drop-zone detection to allow dropping *between* items.
10+
11+
use dioxus::prelude::*;
12+
13+
fn main() {
14+
dioxus::launch(app);
15+
}
16+
17+
struct Item {
18+
id: usize,
19+
name: String,
20+
category: String,
21+
contents: String,
22+
}
23+
24+
fn app() -> Element {
25+
let mut items = use_signal(initial_kanban_data);
26+
27+
rsx! {
28+
div {
29+
display: "flex",
30+
gap: "20px",
31+
flex_direction: "row",
32+
for category in ["A", "B", "C"] {
33+
div {
34+
class: "category",
35+
display: "flex",
36+
flex_direction: "column",
37+
gap: "10px",
38+
padding: "10px",
39+
flex_grow: "1",
40+
border: "2px solid black",
41+
min_height: "300px",
42+
background_color: "#f0f0f0",
43+
ondragover: |e| e.prevent_default(),
44+
ondrop: move |e| {
45+
if let Some(item_id) = e.data_transfer().get_data("text/plain").and_then(|data| data.parse::<usize>().ok()) {
46+
if let Some(pos) = items.iter().position(|item| item.id == item_id) {
47+
items.write()[pos].category = category.to_string();
48+
}
49+
}
50+
},
51+
h2 { "Category: {category}" }
52+
for (index, item) in items.iter().enumerate().filter(|item| item.1.category == category) {
53+
div {
54+
key: "{item.id}",
55+
width: "200px",
56+
height: "50px",
57+
border: "1px solid black",
58+
padding: "10px",
59+
class: "item",
60+
draggable: "true",
61+
background: "white",
62+
cursor: "grab",
63+
ondragstart: move |e| {
64+
let id = items.read()[index].id.to_string();
65+
e.data_transfer().set_data("text/plain", &id).unwrap();
66+
},
67+
pre { webkit_user_select: "none", "{item.name}" }
68+
input {
69+
r#type: "text",
70+
value: "{item.contents}",
71+
oninput: move |e| {
72+
items.write()[index].contents = e.value();
73+
}
74+
}
75+
}
76+
}
77+
}
78+
}
79+
}
80+
}
81+
}
82+
83+
fn initial_kanban_data() -> Vec<Item> {
84+
vec![
85+
Item {
86+
id: 1,
87+
name: "Item 1".into(),
88+
category: "A".into(),
89+
contents: "This is item 1".into(),
90+
},
91+
Item {
92+
id: 2,
93+
name: "Item 2".into(),
94+
category: "A".into(),
95+
contents: "This is item 2".into(),
96+
},
97+
Item {
98+
id: 3,
99+
name: "Item 3".into(),
100+
category: "A".into(),
101+
contents: "This is item 3".into(),
102+
},
103+
Item {
104+
id: 4,
105+
name: "Item 4".into(),
106+
category: "B".into(),
107+
contents: "This is item 4".into(),
108+
},
109+
Item {
110+
id: 5,
111+
name: "Item 5".into(),
112+
category: "B".into(),
113+
contents: "This is item 5".into(),
114+
},
115+
Item {
116+
id: 6,
117+
name: "Item 6".into(),
118+
category: "C".into(),
119+
contents: "This is item 6".into(),
120+
},
121+
]
122+
}

packages/desktop/src/file_upload.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ use dioxus_html::{
1111
point_interaction::{
1212
InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
1313
},
14-
FileData, FormValue, HasDragData, HasFileData, HasFormData, HasMouseData, NativeFileData,
15-
SerializedFormData, SerializedFormObject, SerializedMouseData, SerializedPointInteraction,
14+
FileData, FormValue, HasDataTransferData, HasDragData, HasFileData, HasFormData, HasMouseData,
15+
NativeFileData, SerializedDataTransfer, SerializedFormData, SerializedFormObject,
16+
SerializedMouseData, SerializedPointInteraction,
1617
};
1718

1819
use serde::{Deserialize, Serialize};
@@ -234,6 +235,7 @@ impl NativeFileHover {
234235
#[derive(Clone)]
235236
pub(crate) struct DesktopFileDragEvent {
236237
pub mouse: SerializedPointInteraction,
238+
pub data_transfer: SerializedDataTransfer,
237239
pub files: Vec<PathBuf>,
238240
}
239241

@@ -247,6 +249,12 @@ impl HasFileData for DesktopFileDragEvent {
247249
}
248250
}
249251

252+
impl HasDataTransferData for DesktopFileDragEvent {
253+
fn data_transfer(&self) -> dioxus_html::DataTransfer {
254+
dioxus_html::DataTransfer::new(self.data_transfer.clone())
255+
}
256+
}
257+
250258
impl HasDragData for DesktopFileDragEvent {
251259
fn as_any(&self) -> &dyn std::any::Any {
252260
self
@@ -389,3 +397,5 @@ impl NativeFileData for DesktopFileData {
389397
)
390398
}
391399
}
400+
401+
pub struct DesktopDataTransfer {}

packages/desktop/src/webview.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,17 @@ impl WebviewEdits {
155155
}
156156
dioxus_html::EventData::Drag(ref drag) => {
157157
// we want to override this with a native file engine, provided by the most recent drag event
158-
let file_event = hovered_file.current().unwrap();
159-
let paths = match file_event {
160-
wry::DragDropEvent::Enter { paths, .. } => paths,
161-
wry::DragDropEvent::Drop { paths, .. } => paths,
158+
let file_event = hovered_file.current();
159+
let file_paths = match file_event {
160+
Some(wry::DragDropEvent::Enter { paths, .. }) => paths,
161+
Some(wry::DragDropEvent::Drop { paths, .. }) => paths,
162162
_ => vec![],
163163
};
164164

165165
Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
166166
mouse: drag.mouse.clone(),
167-
files: paths,
167+
data_transfer: drag.data_transfer.clone(),
168+
files: file_paths,
168169
})))
169170
}
170171
_ => data.into_any(),

packages/html/src/data_transfer.rs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
pub struct DataTransfer {
2+
inner: Box<dyn NativeDataTransfer>,
3+
}
4+
5+
impl DataTransfer {
6+
pub fn new(inner: impl NativeDataTransfer + 'static) -> Self {
7+
Self {
8+
inner: Box::new(inner),
9+
}
10+
}
11+
12+
#[cfg(feature = "serialize")]
13+
pub fn store(&self, item: impl Serialize) -> Result<(), String> {
14+
let serialized = serde_json::to_string(&item).map_err(|e| e.to_string())?;
15+
self.set_data("application/json", &serialized)
16+
}
17+
18+
#[cfg(feature = "serialize")]
19+
pub fn retrieve<T: for<'de> serde::Deserialize<'de>>(&self) -> Result<Option<T>, String> {
20+
if let Some(data) = self.get_data("application/json") {
21+
let deserialized = serde_json::from_str(&data).map_err(|e| e.to_string())?;
22+
Ok(Some(deserialized))
23+
} else {
24+
Ok(None)
25+
}
26+
}
27+
28+
pub fn get_data(&self, format: &str) -> Option<String> {
29+
self.inner.get_data(format)
30+
}
31+
32+
pub fn get_as_text(&self) -> Option<String> {
33+
self.get_data("text/plain")
34+
}
35+
36+
pub fn set_data(&self, format: &str, data: &str) -> Result<(), String> {
37+
self.inner.set_data(format, data)
38+
}
39+
40+
pub fn clear_data(&self, format: Option<&str>) -> Result<(), String> {
41+
self.inner.clear_data(format)
42+
}
43+
44+
pub fn effect_allowed(&self) -> String {
45+
self.inner.effect_allowed()
46+
}
47+
48+
pub fn set_effect_allowed(&self, effect: &str) {
49+
self.inner.set_effect_allowed(effect)
50+
}
51+
52+
pub fn drop_effect(&self) -> String {
53+
self.inner.drop_effect()
54+
}
55+
56+
pub fn set_drop_effect(&self, effect: &str) {
57+
self.inner.set_drop_effect(effect)
58+
}
59+
60+
pub fn files(&self) -> Vec<crate::file_data::FileData> {
61+
self.inner.files()
62+
}
63+
}
64+
65+
pub trait NativeDataTransfer: Send + Sync {
66+
fn get_data(&self, format: &str) -> Option<String>;
67+
fn set_data(&self, format: &str, data: &str) -> Result<(), String>;
68+
fn clear_data(&self, format: Option<&str>) -> Result<(), String>;
69+
fn effect_allowed(&self) -> String;
70+
fn set_effect_allowed(&self, effect: &str);
71+
fn drop_effect(&self) -> String;
72+
fn set_drop_effect(&self, effect: &str);
73+
fn files(&self) -> Vec<crate::file_data::FileData>;
74+
}
75+
76+
pub trait HasDataTransferData {
77+
fn data_transfer(&self) -> DataTransfer;
78+
}
79+
80+
#[cfg(feature = "serialize")]
81+
pub use ser::*;
82+
#[cfg(feature = "serialize")]
83+
use serde::Serialize;
84+
85+
#[cfg(feature = "serialize")]
86+
mod ser {
87+
use crate::DragData;
88+
89+
use super::*;
90+
use serde::{Deserialize, Serialize};
91+
92+
/// A serialized version of DataTransfer
93+
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
94+
pub struct SerializedDataTransfer {
95+
pub items: Vec<SerializedDataTransferItem>,
96+
pub files: Vec<crate::file_data::SerializedFileData>,
97+
pub effect_allowed: String,
98+
pub drop_effect: String,
99+
}
100+
101+
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
102+
pub struct SerializedDataTransferItem {
103+
pub kind: String,
104+
pub type_: String,
105+
pub data: String,
106+
}
107+
108+
impl NativeDataTransfer for SerializedDataTransfer {
109+
fn get_data(&self, format: &str) -> Option<String> {
110+
self.items
111+
.iter()
112+
.find(|item| item.type_ == format)
113+
.map(|item| item.data.clone())
114+
}
115+
116+
fn set_data(&self, _format: &str, _data: &str) -> Result<(), String> {
117+
// todo!()
118+
// Err("Cannot set data on serialized DataTransfer".into())
119+
Ok(())
120+
}
121+
122+
fn clear_data(&self, _format: Option<&str>) -> Result<(), String> {
123+
// todo!()
124+
// Err("Cannot clear data on serialized DataTransfer".into())
125+
Ok(())
126+
}
127+
128+
fn effect_allowed(&self) -> String {
129+
self.effect_allowed.clone()
130+
}
131+
132+
fn set_effect_allowed(&self, _effect: &str) {
133+
// No-op
134+
}
135+
136+
fn drop_effect(&self) -> String {
137+
self.drop_effect.clone()
138+
}
139+
140+
fn set_drop_effect(&self, _effect: &str) {
141+
// No-op
142+
}
143+
144+
fn files(&self) -> Vec<crate::file_data::FileData> {
145+
self.files
146+
.iter()
147+
.map(|f| crate::file_data::FileData::new(f.clone()))
148+
.collect()
149+
}
150+
}
151+
152+
impl From<&DragData> for SerializedDataTransfer {
153+
fn from(_drag: &DragData) -> Self {
154+
todo!()
155+
// let items = vec![]; // drag.data_transfer().items().iter().map(|item| SerializedDataTransferItem {
156+
// // kind: item.kind().to_string(),
157+
// // type_: item.type_().to_string(),
158+
// // data: item.data().unwrap_or_default(),
159+
// // }).collect();
160+
161+
// let files = drag
162+
// .files()
163+
// .iter()
164+
// .map(|f| crate::file_data::SerializedFileData {
165+
// name: f.name().to_string(),
166+
// size: f.size(),
167+
// type_: f.type_().to_string(),
168+
// })
169+
// .collect();
170+
171+
// Self {
172+
// items,
173+
// files,
174+
// effect_allowed: drag.effect_allowed().to_string(),
175+
// drop_effect: drag.drop_effect().to_string(),
176+
// }
177+
// }
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)