datatransfer object, drag_and_drop example, fix drag-and-drop (#4789)

* wip: implement datatransfer

* Editable kanban contents

* desktop sorta works without crashing

* lint
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-15 01:06:13 -07:00
1 parent 7856679641
commit 0aa7351473
13 files changed
+562 -122

No files matched your search

+5
View File
@@ -790,6 +790,11 @@ name = "websocket"
path = "examples/07-fullstack/websocket.rs"
doc-scrape-examples = true
[[example]]
name = "drag_and_drop"
path = "examples/08-apis/drag_and_drop.rs"
doc-scrape-examples = true
[[example]]
name = "control_focus"
path = "examples/08-apis/control_focus.rs"
+122
View File
@@ -0,0 +1,122 @@
//! This example shows how to implement a simple drag-and-drop kanban board using Dioxus.
//! You can drag items between different categories and edit their contents.
//!
//! This example uses the `.data_transfer()` API to handle drag-and-drop events. When an item is dragged,
//! its ID is stored in the data transfer object. When the item is dropped into a new category, its ID is retrieved
//! from the data transfer object and used to update the item's category.
//!
//! Note that in a real-world application, you'll want more sophisticated drop handling, such as visual
//! feedback during dragging, and better drop-zone detection to allow dropping *between* items.
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
struct Item {
id: usize,
name: String,
category: String,
contents: String,
}
fn app() -> Element {
let mut items = use_signal(initial_kanban_data);
rsx! {
div {
display: "flex",
gap: "20px",
flex_direction: "row",
for category in ["A", "B", "C"] {
div {
class: "category",
display: "flex",
flex_direction: "column",
gap: "10px",
padding: "10px",
flex_grow: "1",
border: "2px solid black",
min_height: "300px",
background_color: "#f0f0f0",
ondragover: |e| e.prevent_default(),
ondrop: move |e| {
if let Some(item_id) = e.data_transfer().get_data("text/plain").and_then(|data| data.parse::<usize>().ok()) {
if let Some(pos) = items.iter().position(|item| item.id == item_id) {
items.write()[pos].category = category.to_string();
}
}
},
h2 { "Category: {category}" }
for (index, item) in items.iter().enumerate().filter(|item| item.1.category == category) {
div {
key: "{item.id}",
width: "200px",
height: "50px",
border: "1px solid black",
padding: "10px",
class: "item",
draggable: "true",
background: "white",
cursor: "grab",
ondragstart: move |e| {
let id = items.read()[index].id.to_string();
e.data_transfer().set_data("text/plain", &id).unwrap();
},
pre { webkit_user_select: "none", "{item.name}" }
input {
r#type: "text",
value: "{item.contents}",
oninput: move |e| {
items.write()[index].contents = e.value();
}
}
}
}
}
}
}
}
}
fn initial_kanban_data() -> Vec<Item> {
vec![
Item {
id: 1,
name: "Item 1".into(),
category: "A".into(),
contents: "This is item 1".into(),
},
Item {
id: 2,
name: "Item 2".into(),
category: "A".into(),
contents: "This is item 2".into(),
},
Item {
id: 3,
name: "Item 3".into(),
category: "A".into(),
contents: "This is item 3".into(),
},
Item {
id: 4,
name: "Item 4".into(),
category: "B".into(),
contents: "This is item 4".into(),
},
Item {
id: 5,
name: "Item 5".into(),
category: "B".into(),
contents: "This is item 5".into(),
},
Item {
id: 6,
name: "Item 6".into(),
category: "C".into(),
contents: "This is item 6".into(),
},
]
}
+12 -2
View File
@@ -11,8 +11,9 @@ use dioxus_html::{
point_interaction::{
InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
},
FileData, FormValue, HasDragData, HasFileData, HasFormData, HasMouseData, NativeFileData,
SerializedFormData, SerializedFormObject, SerializedMouseData, SerializedPointInteraction,
FileData, FormValue, HasDataTransferData, HasDragData, HasFileData, HasFormData, HasMouseData,
NativeFileData, SerializedDataTransfer, SerializedFormData, SerializedFormObject,
SerializedMouseData, SerializedPointInteraction,
};
use serde::{Deserialize, Serialize};
@@ -234,6 +235,7 @@ impl NativeFileHover {
#[derive(Clone)]
pub(crate) struct DesktopFileDragEvent {
pub mouse: SerializedPointInteraction,
pub data_transfer: SerializedDataTransfer,
pub files: Vec<PathBuf>,
}
@@ -247,6 +249,12 @@ impl HasFileData for DesktopFileDragEvent {
}
}
impl HasDataTransferData for DesktopFileDragEvent {
fn data_transfer(&self) -> dioxus_html::DataTransfer {
dioxus_html::DataTransfer::new(self.data_transfer.clone())
}
}
impl HasDragData for DesktopFileDragEvent {
fn as_any(&self) -> &dyn std::any::Any {
self
@@ -389,3 +397,5 @@ impl NativeFileData for DesktopFileData {
)
}
}
pub struct DesktopDataTransfer {}
+6 -5
View File
@@ -155,16 +155,17 @@ impl WebviewEdits {
}
dioxus_html::EventData::Drag(ref drag) => {
// we want to override this with a native file engine, provided by the most recent drag event
let file_event = hovered_file.current().unwrap();
let paths = match file_event {
wry::DragDropEvent::Enter { paths, .. } => paths,
wry::DragDropEvent::Drop { paths, .. } => paths,
let file_event = hovered_file.current();
let file_paths = match file_event {
Some(wry::DragDropEvent::Enter { paths, .. }) => paths,
Some(wry::DragDropEvent::Drop { paths, .. }) => paths,
_ => vec![],
};
Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
mouse: drag.mouse.clone(),
files: paths,
data_transfer: drag.data_transfer.clone(),
files: file_paths,
})))
}
_ => data.into_any(),
+180
View File
@@ -0,0 +1,180 @@
pub struct DataTransfer {
inner: Box<dyn NativeDataTransfer>,
}
impl DataTransfer {
pub fn new(inner: impl NativeDataTransfer + 'static) -> Self {
Self {
inner: Box::new(inner),
}
}
#[cfg(feature = "serialize")]
pub fn store(&self, item: impl Serialize) -> Result<(), String> {
let serialized = serde_json::to_string(&item).map_err(|e| e.to_string())?;
self.set_data("application/json", &serialized)
}
#[cfg(feature = "serialize")]
pub fn retrieve<T: for<'de> serde::Deserialize<'de>>(&self) -> Result<Option<T>, String> {
if let Some(data) = self.get_data("application/json") {
let deserialized = serde_json::from_str(&data).map_err(|e| e.to_string())?;
Ok(Some(deserialized))
} else {
Ok(None)
}
}
pub fn get_data(&self, format: &str) -> Option<String> {
self.inner.get_data(format)
}
pub fn get_as_text(&self) -> Option<String> {
self.get_data("text/plain")
}
pub fn set_data(&self, format: &str, data: &str) -> Result<(), String> {
self.inner.set_data(format, data)
}
pub fn clear_data(&self, format: Option<&str>) -> Result<(), String> {
self.inner.clear_data(format)
}
pub fn effect_allowed(&self) -> String {
self.inner.effect_allowed()
}
pub fn set_effect_allowed(&self, effect: &str) {
self.inner.set_effect_allowed(effect)
}
pub fn drop_effect(&self) -> String {
self.inner.drop_effect()
}
pub fn set_drop_effect(&self, effect: &str) {
self.inner.set_drop_effect(effect)
}
pub fn files(&self) -> Vec<crate::file_data::FileData> {
self.inner.files()
}
}
pub trait NativeDataTransfer: Send + Sync {
fn get_data(&self, format: &str) -> Option<String>;
fn set_data(&self, format: &str, data: &str) -> Result<(), String>;
fn clear_data(&self, format: Option<&str>) -> Result<(), String>;
fn effect_allowed(&self) -> String;
fn set_effect_allowed(&self, effect: &str);
fn drop_effect(&self) -> String;
fn set_drop_effect(&self, effect: &str);
fn files(&self) -> Vec<crate::file_data::FileData>;
}
pub trait HasDataTransferData {
fn data_transfer(&self) -> DataTransfer;
}
#[cfg(feature = "serialize")]
pub use ser::*;
#[cfg(feature = "serialize")]
use serde::Serialize;
#[cfg(feature = "serialize")]
mod ser {
use crate::DragData;
use super::*;
use serde::{Deserialize, Serialize};
/// A serialized version of DataTransfer
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedDataTransfer {
pub items: Vec<SerializedDataTransferItem>,
pub files: Vec<crate::file_data::SerializedFileData>,
pub effect_allowed: String,
pub drop_effect: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedDataTransferItem {
pub kind: String,
pub type_: String,
pub data: String,
}
impl NativeDataTransfer for SerializedDataTransfer {
fn get_data(&self, format: &str) -> Option<String> {
self.items
.iter()
.find(|item| item.type_ == format)
.map(|item| item.data.clone())
}
fn set_data(&self, _format: &str, _data: &str) -> Result<(), String> {
// todo!()
// Err("Cannot set data on serialized DataTransfer".into())
Ok(())
}
fn clear_data(&self, _format: Option<&str>) -> Result<(), String> {
// todo!()
// Err("Cannot clear data on serialized DataTransfer".into())
Ok(())
}
fn effect_allowed(&self) -> String {
self.effect_allowed.clone()
}
fn set_effect_allowed(&self, _effect: &str) {
// No-op
}
fn drop_effect(&self) -> String {
self.drop_effect.clone()
}
fn set_drop_effect(&self, _effect: &str) {
// No-op
}
fn files(&self) -> Vec<crate::file_data::FileData> {
self.files
.iter()
.map(|f| crate::file_data::FileData::new(f.clone()))
.collect()
}
}
impl From<&DragData> for SerializedDataTransfer {
fn from(_drag: &DragData) -> Self {
todo!()
// let items = vec![]; // drag.data_transfer().items().iter().map(|item| SerializedDataTransferItem {
// // kind: item.kind().to_string(),
// // type_: item.type_().to_string(),
// // data: item.data().unwrap_or_default(),
// // }).collect();
// let files = drag
// .files()
// .iter()
// .map(|f| crate::file_data::SerializedFileData {
// name: f.name().to_string(),
// size: f.size(),
// type_: f.type_().to_string(),
// })
// .collect();
// Self {
// items,
// files,
// effect_allowed: drag.effect_allowed().to_string(),
// drop_effect: drag.drop_effect().to_string(),
// }
// }
}
}
}
+111 -99
View File
@@ -1,6 +1,9 @@
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
use crate::input_data::{MouseButton, MouseButtonSet};
use crate::*;
use crate::{
data_transfer::DataTransfer,
geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint},
};
use dioxus_core::Event;
use keyboard_types::Modifiers;
@@ -51,6 +54,11 @@ impl DragData {
}
}
/// The DataTransfer object is used to hold the data that is being dragged during a drag and drop operation.
pub fn data_transfer(&self) -> DataTransfer {
self.inner.data_transfer()
}
/// Downcast this event data to a specific type
#[inline(always)]
pub fn downcast<T: 'static>(&self) -> Option<&T> {
@@ -106,111 +114,115 @@ impl PointerInteraction for DragData {
}
#[cfg(feature = "serialize")]
/// A serialized version of DragData
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedDragData {
pub mouse: crate::point_interaction::SerializedPointInteraction,
pub files: Vec<SerializedFileData>,
}
pub use ser::*;
#[cfg(feature = "serialize")]
impl SerializedDragData {
fn new(drag: &DragData) -> Self {
Self {
mouse: crate::point_interaction::SerializedPointInteraction::from(drag),
files: vec![],
mod ser {
use super::*;
/// A serialized version of DragData
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedDragData {
pub mouse: crate::point_interaction::SerializedPointInteraction,
pub data_transfer: crate::data_transfer::SerializedDataTransfer,
}
impl SerializedDragData {
fn new(drag: &DragData) -> Self {
Self {
mouse: crate::point_interaction::SerializedPointInteraction::from(drag),
data_transfer: crate::data_transfer::SerializedDataTransfer::from(drag),
}
}
}
impl HasDataTransferData for SerializedDragData {
fn data_transfer(&self) -> crate::data_transfer::DataTransfer {
crate::data_transfer::DataTransfer::new(self.data_transfer.clone())
}
}
impl HasDragData for SerializedDragData {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl crate::file_data::HasFileData for SerializedDragData {
fn files(&self) -> Vec<FileData> {
self.data_transfer
.files
.iter()
.map(|f| FileData::new(f.clone()))
.collect()
}
}
impl HasMouseData for SerializedDragData {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl InteractionLocation for SerializedDragData {
fn client_coordinates(&self) -> ClientPoint {
self.mouse.client_coordinates()
}
fn page_coordinates(&self) -> PagePoint {
self.mouse.page_coordinates()
}
fn screen_coordinates(&self) -> ScreenPoint {
self.mouse.screen_coordinates()
}
}
impl InteractionElementOffset for SerializedDragData {
fn element_coordinates(&self) -> ElementPoint {
self.mouse.element_coordinates()
}
fn coordinates(&self) -> Coordinates {
self.mouse.coordinates()
}
}
impl ModifiersInteraction for SerializedDragData {
fn modifiers(&self) -> Modifiers {
self.mouse.modifiers()
}
}
impl PointerInteraction for SerializedDragData {
fn held_buttons(&self) -> MouseButtonSet {
self.mouse.held_buttons()
}
fn trigger_button(&self) -> Option<MouseButton> {
self.mouse.trigger_button()
}
}
impl serde::Serialize for DragData {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
SerializedDragData::new(self).serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for DragData {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let data = SerializedDragData::deserialize(deserializer)?;
Ok(Self {
inner: Box::new(data),
})
}
}
}
#[cfg(feature = "serialize")]
impl HasDragData for SerializedDragData {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(feature = "serialize")]
impl crate::file_data::HasFileData for SerializedDragData {
fn files(&self) -> Vec<FileData> {
self.files
.iter()
.map(|f| FileData::new(f.clone()))
.collect()
}
}
#[cfg(feature = "serialize")]
impl HasMouseData for SerializedDragData {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(feature = "serialize")]
impl InteractionLocation for SerializedDragData {
fn client_coordinates(&self) -> ClientPoint {
self.mouse.client_coordinates()
}
fn page_coordinates(&self) -> PagePoint {
self.mouse.page_coordinates()
}
fn screen_coordinates(&self) -> ScreenPoint {
self.mouse.screen_coordinates()
}
}
#[cfg(feature = "serialize")]
impl InteractionElementOffset for SerializedDragData {
fn element_coordinates(&self) -> ElementPoint {
self.mouse.element_coordinates()
}
fn coordinates(&self) -> Coordinates {
self.mouse.coordinates()
}
}
#[cfg(feature = "serialize")]
impl ModifiersInteraction for SerializedDragData {
fn modifiers(&self) -> Modifiers {
self.mouse.modifiers()
}
}
#[cfg(feature = "serialize")]
impl PointerInteraction for SerializedDragData {
fn held_buttons(&self) -> MouseButtonSet {
self.mouse.held_buttons()
}
fn trigger_button(&self) -> Option<MouseButton> {
self.mouse.trigger_button()
}
}
#[cfg(feature = "serialize")]
impl serde::Serialize for DragData {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
SerializedDragData::new(self).serialize(serializer)
}
}
#[cfg(feature = "serialize")]
impl<'de> serde::Deserialize<'de> for DragData {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let data = SerializedDragData::deserialize(deserializer)?;
Ok(Self {
inner: Box::new(data),
})
}
}
/// A trait for any object that has the data for a drag event
pub trait HasDragData: HasMouseData + crate::HasFileData {
pub trait HasDragData: HasMouseData + crate::HasFileData + crate::HasDataTransferData {
/// return self as Any
fn as_any(&self) -> &dyn std::any::Any;
}
+2
View File
@@ -25,10 +25,12 @@ pub mod events;
pub(crate) mod file_data;
pub use file_data::*;
mod attribute_groups;
mod data_transfer;
pub mod geometry;
pub mod input_data;
pub mod point_interaction;
mod render_template;
pub use data_transfer::*;
pub use bytes;
+1 -1
View File
@@ -1 +1 @@
[17669692872757955279, 11420464406527728232, 3770103091118609057, 5444526391971481782, 8889858244860485542, 5052021921702764563, 5113938988622536969, 11339769846046015954]
[17669692872757955279, 11420464406527728232, 3770103091118609057, 5444526391971481782, 8889858244860485542, 5052021921702764563, 15193138787416885137, 11339769846046015954]
+1 -1
View File
@@ -1 +1 @@
function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent<"u"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent){let files=[];if(event.dataTransfer&&event.dataTransfer.files)for(let i=0;i<event.dataTransfer.files.length;i++){let file=event.dataTransfer.files[i],data={path:file.name,size:file.size,last_modified:file.lastModified,content_type:file.type};files.push({key:file.name,file:data})}extend({files})}if(event.type==="scroll")extend(serializeScrollEvent(event));return contents}function toSerializableResizeObserverSize(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]}function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}function serializeInputEvent(event,target){let contents={};if(target instanceof HTMLElement){let values=extractSerializedFormValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents}function serializeWheelEvent(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}}function serializeTouchEvent(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:serializeTouchList(event.changedTouches),target_touches:serializeTouchList(event.targetTouches),touches:serializeTouchList(event.touches)}}function serializePointerEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}}function serializeTouchList(touchList){let serializedTouches=[];for(let i=0;i<touchList.length;i++){let touch=touchList[i];serializedTouches.push({identifier:touch.identifier,client_x:touch.clientX,client_y:touch.clientY,page_x:touch.pageX,page_y:touch.pageY,screen_x:touch.screenX,screen_y:touch.screenY,radius_x:touch.radiusX,radius_y:touch.radiusY,rotation_angle:touch.rotationAngle,force:touch.force})}return serializedTouches}function serializeMouseEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttonsLine truncated
function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent<"u"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent){let files=[];if(event.dataTransfer&&event.dataTransfer.files)for(let i=0;i<event.dataTransfer.files.length;i++){let file=event.dataTransfer.files[i],data={path:file.name,size:file.size,last_modified:file.lastModified,content_type:file.type};files.push({key:file.name,file:data})}extend({files})}if(event.type==="scroll")extend(serializeScrollEvent(event));return contents}function toSerializableResizeObserverSize(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]}function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}function serializeInputEvent(event,target){let contents={};if(target instanceof HTMLElement){let values=extractSerializedFormValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents}function serializeWheelEvent(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}}function serializeTouchEvent(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:serializeTouchList(event.changedTouches),target_touches:serializeTouchList(event.targetTouches),touches:serializeTouchList(event.touches)}}function serializePointerEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}}function serializeTouchList(touchList){let serializedTouches=[];for(let i=0;i<touchList.length;i++){let touch=touchList[i];serializedTouches.push({identifier:touch.identifier,client_x:touch.clientX,client_y:touch.clientY,page_x:touch.pageX,page_y:touch.pageY,screen_x:touch.screenX,screen_y:touch.screenY,radius_x:touch.radiusX,radius_y:touch.radiusY,rotation_angle:touch.rotationAngle,force:touch.force})}return serializedTouches}function serializeMouseEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttonsLine truncated
+44 -11
View File
@@ -321,17 +321,42 @@ function serializeAnimationEvent(event: AnimationEvent): SerializedEvent {
}
function serializeDragEvent(event: DragEvent): SerializedEvent {
let files = undefined;
// On desktop if there is file data, we insert it from wry. We just add a placeholder to let the rust side of dioxus know there's files
if (
event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files.length > 0
) {
files = {
files: { placeholder: [] },
};
let data_transfer = event.dataTransfer || new DataTransfer();
let items = [];
let files = [];
let effect_allowed = data_transfer.effectAllowed;
let drop_effect = data_transfer.dropEffect;
for (let i = 0; i < data_transfer.items.length; i++) {
let item = data_transfer.items[i];
let data;
if (item.kind === "string") {
// we can only get the data if we do this synchronously
// which is a bit sad, but oh well.
data = data_transfer.getData(item.type);
} else {
data = item.getAsFile()?.name || "";
}
items.push({
kind: item.kind,
type: item.type,
data
});
}
for (let i = 0; i < data_transfer.files.length; i++) {
let file = data_transfer.files[i];
files.push({
name: file.name,
path: file.name,
size: file.size,
last_modified: file.lastModified,
content_type: file.type,
contents: undefined, // we don't serialize contents here
});
}
return {
mouse: {
alt_key: event.altKey,
@@ -340,7 +365,12 @@ function serializeDragEvent(event: DragEvent): SerializedEvent {
shift_key: event.shiftKey,
...serializeMouseEvent(event),
},
files,
data_transfer: {
items,
files,
effect_allowed,
drop_effect,
},
};
}
@@ -458,3 +488,6 @@ export function retrieveSelectValue(target: HTMLSelectElement): string[] {
}
return values;
}
export type SerializedDataTransfer = {}
export type SerializedDataTransferItem = {}
+54
View File
@@ -0,0 +1,54 @@
use dioxus_html::{FileData, NativeDataTransfer};
/// A wrapper around the web_sys::DataTransfer to implement NativeDataTransfer
#[derive(Clone)]
pub struct WebDataTransfer {
pub(crate) data: web_sys::DataTransfer,
}
impl WebDataTransfer {
/// Create a new WebDataTransfer from a web_sys::DataTransfer
pub fn new(data: web_sys::DataTransfer) -> Self {
Self { data }
}
}
unsafe impl Send for WebDataTransfer {}
unsafe impl Sync for WebDataTransfer {}
impl NativeDataTransfer for WebDataTransfer {
fn get_data(&self, format: &str) -> Option<String> {
self.data.get_data(format).ok()
}
fn set_data(&self, format: &str, data: &str) -> Result<(), String> {
self.data.set_data(format, data).map_err(|e| {
format!(
"Failed to set data for format {format}: {:?}",
e.as_string()
)
})
}
fn clear_data(&self, format: Option<&str>) -> Result<(), String> {
match format {
Some(f) => self.data.clear_data_with_format(f),
None => self.data.clear_data(),
}
.map_err(|e| format!("{:?}", e))
}
fn effect_allowed(&self) -> String {
self.data.effect_allowed()
}
fn set_effect_allowed(&self, effect: &str) {
self.data.set_effect_allowed(effect);
}
fn drop_effect(&self) -> String {
self.data.drop_effect()
}
fn set_drop_effect(&self, effect: &str) {
self.data.set_drop_effect(effect);
}
fn files(&self) -> Vec<FileData> {
todo!()
}
}
+21 -3
View File
@@ -1,11 +1,12 @@
use crate::{WebFileData, WebFileEngine};
use crate::{WebDataTransfer, WebFileData, WebFileEngine};
use super::{Synthetic, WebEventExt};
use dioxus_html::{
geometry::{ClientPoint, ElementPoint, PagePoint, ScreenPoint},
input_data::{decode_mouse_button_set, MouseButton},
FileData, HasDragData, HasFileData, HasMouseData, InteractionElementOffset,
InteractionLocation, Modifiers, ModifiersInteraction, PointerInteraction,
FileData, HasDataTransferData, HasDragData, HasFileData, HasMouseData,
InteractionElementOffset, InteractionLocation, Modifiers, ModifiersInteraction,
PointerInteraction,
};
use web_sys::{DragEvent, FileReader};
@@ -72,6 +73,23 @@ impl HasDragData for Synthetic<DragEvent> {
}
}
impl HasDataTransferData for Synthetic<DragEvent> {
fn data_transfer(&self) -> dioxus_html::DataTransfer {
use wasm_bindgen::JsCast;
if let Some(target) = self.event.dyn_ref::<web_sys::DragEvent>() {
if let Some(data) = target.data_transfer() {
let web_data_transfer = WebDataTransfer::new(data);
return dioxus_html::DataTransfer::new(web_data_transfer);
}
}
// Return an empty DataTransfer if we couldn't get one from the event
let web_data_transfer = WebDataTransfer::new(web_sys::DataTransfer::new().unwrap());
dioxus_html::DataTransfer::new(web_data_transfer)
}
}
impl HasFileData for Synthetic<DragEvent> {
fn files(&self) -> Vec<FileData> {
use wasm_bindgen::JsCast;
+3
View File
@@ -30,6 +30,9 @@ pub use history::{HashHistory, WebHistory};
mod files;
pub use files::*;
mod data_transfer;
pub use data_transfer::*;
#[cfg(all(feature = "devtools", debug_assertions))]
mod devtools;