ソースを参照

wip: native file handles when dropping 🎉

Jonathan Kelley 1 年間 前
コミット
78d16536a7

+ 3 - 4
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"
         }
 

+ 2 - 0
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::<FileDialogRequest>(msg.params()) else {
             return;

+ 0 - 9
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<F>(mut self, name: String, handler: F) -> Self
     where

+ 11 - 3
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<MutationState>,
     pub(crate) asset_handlers: AssetHandlerRegistry,
+    pub(crate) file_hover: NativeFileHover,
 
     #[cfg(target_os = "ios")]
     pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
@@ -83,14 +89,16 @@ impl DesktopService {
         shared: Rc<SharedContext>,
         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(),

+ 12 - 1
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<Cell<Option<FileDropEvent>>>,
+}
+impl NativeFileHover {
+    pub fn set(&self, event: FileDropEvent) {
+        self.event.set(Some(event));
+    }
+}

+ 2 - 0
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,

+ 1 - 0
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),

+ 16 - 7
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<dyn EvalProvider> =

+ 1 - 1
packages/interpreter/src/js/hash.txt

@@ -1 +1 @@
-12023679989252671232
+2617078454438840343

+ 18 - 17
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,24 +351,7 @@ 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;
@@ -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

+ 68 - 34
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 = {