Synchronous prevent default (#2792)

* add prevent default methods to the event

* sync prevent default almost working

* sync prevent default working

* Move event handling into the runtime

* update core tests

* restore desktop file dialog

* implement prevent default on web

* add a hint about the new prevent default method

* fix web prevent default

* Fix CTRL+click on links

* fix values memorize in place test

* Fix a few more tests

* Add a playwright test for sync prevent default

* Fix core doc tests

* create a deprecated VirtualDom::handle_event

* fix macos imports in desktop

* Fix onmounted event

* Fix liveview support

* switch to RefCell for metadata

* Remove println

* remove prevent default attribute

* remove web specific link behavior

* Fix liveview links

* more liveview fixes for link

* Fix merge conflicts

* Fix clippy

* use the new prevent default in the file upload example
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-08-13 11:57:54 -07:00
1 parent 8b62b71e2d
commit cab573eefd
54 files changed
+940 -729

No files matched your search

+10 -64
View File
@@ -1,17 +1,16 @@
use crate::{
config::{Config, WindowCloseBehaviour},
element::DesktopElement,
event_handlers::WindowEventHandlers,
file_upload::{DesktopFileDragEvent, DesktopFileUploadForm, FileDialogRequest},
file_upload::{DesktopFileUploadForm, FileDialogRequest},
ipc::{IpcMessage, UserWindowEvent},
query::QueryResult,
shortcut::ShortcutRegistry,
webview::WebviewInstance,
};
use dioxus_core::ElementId;
use dioxus_core::VirtualDom;
use dioxus_html::{native_bind::NativeFileEngine, HasFileData, HtmlEvent, PlatformEventData};
use dioxus_core::{ElementId, VirtualDom};
use dioxus_html::{native_bind::NativeFileEngine, PlatformEventData};
use std::{
any::Any,
cell::{Cell, RefCell},
collections::HashMap,
rc::Rc,
@@ -244,9 +243,9 @@ impl App {
let view = self.webviews.get_mut(&id).unwrap();
view.dom
.rebuild(&mut *view.desktop_context.mutation_state.borrow_mut());
.rebuild(&mut *view.edits.wry_queue.mutation_state_mut());
view.desktop_context.send_edits();
view.edits.wry_queue.send_edits();
view.desktop_context
.window
@@ -277,55 +276,6 @@ impl App {
view.desktop_context.query.send(result);
}
pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) {
let parsed_params = serde_json::from_value(msg.params())
.map_err(|err| tracing::error!("Error parsing user_event: {:?}", err));
let Ok(evt) = parsed_params else { return };
let HtmlEvent {
element,
name,
bubbles,
data,
} = evt;
let view = self.webviews.get_mut(&id).unwrap();
let query = view.desktop_context.query.clone();
let recent_file = view.desktop_context.file_hover.clone();
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = match data {
dioxus_html::EventData::Mounted => {
let element = DesktopElement::new(element, view.desktop_context.clone(), query);
Rc::new(PlatformEventData::new(Box::new(element)))
}
dioxus_html::EventData::Drag(ref drag) => {
// we want to override this with a native file engine, provided by the most recent drag event
if drag.files().is_some() {
let file_event = recent_file.current().unwrap();
let paths = match file_event {
wry::DragDropEvent::Enter { paths, .. } => paths,
wry::DragDropEvent::Drop { paths, .. } => paths,
_ => vec![],
};
Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
mouse: drag.mouse.clone(),
files: Arc::new(NativeFileEngine::new(paths)),
})))
} else {
data.into_any()
}
}
_ => data.into_any(),
};
view.dom.handle_event(&name, as_any, element, bubbles);
view.dom
.render_immediate(&mut *view.desktop_context.mutation_state.borrow_mut());
view.desktop_context.send_edits();
}
#[cfg(all(
feature = "hot-reload",
debug_assertions,
@@ -378,17 +328,13 @@ impl App {
let view = self.webviews.get_mut(&window).unwrap();
let event = dioxus_core::Event::new(data as Rc<dyn Any>, event_bubbles);
if event_name == "change&input" {
view.dom
.handle_event("input", data.clone(), id, event_bubbles);
view.dom.handle_event("change", data, id, event_bubbles);
view.dom.runtime().handle_event("input", event.clone(), id);
view.dom.runtime().handle_event("change", event, id);
} else {
view.dom.handle_event(event_name, data, id, event_bubbles);
view.dom.runtime().handle_event(event_name, event, id);
}
view.dom
.render_immediate(&mut *view.desktop_context.mutation_state.borrow_mut());
view.desktop_context.send_edits();
}
/// Poll the virtualdom until it's pending
+2 -19
View File
@@ -1,7 +1,6 @@
use crate::{
app::SharedContext,
assets::AssetHandlerRegistry,
edits::EditQueue,
file_upload::NativeFileHover,
ipc::UserWindowEvent,
query::QueryEngine,
@@ -13,11 +12,7 @@ use dioxus_core::{
prelude::{current_scope_id, ScopeId},
VirtualDom,
};
use dioxus_interpreter_js::MutationState;
use std::{
cell::RefCell,
rc::{Rc, Weak},
};
use std::rc::{Rc, Weak};
use tao::{
event::Event,
event_loop::EventLoopWindowTarget,
@@ -63,13 +58,11 @@ pub struct DesktopService {
/// The receiver for queries about the current window
pub(super) query: QueryEngine,
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>>>,
pub(crate) views: Rc<std::cell::RefCell<Vec<*mut objc::runtime::Object>>>,
}
/// A smart pointer to the current window.
@@ -86,7 +79,6 @@ impl DesktopService {
webview: WebView,
window: Window,
shared: Rc<SharedContext>,
edit_queue: EditQueue,
asset_handlers: AssetHandlerRegistry,
file_hover: NativeFileHover,
) -> Self {
@@ -94,23 +86,14 @@ impl DesktopService {
window,
webview,
shared,
edit_queue,
asset_handlers,
file_hover,
mutation_state: Default::default(),
query: Default::default(),
#[cfg(target_os = "ios")]
views: Default::default(),
}
}
/// Send a list of mutations to the webview
pub(crate) fn send_edits(&self) {
let mut mutations = self.mutation_state.borrow_mut();
let serialized_edits = mutations.export_memory();
self.edit_queue.add_edits(serialized_edits);
}
/// Create a new window using the props and window builder
///
/// Returns the webview handle for the new window.
+39 -35
View File
@@ -1,64 +1,68 @@
use std::cell::Cell;
use std::{cell::RefCell, collections::VecDeque, rc::Rc, task::Waker};
use dioxus_interpreter_js::MutationState;
/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events.
/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made.
#[derive(Default, Clone)]
pub(crate) struct EditQueue {
queue: Rc<RefCell<VecDeque<Vec<u8>>>>,
responder: Rc<RefCell<Option<wry::RequestAsyncResponder>>>,
// Stores any futures waiting for edits to be applied to the webview
// NOTE: We don't use a Notify here because we need polling the notify to be cancel safe
waiting_for_edits_flushed: Rc<RefCell<Vec<Waker>>>,
// If this webview is currently waiting for an edit to be flushed. We don't run the virtual dom while this is true to avoid running effects before the dom has been updated
edits_in_progress: Rc<Cell<bool>>,
pub(crate) struct WryQueue {
inner: Rc<RefCell<WryQueueInner>>,
}
impl EditQueue {
#[derive(Default)]
pub(crate) struct WryQueueInner {
edit_queue: VecDeque<Vec<u8>>,
edit_responder: Option<wry::RequestAsyncResponder>,
// Stores any futures waiting for edits to be applied to the webview
// NOTE: We don't use a Notify here because we need polling the notify to be cancel safe
waiting_for_edits_flushed: Vec<Waker>,
// If this webview is currently waiting for an edit to be flushed. We don't run the virtual dom while this is true to avoid running effects before the dom has been updated
edits_in_progress: bool,
mutation_state: MutationState,
}
impl WryQueue {
pub fn handle_request(&self, responder: wry::RequestAsyncResponder) {
let mut queue = self.queue.borrow_mut();
if let Some(bytes) = queue.pop_back() {
let mut myself = self.inner.borrow_mut();
if let Some(bytes) = myself.edit_queue.pop_back() {
responder.respond(wry::http::Response::new(bytes));
} else {
// There are now no edits that need to be applied to the webview
self.edits_finished();
*self.responder.borrow_mut() = Some(responder);
for waker in myself.waiting_for_edits_flushed.drain(..) {
waker.wake();
}
myself.edits_in_progress = false;
myself.edit_responder = Some(responder);
}
}
pub fn add_edits(&self, edits: Vec<u8>) {
let mut responder = self.responder.borrow_mut();
pub fn mutation_state_mut(&self) -> std::cell::RefMut<'_, MutationState> {
std::cell::RefMut::map(self.inner.borrow_mut(), |myself| &mut myself.mutation_state)
}
/// Send a list of mutations to the webview
pub(crate) fn send_edits(&self) {
let mut myself = self.inner.borrow_mut();
let serialized_edits = myself.mutation_state.export_memory();
// There are pending edits that need to be applied to the webview before we run futures
self.start_edits();
if let Some(responder) = responder.take() {
responder.respond(wry::http::Response::new(edits));
myself.edits_in_progress = true;
if let Some(responder) = myself.edit_responder.take() {
responder.respond(wry::http::Response::new(serialized_edits));
} else {
self.queue.borrow_mut().push_front(edits);
myself.edit_queue.push_front(serialized_edits);
}
}
fn start_edits(&self) {
self.edits_in_progress.set(true);
}
fn edits_finished(&self) {
for waker in self.waiting_for_edits_flushed.borrow_mut().drain(..) {
waker.wake();
}
self.edits_in_progress.set(false);
}
fn edits_in_progress(&self) -> bool {
self.edits_in_progress.get()
self.inner.borrow().edits_in_progress
}
/// Wait until all pending edits have been rendered in the webview
pub fn poll_edits_flushed(&self, cx: &mut std::task::Context<'_>) -> std::task::Poll<()> {
if self.edits_in_progress() {
let mut myself = self.inner.borrow_mut();
let waker = cx.waker();
self.waiting_for_edits_flushed
.borrow_mut()
.push(waker.clone());
myself.waiting_for_edits_flushed.push(waker.clone());
std::task::Poll::Pending
} else {
std::task::Poll::Ready(())
+1 -1
View File
@@ -52,7 +52,7 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf
UserWindowEvent::Ipc { id, msg } => match msg.method() {
IpcMethod::Initialize => app.handle_initialize_msg(id),
IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id),
IpcMethod::UserEvent => app.handle_user_event_msg(msg, id),
IpcMethod::UserEvent => {}
IpcMethod::Query => app.handle_query_msg(msg, id),
IpcMethod::BrowserOpen => app.handle_browser_open(msg),
IpcMethod::Other(_) => {}
+20 -8
View File
@@ -1,4 +1,4 @@
use crate::{assets::*, edits::EditQueue};
use crate::{assets::*, webview::WebviewEdits};
use dioxus_html::document::NATIVE_EVAL_JS;
use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
use dioxus_interpreter_js::NATIVE_JS;
@@ -14,10 +14,16 @@ use wry::{
};
#[cfg(any(target_os = "android", target_os = "windows"))]
const EDITS_PATH: &str = "http://dioxus.index.html/edits";
const EDITS_PATH: &str = "http://dioxus.index.html/__edits";
#[cfg(not(any(target_os = "android", target_os = "windows")))]
const EDITS_PATH: &str = "dioxus://index.html/edits";
const EDITS_PATH: &str = "dioxus://index.html/__edits";
#[cfg(any(target_os = "android", target_os = "windows"))]
const EVENTS_PATH: &str = "http://dioxus.index.html/__events";
#[cfg(not(any(target_os = "android", target_os = "windows")))]
const EVENTS_PATH: &str = "dioxus://index.html/__events";
static DEFAULT_INDEX: &str = include_str!("./index.html");
@@ -149,12 +155,18 @@ fn resolve_resource(path: &Path) -> PathBuf {
pub(super) fn desktop_handler(
request: Request<Vec<u8>>,
asset_handlers: AssetHandlerRegistry,
edit_queue: &EditQueue,
responder: RequestAsyncResponder,
edit_state: &WebviewEdits,
) {
// If the request is asking for edits (ie binary protocol streaming, do that)
if request.uri().path().trim_matches('/') == "edits" {
return edit_queue.handle_request(responder);
// If the request is asking for edits (ie binary protocol streaming), do that
let trimmed_uri = request.uri().path().trim_matches('/');
if trimmed_uri == "__edits" {
return edit_state.wry_queue.handle_request(responder);
}
// If the request is asking for an event response, do that
if trimmed_uri == "__events" {
return edit_state.handle_event(request, responder);
}
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
@@ -222,7 +234,7 @@ fn module_loader(root_id: &str, headless: bool) -> String {
{NATIVE_JS}
// The native interpreter extends the sledgehammer interpreter with a few extra methods that we use for IPC
window.interpreter = new NativeInterpreter("{EDITS_PATH}");
window.interpreter = new NativeInterpreter("{EDITS_PATH}", "{EVENTS_PATH}");
// Wait for the page to load before sending the initialize message
window.onload = function() {{
+162 -48
View File
@@ -1,17 +1,129 @@
use crate::element::DesktopElement;
use crate::file_upload::DesktopFileDragEvent;
use crate::menubar::DioxusMenu;
use crate::{
app::SharedContext, assets::AssetHandlerRegistry, document::DesktopDocument, edits::EditQueue,
app::SharedContext, assets::AssetHandlerRegistry, document::DesktopDocument, edits::WryQueue,
file_upload::NativeFileHover, ipc::UserWindowEvent, protocol, waker::tao_waker, Config,
DesktopContext, DesktopService,
};
use dioxus_core::{ScopeId, VirtualDom};
use dioxus_core::{Runtime, ScopeId, VirtualDom};
use dioxus_hooks::to_owned;
use dioxus_html::document::Document;
use dioxus_html::native_bind::NativeFileEngine;
use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData};
use dioxus_interpreter_js::SynchronousEventResponse;
use futures_util::{pin_mut, FutureExt};
use std::cell::OnceCell;
use std::sync::Arc;
use std::{rc::Rc, task::Waker};
use wry::{RequestAsyncResponder, WebContext, WebViewBuilder};
#[derive(Clone)]
pub(crate) struct WebviewEdits {
runtime: Rc<Runtime>,
pub wry_queue: WryQueue,
desktop_context: Rc<OnceCell<DesktopContext>>,
}
impl WebviewEdits {
fn new(runtime: Rc<Runtime>, wry_queue: WryQueue) -> Self {
Self {
runtime,
wry_queue,
desktop_context: Default::default(),
}
}
fn set_desktop_context(&self, context: DesktopContext) {
_ = self.desktop_context.set(context);
}
pub fn handle_event(
&self,
request: wry::http::Request<Vec<u8>>,
responder: wry::RequestAsyncResponder,
) {
let body = self.try_handle_event(request).unwrap_or_default();
responder.respond(wry::http::Response::new(body))
}
pub fn try_handle_event(
&self,
request: wry::http::Request<Vec<u8>>,
) -> Result<Vec<u8>, serde_json::Error> {
let response = match serde_json::from_slice(request.body()) {
Ok(event) => self.handle_html_event(event),
Err(err) => {
tracing::error!("Error parsing user_event: {:?}", err);
SynchronousEventResponse::new(false)
}
};
let body = match serde_json::to_vec(&response) {
Ok(body) => body,
Err(err) => {
tracing::error!("failed to serialize SynchronousEventResponse: {err:?}");
return Err(err);
}
};
Ok(body)
}
pub fn handle_html_event(&self, event: HtmlEvent) -> SynchronousEventResponse {
let HtmlEvent {
element,
name,
bubbles,
data,
} = event;
let Some(desktop_context) = self.desktop_context.get() else {
tracing::error!(
"Tried to handle event before setting the desktop context on the event handler"
);
return Default::default();
};
let query = desktop_context.query.clone();
let recent_file = desktop_context.file_hover.clone();
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = match data {
dioxus_html::EventData::Mounted => {
let element = DesktopElement::new(element, desktop_context.clone(), query);
Rc::new(PlatformEventData::new(Box::new(element)))
}
dioxus_html::EventData::Drag(ref drag) => {
// we want to override this with a native file engine, provided by the most recent drag event
if drag.files().is_some() {
let file_event = recent_file.current().unwrap();
let paths = match file_event {
wry::DragDropEvent::Enter { paths, .. } => paths,
wry::DragDropEvent::Drop { paths, .. } => paths,
_ => vec![],
};
Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
mouse: drag.mouse.clone(),
files: Arc::new(NativeFileEngine::new(paths)),
})))
} else {
data.into_any()
}
}
_ => data.into_any(),
};
let event = dioxus_core::Event::new(as_any, bubbles);
self.runtime.handle_event(&name, event.clone(), element);
// Get the response from the event
SynchronousEventResponse::new(!event.default_action_enabled())
}
}
pub(crate) struct WebviewInstance {
pub dom: VirtualDom,
pub edits: WebviewEdits,
pub desktop_context: DesktopContext,
pub waker: Waker,
@@ -69,56 +181,57 @@ impl WebviewInstance {
}
let mut web_context = WebContext::new(cfg.data_dir.clone());
let edit_queue = EditQueue::default();
let file_hover = NativeFileHover::default();
let edit_queue = WryQueue::default();
let asset_handlers = AssetHandlerRegistry::new(dom.runtime());
let edits = WebviewEdits::new(dom.runtime(), edit_queue.clone());
let file_hover = NativeFileHover::default();
let headless = !cfg.window.window.visible;
// Rust :(
let window_id = window.id();
let custom_head = cfg.custom_head.clone();
let index_file = cfg.custom_index.clone();
let root_name = cfg.root_name.clone();
let asset_handlers_ = asset_handlers.clone();
let edit_queue_ = edit_queue.clone();
let proxy_ = shared.proxy.clone();
let file_hover_ = file_hover.clone();
let request_handler = {
to_owned![
cfg.custom_head,
cfg.custom_index,
cfg.root_name,
asset_handlers,
edits
];
move |request, responder: RequestAsyncResponder| {
// Try to serve the index file first
if let Some(index_bytes) = protocol::index_request(
&request,
custom_head.clone(),
custom_index.clone(),
&root_name,
headless,
) {
return responder.respond(index_bytes);
}
let request_handler = move |request, responder: RequestAsyncResponder| {
// Try to serve the index file first
let index_bytes = protocol::index_request(
&request,
custom_head.clone(),
index_file.clone(),
&root_name,
headless,
);
// Otherwise, try to serve an asset, either from the user or the filesystem
match index_bytes {
Some(body) => responder.respond(body),
None => protocol::desktop_handler(
request,
asset_handlers_.clone(),
&edit_queue_,
responder,
),
// Otherwise, try to serve an asset, either from the user or the filesystem
protocol::desktop_handler(request, asset_handlers.clone(), responder, &edits);
}
};
let ipc_handler = move |payload: wry::http::Request<String>| {
// defer the event to the main thread
let body = payload.into_body();
if let Ok(msg) = serde_json::from_str(&body) {
_ = proxy_.send_event(UserWindowEvent::Ipc { id: window_id, msg });
let ipc_handler = {
let window_id = window.id();
to_owned![shared.proxy];
move |payload: wry::http::Request<String>| {
// defer the event to the main thread
let body = payload.into_body();
if let Ok(msg) = serde_json::from_str(&body) {
_ = proxy.send_event(UserWindowEvent::Ipc { id: window_id, msg });
}
}
};
let file_drop_handler = move |evt| {
// Update the most recent file drop event - when the event comes in from the webview we can use the
// most recent event to build a new event with the files in it.
file_hover_.set(evt);
false
let file_drop_handler = {
to_owned![file_hover];
move |evt| {
// Update the most recent file drop event - when the event comes in from the webview we can use the
// most recent event to build a new event with the files in it.
file_hover.set(evt);
false
}
};
#[cfg(any(
@@ -216,22 +329,23 @@ impl WebviewInstance {
webview,
window,
shared.clone(),
edit_queue,
asset_handlers,
file_hover,
));
// Provide the desktop context to the virtual dom and edit handler
edits.set_desktop_context(desktop_context.clone());
let provider: Rc<dyn Document> = Rc::new(DesktopDocument::new(desktop_context.clone()));
dom.in_runtime(|| {
ScopeId::ROOT.provide_context(desktop_context.clone());
ScopeId::ROOT.provide_context(provider);
});
WebviewInstance {
dom,
edits,
waker: tao_waker(shared.proxy.clone(), desktop_context.window.id()),
desktop_context,
dom,
_menu: menu,
_web_context: web_context,
}
@@ -245,7 +359,7 @@ impl WebviewInstance {
// It will return Pending when it needs to be polled again - nothing is ready
loop {
// If we're waiting for a render, wait for it to finish before we continue
let edits_flushed_poll = self.desktop_context.edit_queue.poll_edits_flushed(&mut cx);
let edits_flushed_poll = self.edits.wry_queue.poll_edits_flushed(&mut cx);
if edits_flushed_poll.is_pending() {
return;
}
@@ -261,8 +375,8 @@ impl WebviewInstance {
}
self.dom
.render_immediate(&mut *self.desktop_context.mutation_state.borrow_mut());
self.desktop_context.send_edits();
.render_immediate(&mut *self.edits.wry_queue.mutation_state_mut());
self.edits.wry_queue.send_edits();
}
}