wip: native file handles when dropping 🎉

This commit is contained in:
Jonathan Kelley committed 2024-03-05 13:57:28 -08:00
1 parent 403e8e2f49
commit 78d16536a7
11 files changed
+134 -76

No files matched your search

+3 -4
View File
@@ -9,7 +9,7 @@ use dioxus::prelude::*;
use dioxus::{html::HasFileData, prelude::dioxus_elements::FileEngine}; use dioxus::{html::HasFileData, prelude::dioxus_elements::FileEngine};
fn main() { fn main() {
launch(app); LaunchBuilder::desktop().launch(app);
} }
fn app() -> Element { fn app() -> Element {
@@ -51,7 +51,7 @@ fn app() -> Element {
input { input {
r#type: "file", r#type: "file",
accept: ".txt,.rs", accept: ".txt,.rs,.js",
multiple: true, multiple: true,
name: "textreader", name: "textreader",
directory: enable_directory_upload, directory: enable_directory_upload,
@@ -64,10 +64,9 @@ fn app() -> Element {
// cheating with a little bit of JS... // cheating with a little bit of JS...
"ondragover": "this.style.backgroundColor='#88FF88';", "ondragover": "this.style.backgroundColor='#88FF88';",
"ondragleave": "this.style.backgroundColor='#FFFFFF';", "ondragleave": "this.style.backgroundColor='#FFFFFF';",
"ondrop": "this.style.backgroundColor='#FFFFFF';",
id: "drop-zone", id: "drop-zone",
// prevent_default: "ondrop dragover dragenter",
ondrop: handle_file_drop, ondrop: handle_file_drop,
ondragover: move |event| event.stop_propagation(),
"Drop files here" "Drop files here"
} }
+2
View File
@@ -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) { pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else { let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
return; return;
-9
View File
@@ -118,15 +118,6 @@ impl Config {
self 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 /// Set a custom protocol
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
where where
+11 -3
View File
@@ -2,6 +2,7 @@ use crate::{
app::SharedContext, app::SharedContext,
assets::AssetHandlerRegistry, assets::AssetHandlerRegistry,
edits::EditQueue, edits::EditQueue,
file_upload::NativeFileHover,
ipc::{EventData, UserWindowEvent}, ipc::{EventData, UserWindowEvent},
query::QueryEngine, query::QueryEngine,
shortcut::{HotKey, ShortcutHandle, ShortcutRegistryError}, shortcut::{HotKey, ShortcutHandle, ShortcutRegistryError},
@@ -13,13 +14,17 @@ use dioxus_core::{
VirtualDom, VirtualDom,
}; };
use dioxus_interpreter_js::MutationState; use dioxus_interpreter_js::MutationState;
use std::{cell::RefCell, rc::Rc, rc::Weak}; use std::{
cell::RefCell,
rc::{Rc, Weak},
sync::Arc,
};
use tao::{ use tao::{
event::Event, event::Event,
event_loop::EventLoopWindowTarget, event_loop::EventLoopWindowTarget,
window::{Fullscreen as WryFullscreen, Window, WindowId}, window::{Fullscreen as WryFullscreen, Window, WindowId},
}; };
use wry::{RequestAsyncResponder, WebView}; use wry::{FileDropEvent, RequestAsyncResponder, WebView};
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
use tao::platform::ios::WindowExtIOS; use tao::platform::ios::WindowExtIOS;
@@ -62,6 +67,7 @@ pub struct DesktopService {
pub(crate) edit_queue: EditQueue, pub(crate) edit_queue: EditQueue,
pub(crate) mutation_state: RefCell<MutationState>, pub(crate) mutation_state: RefCell<MutationState>,
pub(crate) asset_handlers: AssetHandlerRegistry, pub(crate) asset_handlers: AssetHandlerRegistry,
pub(crate) file_hover: NativeFileHover,
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>, pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
@@ -83,14 +89,16 @@ impl DesktopService {
shared: Rc<SharedContext>, shared: Rc<SharedContext>,
edit_queue: EditQueue, edit_queue: EditQueue,
asset_handlers: AssetHandlerRegistry, asset_handlers: AssetHandlerRegistry,
file_hover: NativeFileHover,
) -> Self { ) -> Self {
Self { Self {
window, window,
webview, webview,
shared, shared,
edit_queue, edit_queue,
mutation_state: Default::default(),
asset_handlers, asset_handlers,
file_hover,
mutation_state: Default::default(),
query: Default::default(), query: Default::default(),
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
views: Default::default(), views: Default::default(),
+12 -1
View File
@@ -2,7 +2,8 @@
use dioxus_html::{native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData}; use dioxus_html::{native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData};
use serde::Deserialize; 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)] #[derive(Debug, Deserialize)]
pub(crate) struct FileDialogRequest { pub(crate) struct FileDialogRequest {
@@ -142,3 +143,13 @@ impl HasFormData for DesktopFileUploadForm {
self 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
View File
@@ -42,6 +42,7 @@ pub enum IpcMethod<'a> {
Query, Query,
BrowserOpen, BrowserOpen,
Initialize, Initialize,
FileDrag,
Other(&'a str), Other(&'a str),
} }
@@ -50,6 +51,7 @@ impl IpcMessage {
match self.method.as_str() { match self.method.as_str() {
// todo: this is a misspelling, needs to be fixed // todo: this is a misspelling, needs to be fixed
"file_dialog" => IpcMethod::FileDialog, "file_dialog" => IpcMethod::FileDialog,
"file_drag" => IpcMethod::FileDrag,
"user_event" => IpcMethod::UserEvent, "user_event" => IpcMethod::UserEvent,
"query" => IpcMethod::Query, "query" => IpcMethod::Query,
"browser_open" => IpcMethod::BrowserOpen, "browser_open" => IpcMethod::BrowserOpen,
+1
View File
@@ -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::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg),
EventData::Ipc(msg) => match msg.method() { EventData::Ipc(msg) => match msg.method() {
IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), 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::UserEvent => app.handle_user_event_msg(msg, id),
IpcMethod::Query => app.handle_query_msg(msg, id), IpcMethod::Query => app.handle_query_msg(msg, id),
IpcMethod::BrowserOpen => app.handle_browser_open(msg), IpcMethod::BrowserOpen => app.handle_browser_open(msg),
+16 -7
View File
@@ -3,8 +3,9 @@ use crate::{
assets::AssetHandlerRegistry, assets::AssetHandlerRegistry,
edits::EditQueue, edits::EditQueue,
eval::DesktopEvalProvider, eval::DesktopEvalProvider,
file_upload::NativeFileHover,
ipc::{EventData, UserWindowEvent}, ipc::{EventData, UserWindowEvent},
protocol::{self}, protocol,
waker::tao_waker, waker::tao_waker,
Config, DesktopContext, DesktopService, Config, DesktopContext, DesktopService,
}; };
@@ -65,7 +66,6 @@ impl WebviewInstance {
// Rust :( // Rust :(
let window_id = window.id(); let window_id = window.id();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone(); let custom_head = cfg.custom_head.clone();
let index_file = cfg.custom_index.clone(); let index_file = cfg.custom_index.clone();
let root_name = cfg.root_name.clone(); let root_name = cfg.root_name.clone();
@@ -102,6 +102,8 @@ impl WebviewInstance {
} }
}; };
let file_hover = NativeFileHover::default();
#[cfg(any( #[cfg(any(
target_os = "windows", target_os = "windows",
target_os = "macos", target_os = "macos",
@@ -128,12 +130,18 @@ impl WebviewInstance {
.with_url("dioxus://index.html/") .with_url("dioxus://index.html/")
.unwrap() .unwrap()
.with_ipc_handler(ipc_handler) .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_asynchronous_custom_protocol(String::from("dioxus"), request_handler)
.with_web_context(&mut web_context); .with_web_context(&mut web_context)
.with_file_drop_handler({
if let Some(handler) = file_handler { let file_hover = file_hover.clone();
webview = webview.with_file_drop_handler(move |evt| handler(window_id, evt)) 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 { if let Some(color) = cfg.background_color {
webview = webview.with_background_color(color); webview = webview.with_background_color(color);
@@ -178,6 +186,7 @@ impl WebviewInstance {
shared.clone(), shared.clone(),
edit_queue, edit_queue,
asset_handlers, asset_handlers,
file_hover,
)); ));
let provider: Rc<dyn EvalProvider> = let provider: Rc<dyn EvalProvider> =
+1 -1
View File
@@ -1 +1 @@
12023679989252671232 2617078454438840343
+18 -17
View File
@@ -248,6 +248,8 @@ class NativeInterpreter extends JSChannel_ {
initialize(root) { initialize(root) {
this.intercept_link_redirects = true; this.intercept_link_redirects = true;
this.liveview = false; this.liveview = false;
const dragEventHandler = (e) => {
};
window.addEventListener("dragover", function(e) { window.addEventListener("dragover", function(e) {
if (e.target instanceof Element && e.target.tagName != "INPUT") { if (e.target instanceof Element && e.target.tagName != "INPUT") {
e.preventDefault(); e.preventDefault();
@@ -349,25 +351,8 @@ class NativeInterpreter extends JSChannel_ {
} else { } else {
const message = this.serializeIpcMessage("user_event", body); const message = this.serializeIpcMessage("user_event", body);
this.ipc.postMessage(message); 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) { preventDefaults(event, target) {
let preventDefaultRequests = null; let preventDefaultRequests = null;
if (target instanceof Element) { if (target instanceof Element) {
@@ -415,6 +400,22 @@ class NativeInterpreter extends JSChannel_ {
this.waitForRequest(headless); 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 { export {
NativeInterpreter NativeInterpreter
+68 -34
View File
@@ -29,10 +29,50 @@ export class NativeInterpreter extends JSChannel_ {
} }
initialize(root: HTMLElement): void { initialize(root: HTMLElement): void {
this.intercept_link_redirects = true; this.intercept_link_redirects = true;
this.liveview = false; 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 // 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 // this is because the browser will try to navigate to the file if it's dropped on the window
window.addEventListener("dragover", function (e) { 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 // Dropping a file on the window will navigate to the file, which we don't want
e.preventDefault(); 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); }, false);
// attach a listener to the route that listens for clicks and prevents the default file dialog // 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 // @ts-ignore
loadChild(array: number[]) { loadChild(array: number[]) {
// iterate through each number and get that child // iterate through each number and get that child
@@ -168,9 +205,6 @@ export class NativeInterpreter extends JSChannel_ {
// Liveview will still need to use this // Liveview will still need to use this
this.preventDefaults(event, target); this.preventDefaults(event, target);
// liveview does not have syncronous event handling, so we need to send the event to the host // liveview does not have syncronous event handling, so we need to send the event to the host
if (this.liveview) { if (this.liveview) {
// Okay, so the user might've requested some files to be read // 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); const message = this.serializeIpcMessage("user_event", body);
this.ipc.postMessage(message); this.ipc.postMessage(message);
console.log("sent message to host: ", message);
// // Run the event handler on the virtualdom // // Run the event handler on the virtualdom
// // capture/prevent default of the event if the virtualdom wants to // // capture/prevent default of the event if the virtualdom wants to
// const res = handleVirtualdomEventSync(JSON.stringify(body)); // 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: // This should:
@@ -308,6 +316,32 @@ export class NativeInterpreter extends JSChannel_ {
this.waitForRequest(headless); 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 = { type EventSyncResult = {