Преглед на файлове

Merge pull request #992 from ealmloff/add-file-data-drag-event

Add file property to drag data
Jonathan Kelley преди 1 година
родител
ревизия
9340558adc

+ 24 - 3
examples/file_upload.rs

@@ -39,9 +39,30 @@ fn App(cx: Scope) -> Element {
                     }
                 }
             },
-        },
-
-        div { "progress: {files_uploaded.read().len()}" },
+        }
+        div {
+            width: "100px",
+            height: "100px",
+            border: "1px solid black",
+            prevent_default: "ondrop dragover dragenter",
+            ondrop: move |evt| {
+                to_owned![files_uploaded];
+                async move {
+                    if let Some(file_engine) = &evt.files {
+                        let files = file_engine.files();
+                        for file_name in &files {
+                            if let Some(file) = file_engine.read_file_to_string(file_name).await{
+                                files_uploaded.write().push(file);
+                            }
+                        }
+                    }
+                }
+            },
+            ondragover: move |event: DragEvent| {
+                event.stop_propagation();
+            },
+            "Drop files here"
+        }
 
         ul {
             for file in files_uploaded.read().iter() {

+ 1 - 0
packages/core/src/diff/node.rs

@@ -0,0 +1 @@
+

+ 5 - 2
packages/desktop/src/app.rs

@@ -12,7 +12,8 @@ use crate::{
 use crossbeam_channel::Receiver;
 use dioxus_core::{Component, ElementId, VirtualDom};
 use dioxus_html::{
-    native_bind::NativeFileEngine, FileEngine, HasFormData, HtmlEvent, PlatformEventData,
+    native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData, HtmlEvent, MountedData,
+    PlatformEventData, SerializedHtmlEventConverter,
 };
 use std::{
     cell::{Cell, RefCell},
@@ -279,11 +280,13 @@ impl<P: 'static> App<P> {
             files: Arc<NativeFileEngine>,
         }
 
-        impl HasFormData for DesktopFileUploadForm {
+        impl HasFileData for DesktopFileUploadForm {
             fn files(&self) -> Option<Arc<dyn FileEngine>> {
                 Some(self.files.clone())
             }
+        }
 
+        impl HasFormData for DesktopFileUploadForm {
             fn as_any(&self) -> &dyn std::any::Any {
                 self
             }

+ 1 - 1
packages/desktop/src/config.rs

@@ -134,7 +134,7 @@ impl Config {
         self
     }
 
-    /// Set a file drop handler
+    /// Set a file drop handler. If this is enabled, html drag events will be disabled.
     pub fn with_file_drop_handler(
         mut self,
         handler: impl Fn(WindowId, FileDropEvent) -> bool + 'static,

+ 4 - 8
packages/desktop/src/webview.rs

@@ -88,22 +88,18 @@ impl WebviewInstance {
             }
         };
 
-        let file_drop_handler = move |event| {
-            file_handler
-                .as_ref()
-                .map(|handler| handler(window_id, event))
-                .unwrap_or_default()
-        };
-
         let mut webview = WebViewBuilder::new(&window)
             .with_transparent(cfg.window.window.transparent)
             .with_url("dioxus://index.html/")
             .unwrap()
             .with_ipc_handler(ipc_handler)
             .with_asynchronous_custom_protocol(String::from("dioxus"), request_handler)
-            .with_file_drop_handler(file_drop_handler)
             .with_web_context(&mut web_context);
 
+        if let Some(handler) = file_handler {
+            webview = webview.with_file_drop_handler(move |evt| handler(window_id, evt))
+        }
+
         // This was removed from wry, I'm not sure what replaced it
         // #[cfg(windows)]
         // {

+ 1 - 0
packages/html/Cargo.toml

@@ -34,6 +34,7 @@ features = [
     "TouchList",
     "TouchEvent",
     "MouseEvent",
+    "DragEvent",
     "InputEvent",
     "ClipboardEvent",
     "KeyboardEvent",

+ 34 - 7
packages/html/src/events/drag.rs

@@ -1,3 +1,4 @@
+use crate::file_data::{FileEngine, HasFileData};
 use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
 use crate::input_data::{MouseButton, MouseButtonSet};
 use crate::prelude::*;
@@ -7,6 +8,8 @@ use keyboard_types::Modifiers;
 
 use crate::HasMouseData;
 
+use std::fmt::Debug;
+
 pub type DragEvent = Event<DragData>;
 
 /// The DragEvent interface is a DOM event that represents a drag and drop interaction. The user initiates a drag by
@@ -53,7 +56,13 @@ impl DragData {
 
     /// Downcast this event data to a specific type
     pub fn downcast<T: 'static>(&self) -> Option<&T> {
-        self.inner.as_any().downcast_ref::<T>()
+        HasDragData::as_any(&*self.inner).downcast_ref::<T>()
+    }
+}
+
+impl HasFileData for DragData {
+    fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
+        self.inner.files()
     }
 }
 
@@ -103,19 +112,34 @@ impl PointerInteraction for DragData {
 #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
 pub struct SerializedDragData {
     mouse: crate::point_interaction::SerializedPointInteraction,
+    files: Option<crate::file_data::SerializedFileEngine>,
 }
 
 #[cfg(feature = "serialize")]
-impl From<&DragData> for SerializedDragData {
-    fn from(data: &DragData) -> Self {
+impl SerializedDragData {
+    fn new(drag: &DragData, files: Option<crate::file_data::SerializedFileEngine>) -> Self {
         Self {
-            mouse: crate::point_interaction::SerializedPointInteraction::from(data),
+            mouse: crate::point_interaction::SerializedPointInteraction::from(drag),
+            files,
         }
     }
 }
 
 #[cfg(feature = "serialize")]
-impl HasDragData for SerializedDragData {}
+impl HasDragData for SerializedDragData {
+    fn as_any(&self) -> &dyn std::any::Any {
+        self
+    }
+}
+
+#[cfg(feature = "serialize")]
+impl HasFileData for SerializedDragData {
+    fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
+        self.files
+            .as_ref()
+            .map(|files| std::sync::Arc::new(files.clone()) as _)
+    }
+}
 
 #[cfg(feature = "serialize")]
 impl HasMouseData for SerializedDragData {
@@ -171,7 +195,7 @@ impl PointerInteraction for SerializedDragData {
 #[cfg(feature = "serialize")]
 impl serde::Serialize for DragData {
     fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-        SerializedDragData::from(self).serialize(serializer)
+        SerializedDragData::new(self, None).serialize(serializer)
     }
 }
 
@@ -186,7 +210,10 @@ impl<'de> serde::Deserialize<'de> for DragData {
 }
 
 /// A trait for any object that has the data for a drag event
-pub trait HasDragData: HasMouseData {}
+pub trait HasDragData: HasMouseData + HasFileData {
+    /// return self as Any
+    fn as_any(&self) -> &dyn std::any::Any;
+}
 
 impl_event! {
     DragData;

+ 14 - 59
packages/html/src/events/form.rs

@@ -1,4 +1,6 @@
-use std::{any::Any, collections::HashMap, fmt::Debug};
+use crate::file_data::FileEngine;
+use crate::file_data::HasFileData;
+use std::{collections::HashMap, fmt::Debug};
 
 use dioxus_core::Event;
 
@@ -73,7 +75,7 @@ impl FormData {
 }
 
 /// An object that has all the data for a form event
-pub trait HasFormData: std::any::Any {
+pub trait HasFormData: HasFileData + std::any::Any {
     fn value(&self) -> String {
         Default::default()
     }
@@ -82,10 +84,6 @@ pub trait HasFormData: std::any::Any {
         Default::default()
     }
 
-    fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
-        None
-    }
-
     /// return self as Any
     fn as_any(&self) -> &dyn std::any::Any;
 }
@@ -120,7 +118,7 @@ impl FormData {
 pub struct SerializedFormData {
     value: String,
     values: HashMap<String, FormValue>,
-    files: Option<SerializedFileEngine>,
+    files: Option<crate::file_data::SerializedFileEngine>,
 }
 
 #[cfg(feature = "serialize")]
@@ -129,7 +127,7 @@ impl SerializedFormData {
     pub fn new(
         value: String,
         values: HashMap<String, FormValue>,
-        files: Option<SerializedFileEngine>,
+        files: Option<crate::file_data::SerializedFileEngine>,
     ) -> Self {
         Self {
             value,
@@ -152,7 +150,7 @@ impl SerializedFormData {
                         resolved_files.insert(file, bytes.unwrap_or_default());
                     }
 
-                    Some(SerializedFileEngine {
+                    Some(crate::file_data::SerializedFileEngine {
                         files: resolved_files,
                     })
                 }
@@ -180,15 +178,18 @@ impl HasFormData for SerializedFormData {
         self.values.clone()
     }
 
+    fn as_any(&self) -> &dyn std::any::Any {
+        self
+    }
+}
+
+#[cfg(feature = "serialize")]
+impl HasFileData for SerializedFormData {
     fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
         self.files
             .as_ref()
             .map(|files| std::sync::Arc::new(files.clone()) as _)
     }
-
-    fn as_any(&self) -> &dyn std::any::Any {
-        self
-    }
 }
 
 #[cfg(feature = "serialize")]
@@ -208,52 +209,6 @@ impl<'de> serde::Deserialize<'de> for FormData {
     }
 }
 
-#[cfg(feature = "serialize")]
-/// A file engine that serializes files to bytes
-#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
-pub struct SerializedFileEngine {
-    files: HashMap<String, Vec<u8>>,
-}
-
-#[cfg(feature = "serialize")]
-#[async_trait::async_trait(?Send)]
-impl FileEngine for SerializedFileEngine {
-    fn files(&self) -> Vec<String> {
-        self.files.keys().cloned().collect()
-    }
-
-    async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
-        self.files.get(file).cloned()
-    }
-
-    async fn read_file_to_string(&self, file: &str) -> Option<String> {
-        self.read_file(file)
-            .await
-            .map(|bytes| String::from_utf8_lossy(&bytes).to_string())
-    }
-
-    async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
-        self.read_file(file)
-            .await
-            .map(|val| Box::new(val) as Box<dyn Any>)
-    }
-}
-
-#[async_trait::async_trait(?Send)]
-pub trait FileEngine {
-    // get a list of file names
-    fn files(&self) -> Vec<String>;
-
-    // read a file to bytes
-    async fn read_file(&self, file: &str) -> Option<Vec<u8>>;
-
-    // read a file to string
-    async fn read_file_to_string(&self, file: &str) -> Option<String>;
-
-    // returns a file in platform's native representation
-    async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>>;
-}
-
 impl_event! {
     FormData;
 

+ 53 - 0
packages/html/src/file_data.rs

@@ -0,0 +1,53 @@
+use std::{any::Any, collections::HashMap};
+
+pub trait HasFileData: std::any::Any {
+    fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
+        None
+    }
+}
+
+#[cfg(feature = "serialize")]
+/// A file engine that serializes files to bytes
+#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
+pub struct SerializedFileEngine {
+    pub files: HashMap<String, Vec<u8>>,
+}
+
+#[cfg(feature = "serialize")]
+#[async_trait::async_trait(?Send)]
+impl FileEngine for SerializedFileEngine {
+    fn files(&self) -> Vec<String> {
+        self.files.keys().cloned().collect()
+    }
+
+    async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
+        self.files.get(file).cloned()
+    }
+
+    async fn read_file_to_string(&self, file: &str) -> Option<String> {
+        self.read_file(file)
+            .await
+            .map(|bytes| String::from_utf8_lossy(&bytes).to_string())
+    }
+
+    async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
+        self.read_file(file)
+            .await
+            .map(|val| Box::new(val) as Box<dyn Any>)
+    }
+}
+
+#[async_trait::async_trait(?Send)]
+pub trait FileEngine {
+    // get a list of file names
+    fn files(&self) -> Vec<String>;
+
+    // read a file to bytes
+    async fn read_file(&self, file: &str) -> Option<Vec<u8>>;
+
+    // read a file to string
+    async fn read_file_to_string(&self, file: &str) -> Option<String>;
+
+    // returns a file in platform's native representation
+    async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>>;
+}

+ 2 - 0
packages/html/src/lib.rs

@@ -22,6 +22,8 @@ pub use elements::HtmlCtx;
 #[cfg(feature = "html-to-rsx")]
 pub use elements::{map_html_attribute_to_rsx, map_html_element_to_rsx};
 pub mod events;
+pub(crate) mod file_data;
+pub use file_data::*;
 pub mod geometry;
 mod global_attributes;
 pub mod input_data;

+ 2 - 1
packages/html/src/native_bind/native_file_engine.rs

@@ -1,10 +1,11 @@
 use std::any::Any;
 use std::path::PathBuf;
 
-use crate::FileEngine;
 use tokio::fs::File;
 use tokio::io::AsyncReadExt;
 
+use crate::file_data::FileEngine;
+
 pub struct NativeFileEngine {
     files: Vec<PathBuf>,
 }

+ 29 - 2
packages/html/src/web_sys_bind/events.rs

@@ -3,6 +3,7 @@ use crate::events::{
     AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData,
     TransitionData, WheelData,
 };
+use crate::file_data::{FileEngine, HasFileData};
 use crate::geometry::{ClientPoint, ElementPoint, PagePoint, ScreenPoint};
 use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
 use crate::prelude::*;
@@ -103,8 +104,6 @@ impl ModifiersInteraction for KeyboardEvent {
     }
 }
 
-impl HasDragData for MouseEvent {}
-
 impl InteractionLocation for MouseEvent {
     fn client_coordinates(&self) -> ClientPoint {
         ClientPoint::new(self.client_x().into(), self.client_y().into())
@@ -162,6 +161,14 @@ impl HasMouseData for MouseEvent {
     }
 }
 
+impl HasFileData for MouseEvent {}
+
+impl HasDragData for MouseEvent {
+    fn as_any(&self) -> &dyn std::any::Any {
+        self
+    }
+}
+
 impl ModifiersInteraction for TouchEvent {
     fn modifiers(&self) -> Modifiers {
         let mut modifiers = Modifiers::empty();
@@ -512,3 +519,23 @@ impl HasMediaData for web_sys::Event {
         self
     }
 }
+
+impl HasFileData for web_sys::Event {
+    fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
+        #[cfg(not(feature = "file_engine"))]
+        let files = None;
+        #[cfg(feature = "file_engine")]
+        let files = element
+            .dyn_ref()
+            .and_then(|input: &web_sys::HtmlInputElement| {
+                input.files().and_then(|files| {
+                    #[allow(clippy::arc_with_non_send_sync)]
+                    crate::file_engine::WebFileEngine::new(files).map(|f| {
+                        std::sync::Arc::new(f) as std::sync::Arc<dyn dioxus_html::FileEngine>
+                    })
+                })
+            });
+
+        files
+    }
+}

+ 17 - 12
packages/interpreter/src/interpreter.js

@@ -6,7 +6,7 @@ class InterpreterConfig {
 
 // this handler is only provided on the desktop and liveview implementations since this
 // method is not used by the web implementation
-function handler(event, name, bubbles, config) {
+async function handler(event, name, bubbles, config) {
   let target = event.target;
   if (target != null) {
     let preventDefaultRequests = null;
@@ -61,7 +61,7 @@ function handler(event, name, bubbles, config) {
       event.preventDefault();
     }
 
-    let contents = serialize_event(event);
+    let contents = await serialize_event(event);
 
     // TODO: this should be liveview only
     if (
@@ -112,14 +112,14 @@ function handler(event, name, bubbles, config) {
         const fieldType = target.elements[name].type;
 
         switch (fieldType) {
-            case "select-multiple":
-                contents.values[name] = formData.getAll(name);
-                break;
-
-            // add cases for fieldTypes that can hold multiple values here
-            default:
-                contents.values[name] = formData.get(name);
-                break;
+          case "select-multiple":
+            contents.values[name] = formData.getAll(name);
+            break;
+
+          // add cases for fieldTypes that can hold multiple values here
+          default:
+            contents.values[name] = formData.get(name);
+            break;
         }
       }
     }
@@ -331,7 +331,7 @@ function get_mouse_data(event) {
   };
 }
 
-function serialize_event(event) {
+async function serialize_event(event) {
   switch (event.type) {
     case "copy":
     case "cut":
@@ -417,7 +417,12 @@ function serialize_event(event) {
     case "dragover":
     case "dragstart":
     case "drop": {
-      return { mouse: get_mouse_data(event) };
+      let files = null;
+      if (event.dataTransfer && event.dataTransfer.files) {
+        files = await serializeFileList(event.dataTransfer.files);
+      }
+
+      return { mouse: get_mouse_data(event), files };
     }
     case "click":
     case "contextmenu":

+ 4 - 2
packages/rink/src/hooks.rs

@@ -3,8 +3,8 @@ use crossterm::event::{
     MouseEventKind,
 };
 use dioxus_html::{
-    HasFormData, HasKeyboardData, HasWheelData, SerializedFocusData, SerializedKeyboardData,
-    SerializedMouseData, SerializedWheelData,
+    HasFileData, HasFormData, HasKeyboardData, HasWheelData, SerializedFocusData,
+    SerializedKeyboardData, SerializedMouseData, SerializedWheelData,
 };
 use dioxus_native_core::prelude::*;
 use dioxus_native_core::real_dom::NodeImmutable;
@@ -86,6 +86,8 @@ impl HasFormData for FormData {
     }
 }
 
+impl HasFileData for FormData {}
+
 #[derive(Clone, Debug, PartialEq)]
 pub struct Files {
     files: FxHashMap<String, File>,

+ 1 - 1
packages/web/Cargo.toml

@@ -42,7 +42,7 @@ features = [
     "HtmlFormElement",
     "Text",
     "Window",
-    "console",
+    "DataTransfer",
 ]
 
 [features]

+ 103 - 7
packages/web/src/event.rs

@@ -1,12 +1,16 @@
 use std::{any::Any, collections::HashMap};
 
 use dioxus_html::{
-    prelude::FormValue, FileEngine, FormData, HasFormData, HasImageData, HtmlEventConverter,
-    ImageData, MountedData, PlatformEventData, ScrollData,
+    point_interaction::{
+        InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
+    },
+    prelude::FormValue,
+    DragData, FileEngine, FormData, HasDragData, HasFileData, HasFormData, HasImageData,
+    HasMouseData, HtmlEventConverter, ImageData, MountedData, PlatformEventData, ScrollData,
 };
 use js_sys::Array;
 use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
-use web_sys::{Document, Element, Event};
+use web_sys::{Document, Element, Event, MouseEvent};
 
 pub(crate) struct WebEventConverter;
 
@@ -44,7 +48,11 @@ impl HtmlEventConverter for WebEventConverter {
 
     #[inline(always)]
     fn convert_drag_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::DragData {
-        downcast_event(event).raw.clone().into()
+        let event = downcast_event(event);
+        DragData::new(WebDragData::new(
+            event.element.clone(),
+            event.raw.clone().unchecked_into(),
+        ))
     }
 
     #[inline(always)]
@@ -180,8 +188,10 @@ impl WebEventExt<web_sys::CompositionEvent> for dioxus_html::CompositionData {
 
 impl WebEventExt<web_sys::MouseEvent> for dioxus_html::DragData {
     fn web_event(&self) -> &web_sys::MouseEvent {
-        self.downcast::<web_sys::MouseEvent>()
+        &self
+            .downcast::<WebDragData>()
             .expect("event should be a WebMouseEvent")
+            .raw
     }
 }
 
@@ -403,6 +413,12 @@ impl HasFormData for WebFormData {
         values
     }
 
+    fn as_any(&self) -> &dyn Any {
+        &self.raw as &dyn Any
+    }
+}
+
+impl HasFileData for WebFormData {
     fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
         #[cfg(not(feature = "file_engine"))]
         let files = None;
@@ -421,9 +437,89 @@ impl HasFormData for WebFormData {
 
         files
     }
+}
 
-    fn as_any(&self) -> &dyn Any {
-        &self.raw as &dyn Any
+struct WebDragData {
+    element: Element,
+    raw: MouseEvent,
+}
+
+impl WebDragData {
+    fn new(element: Element, raw: MouseEvent) -> Self {
+        Self { element, raw }
+    }
+}
+
+impl HasDragData for WebDragData {
+    fn as_any(&self) -> &dyn std::any::Any {
+        &self.raw as &dyn std::any::Any
+    }
+}
+
+impl HasMouseData for WebDragData {
+    fn as_any(&self) -> &dyn std::any::Any {
+        &self.raw as &dyn std::any::Any
+    }
+}
+
+impl PointerInteraction for WebDragData {
+    fn trigger_button(&self) -> Option<dioxus_html::input_data::MouseButton> {
+        self.raw.trigger_button()
+    }
+
+    fn held_buttons(&self) -> dioxus_html::input_data::MouseButtonSet {
+        self.raw.held_buttons()
+    }
+}
+
+impl ModifiersInteraction for WebDragData {
+    fn modifiers(&self) -> dioxus_html::prelude::Modifiers {
+        self.raw.modifiers()
+    }
+}
+
+impl InteractionElementOffset for WebDragData {
+    fn coordinates(&self) -> dioxus_html::geometry::Coordinates {
+        self.raw.coordinates()
+    }
+
+    fn element_coordinates(&self) -> dioxus_html::geometry::ElementPoint {
+        self.raw.element_coordinates()
+    }
+}
+
+impl InteractionLocation for WebDragData {
+    fn client_coordinates(&self) -> dioxus_html::geometry::ClientPoint {
+        self.raw.client_coordinates()
+    }
+
+    fn screen_coordinates(&self) -> dioxus_html::geometry::ScreenPoint {
+        self.raw.screen_coordinates()
+    }
+
+    fn page_coordinates(&self) -> dioxus_html::geometry::PagePoint {
+        self.raw.page_coordinates()
+    }
+}
+
+impl HasFileData for WebDragData {
+    fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
+        #[cfg(not(feature = "file_engine"))]
+        let files = None;
+        #[cfg(feature = "file_engine")]
+        let files = self
+            .element
+            .dyn_ref()
+            .and_then(|input: &web_sys::HtmlInputElement| {
+                input.files().and_then(|files| {
+                    #[allow(clippy::arc_with_non_send_sync)]
+                    crate::file_engine::WebFileEngine::new(files).map(|f| {
+                        std::sync::Arc::new(f) as std::sync::Arc<dyn dioxus_html::FileEngine>
+                    })
+                })
+            });
+
+        files
     }
 }