diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 882285ddf..2af49f848 100644 --- a/examples/file_upload.rs +++ b/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() { diff --git a/packages/core/src/diff/node.rs b/packages/core/src/diff/node.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/core/src/diff/node.rs @@ -0,0 +1 @@ + diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index df3efe349..8efda86e5 100644 --- a/packages/desktop/src/app.rs +++ b/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 App

{ files: Arc, } - impl HasFormData for DesktopFileUploadForm { + impl HasFileData for DesktopFileUploadForm { fn files(&self) -> Option> { Some(self.files.clone()) } + } + impl HasFormData for DesktopFileUploadForm { fn as_any(&self) -> &dyn std::any::Any { self } diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index a69c8c6df..2b4f5d0d6 100644 --- a/packages/desktop/src/config.rs +++ b/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, diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index a8a4dd23c..b23bd5ddd 100644 --- a/packages/desktop/src/webview.rs +++ b/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)] // { diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index f5ae62055..82d1640e3 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -34,6 +34,7 @@ features = [ "TouchList", "TouchEvent", "MouseEvent", + "DragEvent", "InputEvent", "ClipboardEvent", "KeyboardEvent", diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index e58c20e22..d8770872d 100644 --- a/packages/html/src/events/drag.rs +++ b/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; /// 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(&self) -> Option<&T> { - self.inner.as_any().downcast_ref::() + HasDragData::as_any(&*self.inner).downcast_ref::() + } +} + +impl HasFileData for DragData { + fn files(&self) -> Option> { + 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, } #[cfg(feature = "serialize")] -impl From<&DragData> for SerializedDragData { - fn from(data: &DragData) -> Self { +impl SerializedDragData { + fn new(drag: &DragData, files: Option) -> 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> { + 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(&self, serializer: S) -> Result { - 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; diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index ca1686cb7..d22a5219f 100644 --- a/packages/html/src/events/form.rs +++ b/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> { - 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, - files: Option, + files: Option, } #[cfg(feature = "serialize")] @@ -129,7 +127,7 @@ impl SerializedFormData { pub fn new( value: String, values: HashMap, - files: Option, + files: Option, ) -> 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> { 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>, -} - -#[cfg(feature = "serialize")] -#[async_trait::async_trait(?Send)] -impl FileEngine for SerializedFileEngine { - fn files(&self) -> Vec { - self.files.keys().cloned().collect() - } - - async fn read_file(&self, file: &str) -> Option> { - self.files.get(file).cloned() - } - - async fn read_file_to_string(&self, file: &str) -> Option { - self.read_file(file) - .await - .map(|bytes| String::from_utf8_lossy(&bytes).to_string()) - } - - async fn get_native_file(&self, file: &str) -> Option> { - self.read_file(file) - .await - .map(|val| Box::new(val) as Box) - } -} - -#[async_trait::async_trait(?Send)] -pub trait FileEngine { - // get a list of file names - fn files(&self) -> Vec; - - // read a file to bytes - async fn read_file(&self, file: &str) -> Option>; - - // read a file to string - async fn read_file_to_string(&self, file: &str) -> Option; - - // returns a file in platform's native representation - async fn get_native_file(&self, file: &str) -> Option>; -} - impl_event! { FormData; diff --git a/packages/html/src/file_data.rs b/packages/html/src/file_data.rs new file mode 100644 index 000000000..e206015bb --- /dev/null +++ b/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> { + 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>, +} + +#[cfg(feature = "serialize")] +#[async_trait::async_trait(?Send)] +impl FileEngine for SerializedFileEngine { + fn files(&self) -> Vec { + self.files.keys().cloned().collect() + } + + async fn read_file(&self, file: &str) -> Option> { + self.files.get(file).cloned() + } + + async fn read_file_to_string(&self, file: &str) -> Option { + self.read_file(file) + .await + .map(|bytes| String::from_utf8_lossy(&bytes).to_string()) + } + + async fn get_native_file(&self, file: &str) -> Option> { + self.read_file(file) + .await + .map(|val| Box::new(val) as Box) + } +} + +#[async_trait::async_trait(?Send)] +pub trait FileEngine { + // get a list of file names + fn files(&self) -> Vec; + + // read a file to bytes + async fn read_file(&self, file: &str) -> Option>; + + // read a file to string + async fn read_file_to_string(&self, file: &str) -> Option; + + // returns a file in platform's native representation + async fn get_native_file(&self, file: &str) -> Option>; +} diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 379b5f581..17ca25aef 100644 --- a/packages/html/src/lib.rs +++ b/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; diff --git a/packages/html/src/native_bind/native_file_engine.rs b/packages/html/src/native_bind/native_file_engine.rs index 88b996a4d..81070bf0d 100644 --- a/packages/html/src/native_bind/native_file_engine.rs +++ b/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, } diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index b1e6c5341..40d14f28b 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/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> { + #[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 + }) + }) + }); + + files + } +} diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 10e07a385..c8384a8b7 100644 --- a/packages/interpreter/src/interpreter.js +++ b/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; + 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; + // 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": diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index 012c0c1eb..d519273f0 100644 --- a/packages/rink/src/hooks.rs +++ b/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, diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 14ece61d0..12c2c974a 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -42,7 +42,7 @@ features = [ "HtmlFormElement", "Text", "Window", - "console", + "DataTransfer", ] [features] diff --git a/packages/web/src/event.rs b/packages/web/src/event.rs index 994bf304a..ae21414db 100644 --- a/packages/web/src/event.rs +++ b/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 for dioxus_html::CompositionData { impl WebEventExt for dioxus_html::DragData { fn web_event(&self) -> &web_sys::MouseEvent { - self.downcast::() + &self + .downcast::() .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> { #[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 { + 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> { + #[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 + }) + }) + }); + + files } }