From 78d16536a783cddcc192d2fe19c821ee5ac87c1b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 13:57:28 -0800 Subject: [PATCH] =?UTF-8?q?wip:=20native=20file=20handles=20when=20droppin?= =?UTF-8?q?g=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/file_upload.rs | 7 +- packages/desktop/src/app.rs | 2 + packages/desktop/src/config.rs | 9 --- packages/desktop/src/desktop_context.rs | 14 +++- packages/desktop/src/file_upload.rs | 13 ++- packages/desktop/src/ipc.rs | 2 + packages/desktop/src/launch.rs | 1 + packages/desktop/src/webview.rs | 23 ++++-- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 35 ++++---- packages/interpreter/src/ts/native.ts | 102 ++++++++++++++++-------- 11 files changed, 134 insertions(+), 76 deletions(-) diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 4fe2a5a5b..f26b45477 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; use dioxus::{html::HasFileData, prelude::dioxus_elements::FileEngine}; fn main() { - launch(app); + LaunchBuilder::desktop().launch(app); } fn app() -> Element { @@ -51,7 +51,7 @@ fn app() -> Element { input { r#type: "file", - accept: ".txt,.rs", + accept: ".txt,.rs,.js", multiple: true, name: "textreader", directory: enable_directory_upload, @@ -64,10 +64,9 @@ fn app() -> Element { // cheating with a little bit of JS... "ondragover": "this.style.backgroundColor='#88FF88';", "ondragleave": "this.style.backgroundColor='#FFFFFF';", + "ondrop": "this.style.backgroundColor='#FFFFFF';", id: "drop-zone", - // prevent_default: "ondrop dragover dragenter", ondrop: handle_file_drop, - ondragover: move |event| event.stop_propagation(), "Drop files here" } diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 2927ff6c8..b05bb0946 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -262,6 +262,8 @@ impl App { } } + pub fn handle_file_drag(&mut self, msg: IpcMessage, window: WindowId) {} + pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) { let Ok(file_dialog) = serde_json::from_value::(msg.params()) else { return; diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index f84baf6ea..feedf00da 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -118,15 +118,6 @@ impl Config { self } - /// 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, - ) -> Self { - self.file_drop_handler = Some(Box::new(handler)); - self - } - /// Set a custom protocol pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self where diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 867516a64..55f3c6417 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -2,6 +2,7 @@ use crate::{ app::SharedContext, assets::AssetHandlerRegistry, edits::EditQueue, + file_upload::NativeFileHover, ipc::{EventData, UserWindowEvent}, query::QueryEngine, shortcut::{HotKey, ShortcutHandle, ShortcutRegistryError}, @@ -13,13 +14,17 @@ use dioxus_core::{ VirtualDom, }; use dioxus_interpreter_js::MutationState; -use std::{cell::RefCell, rc::Rc, rc::Weak}; +use std::{ + cell::RefCell, + rc::{Rc, Weak}, + sync::Arc, +}; use tao::{ event::Event, event_loop::EventLoopWindowTarget, window::{Fullscreen as WryFullscreen, Window, WindowId}, }; -use wry::{RequestAsyncResponder, WebView}; +use wry::{FileDropEvent, RequestAsyncResponder, WebView}; #[cfg(target_os = "ios")] use tao::platform::ios::WindowExtIOS; @@ -62,6 +67,7 @@ pub struct DesktopService { pub(crate) edit_queue: EditQueue, pub(crate) mutation_state: RefCell, pub(crate) asset_handlers: AssetHandlerRegistry, + pub(crate) file_hover: NativeFileHover, #[cfg(target_os = "ios")] pub(crate) views: Rc>>, @@ -83,14 +89,16 @@ impl DesktopService { shared: Rc, edit_queue: EditQueue, asset_handlers: AssetHandlerRegistry, + file_hover: NativeFileHover, ) -> Self { Self { window, webview, shared, edit_queue, - mutation_state: Default::default(), asset_handlers, + file_hover, + mutation_state: Default::default(), query: Default::default(), #[cfg(target_os = "ios")] views: Default::default(), diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index c2bd651c5..af6643b7a 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -2,7 +2,8 @@ use dioxus_html::{native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData}; use serde::Deserialize; -use std::{path::PathBuf, str::FromStr, sync::Arc}; +use std::{cell::Cell, path::PathBuf, rc::Rc, str::FromStr, sync::Arc}; +use wry::FileDropEvent; #[derive(Debug, Deserialize)] pub(crate) struct FileDialogRequest { @@ -142,3 +143,13 @@ impl HasFormData for DesktopFileUploadForm { self } } + +#[derive(Default, Clone)] +pub struct NativeFileHover { + event: Rc>>, +} +impl NativeFileHover { + pub fn set(&self, event: FileDropEvent) { + self.event.set(Some(event)); + } +} diff --git a/packages/desktop/src/ipc.rs b/packages/desktop/src/ipc.rs index 464b80423..69d8eb5a3 100644 --- a/packages/desktop/src/ipc.rs +++ b/packages/desktop/src/ipc.rs @@ -42,6 +42,7 @@ pub enum IpcMethod<'a> { Query, BrowserOpen, Initialize, + FileDrag, Other(&'a str), } @@ -50,6 +51,7 @@ impl IpcMessage { match self.method.as_str() { // todo: this is a misspelling, needs to be fixed "file_dialog" => IpcMethod::FileDialog, + "file_drag" => IpcMethod::FileDrag, "user_event" => IpcMethod::UserEvent, "query" => IpcMethod::Query, "browser_open" => IpcMethod::BrowserOpen, diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index b5602d10e..40029d87d 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -34,6 +34,7 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), EventData::Ipc(msg) => match msg.method() { IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), + IpcMethod::FileDrag => app.handle_file_drag(msg, id), IpcMethod::UserEvent => app.handle_user_event_msg(msg, id), IpcMethod::Query => app.handle_query_msg(msg, id), IpcMethod::BrowserOpen => app.handle_browser_open(msg), diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index ade023c2f..276327da4 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -3,8 +3,9 @@ use crate::{ assets::AssetHandlerRegistry, edits::EditQueue, eval::DesktopEvalProvider, + file_upload::NativeFileHover, ipc::{EventData, UserWindowEvent}, - protocol::{self}, + protocol, waker::tao_waker, Config, DesktopContext, DesktopService, }; @@ -65,7 +66,6 @@ impl WebviewInstance { // Rust :( let window_id = window.id(); - let file_handler = cfg.file_drop_handler.take(); let custom_head = cfg.custom_head.clone(); let index_file = cfg.custom_index.clone(); let root_name = cfg.root_name.clone(); @@ -102,6 +102,8 @@ impl WebviewInstance { } }; + let file_hover = NativeFileHover::default(); + #[cfg(any( target_os = "windows", target_os = "macos", @@ -128,12 +130,18 @@ impl WebviewInstance { .with_url("dioxus://index.html/") .unwrap() .with_ipc_handler(ipc_handler) + .with_navigation_handler(|var| var.contains("dioxus")) // prevent all navigations .with_asynchronous_custom_protocol(String::from("dioxus"), request_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)) - } + .with_web_context(&mut web_context) + .with_file_drop_handler({ + let file_hover = file_hover.clone(); + move |evt| { + println!("file drop: {:?}", evt); + // Update the most recent file hover status + file_hover.set(evt); + false + } + }); if let Some(color) = cfg.background_color { webview = webview.with_background_color(color); @@ -178,6 +186,7 @@ impl WebviewInstance { shared.clone(), edit_queue, asset_handlers, + file_hover, )); let provider: Rc = diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 31196d748..4bfd228e6 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -12023679989252671232 \ No newline at end of file +2617078454438840343 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index b47df8f8a..0bd145a16 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -248,6 +248,8 @@ class NativeInterpreter extends JSChannel_ { initialize(root) { this.intercept_link_redirects = true; this.liveview = false; + const dragEventHandler = (e) => { + }; window.addEventListener("dragover", function(e) { if (e.target instanceof Element && e.target.tagName != "INPUT") { e.preventDefault(); @@ -349,25 +351,8 @@ class NativeInterpreter extends JSChannel_ { } else { const message = this.serializeIpcMessage("user_event", body); this.ipc.postMessage(message); - console.log("sent message to host: ", message); } } - async readFiles(target, contents, bubbles, realId, name) { - let files = target.files; - let file_contents = {}; - for (let i = 0;i < files.length; i++) { - const file = files[i]; - file_contents[file.name] = Array.from(new Uint8Array(await file.arrayBuffer())); - } - contents.files = { files: file_contents }; - const message = this.serializeIpcMessage("user_event", { - name, - element: realId, - data: contents, - bubbles - }); - this.ipc.postMessage(message); - } preventDefaults(event, target) { let preventDefaultRequests = null; if (target instanceof Element) { @@ -415,6 +400,22 @@ class NativeInterpreter extends JSChannel_ { this.waitForRequest(headless); }); } + async readFiles(target, contents, bubbles, realId, name) { + let files = target.files; + let file_contents = {}; + for (let i = 0;i < files.length; i++) { + const file = files[i]; + file_contents[file.name] = Array.from(new Uint8Array(await file.arrayBuffer())); + } + contents.files = { files: file_contents }; + const message = this.serializeIpcMessage("user_event", { + name, + element: realId, + data: contents, + bubbles + }); + this.ipc.postMessage(message); + } } export { NativeInterpreter diff --git a/packages/interpreter/src/ts/native.ts b/packages/interpreter/src/ts/native.ts index 9755421a8..bcc810940 100644 --- a/packages/interpreter/src/ts/native.ts +++ b/packages/interpreter/src/ts/native.ts @@ -29,10 +29,50 @@ export class NativeInterpreter extends JSChannel_ { } initialize(root: HTMLElement): void { - this.intercept_link_redirects = true; this.liveview = false; + + const dragEventHandler = (e: DragEvent) => { + // e.preventDefault(); + // e.dataTransfer.effectAllowed = 'none'; + // e.dataTransfer.dropEffect = 'none'; + + // + + // we need to signal to the host to provide a native file path, + // not the one coming from here + + // We can't get native + // if (e.type === "drop") { + // let target = e.target; + + // if (target instanceof Element) { + // let target_id = getTargetId(target); + // if (target_id !== null) { + // const message = this.serializeIpcMessage("file_drop", { + // event: "drop", + // target: target_id, + // data: Array.from(e.dataTransfer.files).map(file => { + // return { + // name: file.name, + // type: file.type, + // size: file.size, + // }; + // }), + // }); + + // this.ipc.postMessage(message); + // } + // } + // } + }; + + + // ["dragenter", "dragover", "drop"].forEach((event) => { + // window.addEventListener(event, dragEventHandler, false) + // }); + // attach an event listener on the body that prevents file drops from navigating // this is because the browser will try to navigate to the file if it's dropped on the window window.addEventListener("dragover", function (e) { @@ -51,9 +91,6 @@ export class NativeInterpreter extends JSChannel_ { // Dropping a file on the window will navigate to the file, which we don't want e.preventDefault(); - - // if the element has a drop listener on it, we should send a message to the host with the contents of the drop instead - }, false); // attach a listener to the route that listens for clicks and prevents the default file dialog @@ -123,7 +160,7 @@ export class NativeInterpreter extends JSChannel_ { } } - // ignore the fact the base interpreter uses ptr + len but we use array + // ignore the fact the base interpreter uses ptr + len but we use array... // @ts-ignore loadChild(array: number[]) { // iterate through each number and get that child @@ -168,9 +205,6 @@ export class NativeInterpreter extends JSChannel_ { // Liveview will still need to use this this.preventDefaults(event, target); - - - // liveview does not have syncronous event handling, so we need to send the event to the host if (this.liveview) { // Okay, so the user might've requested some files to be read @@ -184,8 +218,6 @@ export class NativeInterpreter extends JSChannel_ { const message = this.serializeIpcMessage("user_event", body); this.ipc.postMessage(message); - console.log("sent message to host: ", message); - // // Run the event handler on the virtualdom // // capture/prevent default of the event if the virtualdom wants to // const res = handleVirtualdomEventSync(JSON.stringify(body)); @@ -200,30 +232,6 @@ export class NativeInterpreter extends JSChannel_ { } } - // A liveview only function - // Desktop will intercept the event before it hits this - async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) { - let files = target.files!; - let file_contents: { [name: string]: number[] } = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - - contents.files = { files: file_contents }; - - const message = this.serializeIpcMessage("user_event", { - name: name, - element: realId, - data: contents, - bubbles, - }); - - this.ipc.postMessage(message); - } // This should: @@ -308,6 +316,32 @@ export class NativeInterpreter extends JSChannel_ { this.waitForRequest(headless); }); } + + + // A liveview only function + // Desktop will intercept the event before it hits this + async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) { + let files = target.files!; + let file_contents: { [name: string]: number[] } = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + + contents.files = { files: file_contents }; + + const message = this.serializeIpcMessage("user_event", { + name: name, + element: realId, + data: contents, + bubbles, + }); + + this.ipc.postMessage(message); + } } type EventSyncResult = {