diff --git a/Cargo.toml b/Cargo.toml index 7241731a9..c2e23f607 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,7 @@ http = "1.0.0" notify = { version = "6.1.1" } tower-http = "0.5.2" hyper = "1.0.0" -hyper-rustls = { version= "0.27.2", default-features = false , features=["native-tokio","http1","tls12","logging","ring"]} +hyper-rustls = { version= "0.27.2", default-features = false , features=["native-tokio","http1","http2","tls12","logging","ring"]} rustls = { version="0.23.12", default-features=false, features =["logging","std","tls12","ring"] } serde_json = "1.0.61" serde = "1.0.61" diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 3b73e61eb..9fb74072b 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -73,11 +73,14 @@ fn app() -> Element { div { id: "drop-zone", - prevent_default: "ondragover ondrop", background_color: if hovered() { "lightblue" } else { "lightgray" }, - ondragover: move |_| hovered.set(true), + ondragover: move |evt| { + evt.prevent_default(); + hovered.set(true) + }, ondragleave: move |_| hovered.set(false), ondrop: move |evt| async move { + evt.prevent_default(); hovered.set(false); if let Some(file_engine) = evt.files() { read_files(file_engine).await; diff --git a/examples/link.rs b/examples/link.rs index 620c645db..ff29b6562 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -70,8 +70,10 @@ fn DefaultLinks() -> Element { // It will just log "Hello Dioxus" to the console a { href: "http://dioxuslabs.com/", - prevent_default: "onclick", - onclick: |_| println!("Hello Dioxus"), + onclick: |event| { + event.prevent_default(); + println!("Hello Dioxus") + }, "Custom event link - links inside of your app" } } diff --git a/examples/svg.rs b/examples/svg.rs index ae71f2bb9..d4c23d1dd 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -40,8 +40,10 @@ fn Dice() -> Element { rsx! { svg { view_box: "-1000 -1000 2000 2000", - prevent_default: "onclick", - onclick: move |_| value.set(thread_rng().gen_range(1..=6)), + onclick: move |event| { + event.prevent_default(); + value.set(thread_rng().gen_range(1..=6)) + }, rect { x: -1000, y: -1000, width: 2000, height: 2000, rx: 200, fill: "#aaa" } for ((x, y), _) in DOTS.iter().zip(active_dots.read().iter()).filter(|(_, &active)| active) { circle { diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 470582773..bce2d2e9c 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -177,15 +177,15 @@ fn TodoEntry(mut todos: Signal>, id: u32) -> Element { label { r#for: "cbg-{id}", ondoubleclick: move |_| is_editing.set(true), - prevent_default: "onclick", + onclick: |evt| evt.prevent_default(), "{contents}" } button { class: "destroy", - onclick: move |_| { + onclick: move |evt| { + evt.prevent_default(); todos.write().remove(&id); }, - prevent_default: "onclick" } } @@ -220,41 +220,43 @@ fn ListFooter( let show_clear_completed = use_memo(move || todos.read().values().any(|todo| todo.checked)); rsx! { - footer { class: "footer", - span { class: "todo-count", - strong { "{active_todo_count} " } - span { - match active_todo_count() { - 1 => "item", - _ => "items", - } - " left" + footer { class: "footer", + span { class: "todo-count", + strong { "{active_todo_count} " } + span { + match active_todo_count() { + 1 => "item", + _ => "items", } + " left" } - ul { class: "filters", - for (state , state_text , url) in [ - (FilterState::All, "All", "#/"), - (FilterState::Active, "Active", "#/active"), - (FilterState::Completed, "Completed", "#/completed"), - ] { - li { - a { - href: url, - class: if filter() == state { "selected" }, - onclick: move |_| filter.set(state), - prevent_default: "onclick", - {state_text} - } + } + ul { class: "filters", + for (state , state_text , url) in [ + (FilterState::All, "All", "#/"), + (FilterState::Active, "Active", "#/active"), + (FilterState::Completed, "Completed", "#/completed"), + ] { + li { + a { + href: url, + class: if filter() == state { "selected" }, + onclick: move |evt| { + evt.prevent_default(); + filter.set(state) + }, + {state_text} } } } - if show_clear_completed() { - button { - class: "clear-completed", - onclick: move |_| todos.write().retain(|_, todo| !todo.checked), - "Clear completed" - } + } + if show_clear_completed() { + button { + class: "clear-completed", + onclick: move |_| todos.write().retain(|_, todo| !todo.checked), + "Clear completed" } } } + } } diff --git a/packages/cli/src/serve/proxy.rs b/packages/cli/src/serve/proxy.rs index 9adbb1f36..b5a04717f 100644 --- a/packages/cli/src/serve/proxy.rs +++ b/packages/cli/src/serve/proxy.rs @@ -28,7 +28,7 @@ impl ProxyClient { .with_native_roots() .unwrap() .https_or_http() - .enable_http1() + .enable_all_versions() .build(); Self { inner: legacy::Client::builder(TokioExecutor::new()).build(https), diff --git a/packages/cli/src/serve/server.rs b/packages/cli/src/serve/server.rs index ab125c779..1cbb943be 100644 --- a/packages/cli/src/serve/server.rs +++ b/packages/cli/src/serve/server.rs @@ -109,7 +109,7 @@ impl Server { // If we're serving a fullstack app, we need to find a port to proxy to let fullstack_port = if matches!( serve.build_arguments.platform(), - Platform::Fullstack | Platform::StaticGeneration + Platform::Liveview | Platform::Fullstack | Platform::StaticGeneration ) { get_available_port(addr.ip()) } else { @@ -379,7 +379,7 @@ fn setup_router( router = router.nest_service(&base_path, build_serve_dir(serve, config)); } - Platform::Fullstack | Platform::StaticGeneration => { + Platform::Liveview | Platform::Fullstack | Platform::StaticGeneration => { // For fullstack and static generation, forward all requests to the server let address = fullstack_address.unwrap(); diff --git a/packages/core-macro/tests/values_memoize_in_place.rs b/packages/core-macro/tests/values_memoize_in_place.rs index 974b2d158..fa782c422 100644 --- a/packages/core-macro/tests/values_memoize_in_place.rs +++ b/packages/core-macro/tests/values_memoize_in_place.rs @@ -1,6 +1,6 @@ use dioxus::prelude::*; use dioxus_core::ElementId; -use std::rc::Rc; +use std::{any::Any, rc::Rc}; #[tokio::test] async fn values_memoize_in_place() { @@ -49,12 +49,11 @@ async fn values_memoize_in_place() { println!("{:#?}", mutations); dom.mark_dirty(ScopeId::APP); for _ in 0..40 { - dom.handle_event( - "click", - Rc::new(PlatformEventData::new(Box::::default())), - ElementId(1), + let event = Event::new( + Rc::new(PlatformEventData::new(Box::::default())) as Rc, true, ); + dom.runtime().handle_event("click", event, ElementId(1)); tokio::select! { _ = tokio::time::sleep(std::time::Duration::from_millis(20)) => {}, _ = dom.wait_for_work() => {} @@ -95,12 +94,11 @@ fn cloning_event_handler_components_work() { println!("{:#?}", mutations); dom.mark_dirty(ScopeId::APP); for _ in 0..20 { - dom.handle_event( - "click", - Rc::new(PlatformEventData::new(Box::::default())), - ElementId(1), + let event = Event::new( + Rc::new(PlatformEventData::new(Box::::default())) as Rc, true, ); + dom.runtime().handle_event("click", event, ElementId(1)); dom.render_immediate(&mut dioxus_core::NoOpMutations); } dom.render_immediate(&mut dioxus_core::NoOpMutations); diff --git a/packages/core/README.md b/packages/core/README.md index 321088d28..267a0be3e 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -12,7 +12,10 @@ let real_dom = SomeRenderer::new(); loop { tokio::select! { - evt = real_dom.event() => vdom.handle_event("onclick", evt, ElementId(0), true), + evt = real_dom.event() => { + let evt = Event::new(evt, true); + vdom.runtime().handle_event("onclick", evt, ElementId(0)) + }, _ = vdom.wait_for_work() => {} } vdom.render_immediate(&mut real_dom.apply()) diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 79accf881..449aba810 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -56,7 +56,8 @@ pub struct ElementPath { impl VirtualDom { pub(crate) fn next_element(&mut self) -> ElementId { - ElementId(self.elements.insert(None)) + let mut elements = self.runtime.elements.borrow_mut(); + ElementId(elements.insert(None)) } pub(crate) fn reclaim(&mut self, el: ElementId) { @@ -71,7 +72,8 @@ impl VirtualDom { return true; } - self.elements.try_remove(el.0).is_some() + let mut elements = self.runtime.elements.borrow_mut(); + elements.try_remove(el.0).is_some() } // Drop a scope without dropping its children diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 6a8e77049..92c995c77 100644 --- a/packages/core/src/diff/component.rs +++ b/packages/core/src/diff/component.rs @@ -161,10 +161,10 @@ impl VNode { dom: &mut VirtualDom, mut to: Option<&mut impl WriteMutations>, ) { - let scope = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); + let scope = ScopeId(dom.get_mounted_dyn_node(mount, idx)); // Remove the scope id from the mount - dom.mounts[mount.0].mounted_dynamic_nodes[idx] = ScopeId::PLACEHOLDER.0; + dom.set_mounted_dyn_node(mount, idx, ScopeId::PLACEHOLDER.0); let m = self.create_component_node(mount, idx, new, parent, dom, to.as_deref_mut()); // Instead of *just* removing it, we can use the replace mutation @@ -188,7 +188,7 @@ impl VNode { return SuspenseBoundaryProps::create(mount, idx, component, parent, dom, to); } - let mut scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); + let mut scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx)); // If the scopeid is a placeholder, we need to load up a new scope for this vcomponent. If it's already mounted, then we can just use that if scope_id.is_placeholder() { @@ -198,7 +198,7 @@ impl VNode { .id; // Store the scope id for the next render - dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope_id.0; + dom.set_mounted_dyn_node(mount, idx, scope_id.0); // If this is a new scope, we also need to run it once to get the initial state let new = dom.run_scope(scope_id); @@ -207,7 +207,7 @@ impl VNode { dom.scopes[scope_id.0].last_rendered_node = Some(new); } - let scope = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); + let scope = ScopeId(dom.get_mounted_dyn_node(mount, idx)); let new_node = dom.scopes[scope.0] .last_rendered_node diff --git a/packages/core/src/diff/iterator.rs b/packages/core/src/diff/iterator.rs index d51e5f828..2ec81518b 100644 --- a/packages/core/src/diff/iterator.rs +++ b/packages/core/src/diff/iterator.rs @@ -469,7 +469,8 @@ impl VNode { ) -> usize { let template = self.template; - let mount = dom.mounts.get(self.mount.get().0).unwrap(); + let mounts = dom.runtime.mounts.borrow(); + let mount = mounts.get(self.mount.get().0).unwrap(); template .roots diff --git a/packages/core/src/diff/mod.rs b/packages/core/src/diff/mod.rs index 023796539..6aea3169f 100644 --- a/packages/core/src/diff/mod.rs +++ b/packages/core/src/diff/mod.rs @@ -10,10 +10,11 @@ #![allow(clippy::too_many_arguments)] use crate::{ + arena::MountId, innerlude::{ElementRef, WriteMutations}, nodes::VNode, virtual_dom::VirtualDom, - TemplateNode, + ElementId, TemplateNode, }; mod component; @@ -33,6 +34,46 @@ impl VirtualDom { .sum() } + pub(crate) fn get_mounted_parent(&self, mount: MountId) -> Option { + let mounts = self.runtime.mounts.borrow(); + mounts[mount.0].parent + } + + pub(crate) fn get_mounted_dyn_node(&self, mount: MountId, dyn_node_idx: usize) -> usize { + let mounts = self.runtime.mounts.borrow(); + mounts[mount.0].mounted_dynamic_nodes[dyn_node_idx] + } + + pub(crate) fn set_mounted_dyn_node(&self, mount: MountId, dyn_node_idx: usize, value: usize) { + let mut mounts = self.runtime.mounts.borrow_mut(); + mounts[mount.0].mounted_dynamic_nodes[dyn_node_idx] = value; + } + + pub(crate) fn get_mounted_dyn_attr(&self, mount: MountId, dyn_attr_idx: usize) -> ElementId { + let mounts = self.runtime.mounts.borrow(); + mounts[mount.0].mounted_attributes[dyn_attr_idx] + } + + pub(crate) fn set_mounted_dyn_attr( + &self, + mount: MountId, + dyn_attr_idx: usize, + value: ElementId, + ) { + let mut mounts = self.runtime.mounts.borrow_mut(); + mounts[mount.0].mounted_attributes[dyn_attr_idx] = value; + } + + pub(crate) fn get_mounted_root_node(&self, mount: MountId, root_idx: usize) -> ElementId { + let mounts = self.runtime.mounts.borrow(); + mounts[mount.0].root_ids[root_idx] + } + + pub(crate) fn set_mounted_root_node(&self, mount: MountId, root_idx: usize, value: ElementId) { + let mut mounts = self.runtime.mounts.borrow_mut(); + mounts[mount.0].root_ids[root_idx] = value; + } + /// Remove these nodes from the dom /// Wont generate mutations for the inner nodes fn remove_nodes( diff --git a/packages/core/src/diff/node.rs b/packages/core/src/diff/node.rs index d2ad6f278..55e94bfdc 100644 --- a/packages/core/src/diff/node.rs +++ b/packages/core/src/diff/node.rs @@ -19,13 +19,19 @@ impl VNode { mut to: Option<&mut impl WriteMutations>, ) { // The node we are diffing from should always be mounted - debug_assert!(dom.mounts.get(self.mount.get().0).is_some() || to.is_none()); + debug_assert!( + dom.runtime + .mounts + .borrow() + .get(self.mount.get().0) + .is_some() + || to.is_none() + ); // If the templates are different, we need to replace the entire template if self.template != new.template { let mount_id = self.mount.get(); - let mount = &dom.mounts[mount_id.0]; - let parent = mount.parent; + let parent = dom.get_mounted_parent(mount_id); return self.replace(std::slice::from_ref(new), parent, dom, to); } @@ -62,7 +68,8 @@ impl VNode { new.mount.set(mount_id); if mount_id.mounted() { - let mount = &mut dom.mounts[mount_id.0]; + let mut mounts = dom.runtime.mounts.borrow_mut(); + let mount = &mut mounts[mount_id.0]; // Update the reference to the node for bubbling events mount.node = new.clone_mounted(); @@ -83,8 +90,8 @@ impl VNode { (Text(old), Text(new)) => { // Diffing text is just a side effect, if we are diffing suspended nodes and are not outputting mutations, we can skip it if let Some(to) = to { - let mount = &dom.mounts[mount.0]; - self.diff_vtext(to, mount, idx, old, new) + let id = ElementId(dom.get_mounted_dyn_node(mount, idx)); + self.diff_vtext(to, id, old, new) } } (Placeholder(_), Placeholder(_)) => {} @@ -95,7 +102,7 @@ impl VNode { Some(self.reference_to_dynamic_node(mount, idx)), ), (Component(old), Component(new)) => { - let scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); + let scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx)); self.diff_vcomponent( mount, idx, @@ -114,20 +121,20 @@ impl VNode { // Mark the mount as unused. When a scope is created, it reads the mount and // if it is the placeholder value, it will create the scope, otherwise it will // reuse the scope - let old_mount = dom.mounts[mount.0].mounted_dynamic_nodes[idx]; - dom.mounts[mount.0].mounted_dynamic_nodes[idx] = usize::MAX; + let old_mount = dom.get_mounted_dyn_node(mount, idx); + dom.set_mounted_dyn_node(mount, idx, usize::MAX); let new_nodes_on_stack = self.create_dynamic_node(new, mount, idx, dom, to.as_deref_mut()); // Restore the mount for the scope we are removing - let new_mount = dom.mounts[mount.0].mounted_dynamic_nodes[idx]; - dom.mounts[mount.0].mounted_dynamic_nodes[idx] = old_mount; + let new_mount = dom.get_mounted_dyn_node(mount, idx); + dom.set_mounted_dyn_node(mount, idx, old_mount); self.remove_dynamic_node(mount, dom, to, true, idx, old, Some(new_nodes_on_stack)); // Restore the mount for the node we created - dom.mounts[mount.0].mounted_dynamic_nodes[idx] = new_mount; + dom.set_mounted_dyn_node(mount, idx, new_mount); } }; } @@ -143,12 +150,14 @@ impl VNode { } pub(crate) fn find_first_element(&self, dom: &VirtualDom) -> ElementId { - let mount = &dom.mounts[self.mount.get().0]; + let mount_id = self.mount.get(); let first = match self.get_dynamic_root_node_and_id(0) { // This node is static, just get the root id - None => mount.root_ids[0], + None => dom.get_mounted_root_node(mount_id, 0), // If it is dynamic and shallow, grab the id from the mounted dynamic nodes - Some((idx, Placeholder(_) | Text(_))) => ElementId(mount.mounted_dynamic_nodes[idx]), + Some((idx, Placeholder(_) | Text(_))) => { + ElementId(dom.get_mounted_dyn_node(mount_id, idx)) + } // The node is a fragment, so we need to find the first element in the fragment Some((_, Fragment(children))) => { let child = children.first().unwrap(); @@ -156,7 +165,7 @@ impl VNode { } // The node is a component, so we need to find the first element in the component Some((id, Component(_))) => { - let scope = ScopeId(mount.mounted_dynamic_nodes[id]); + let scope = ScopeId(dom.get_mounted_dyn_node(mount_id, id)); dom.get_scope(scope) .unwrap() .root_node() @@ -171,13 +180,15 @@ impl VNode { } pub(crate) fn find_last_element(&self, dom: &VirtualDom) -> ElementId { - let mount = &dom.mounts[self.mount.get().0]; + let mount_id = self.mount.get(); let last_root_index = self.template.roots.len() - 1; let last = match self.get_dynamic_root_node_and_id(last_root_index) { // This node is static, just get the root id - None => mount.root_ids[last_root_index], + None => dom.get_mounted_root_node(mount_id, last_root_index), // If it is dynamic and shallow, grab the id from the mounted dynamic nodes - Some((idx, Placeholder(_) | Text(_))) => ElementId(mount.mounted_dynamic_nodes[idx]), + Some((idx, Placeholder(_) | Text(_))) => { + ElementId(dom.get_mounted_dyn_node(mount_id, idx)) + } // The node is a fragment, so we need to find the first element in the fragment Some((_, Fragment(children))) => { let child = children.first().unwrap(); @@ -185,7 +196,7 @@ impl VNode { } // The node is a component, so we need to find the first element in the component Some((id, Component(_))) => { - let scope = ScopeId(mount.mounted_dynamic_nodes[id]); + let scope = ScopeId(dom.get_mounted_dyn_node(mount_id, id)); dom.get_scope(scope) .unwrap() .root_node() @@ -202,16 +213,8 @@ impl VNode { /// Diff the two text nodes /// /// This just sets the text of the node if it's different. - fn diff_vtext( - &self, - to: &mut impl WriteMutations, - mount: &VNodeMount, - idx: usize, - left: &VText, - right: &VText, - ) { + fn diff_vtext(&self, to: &mut impl WriteMutations, id: ElementId, left: &VText, right: &VText) { if left.value != right.value { - let id = ElementId(mount.mounted_dynamic_nodes[idx]); to.set_node_text(&right.value, id); } } @@ -291,7 +294,7 @@ impl VNode { if destroy_component_state { // Remove the mount information - dom.mounts.remove(mount.0); + dom.runtime.mounts.borrow_mut().remove(mount.0); } } @@ -318,8 +321,7 @@ impl VNode { replace_with.filter(|_| last_node), ); } else if let Some(to) = to.as_deref_mut() { - let mount = &dom.mounts[mount.0]; - let id = mount.root_ids[idx]; + let id = dom.get_mounted_root_node(mount, idx); if let (true, Some(replace_with)) = (last_node, replace_with) { to.replace_node_with(id, replace_with); } else { @@ -366,11 +368,11 @@ impl VNode { ) { match node { Component(_comp) => { - let scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); + let scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx)); dom.remove_component_node(to, destroy_component_state, scope_id, replace_with); } Text(_) | Placeholder(_) => { - let id = ElementId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); + let id = ElementId(dom.get_mounted_dyn_node(mount, idx)); if let Some(to) = to { if let Some(replace_with) = replace_with { to.replace_node_with(id, replace_with); @@ -400,7 +402,7 @@ impl VNode { } // only reclaim the new element if it's different from the previous one - let new_id = dom.mounts[mount.0].mounted_attributes[idx]; + let new_id = dom.get_mounted_dyn_attr(mount, idx); if Some(new_id) != next_id { dom.reclaim(new_id); next_id = Some(new_id); @@ -423,7 +425,7 @@ impl VNode { { let mut old_attributes_iter = old_attrs.iter().peekable(); let mut new_attributes_iter = new_attrs.iter().peekable(); - let attribute_id = dom.mounts[mount_id.0].mounted_attributes[idx]; + let attribute_id = dom.get_mounted_dyn_attr(mount_id, idx); let path = self.template.attr_paths[idx]; loop { @@ -505,7 +507,8 @@ impl VNode { path: ElementPath { path }, mount, }; - dom.elements[id.0] = Some(element_ref); + let mut elements = dom.runtime.elements.borrow_mut(); + elements[id.0] = Some(element_ref); to.create_event_listener(&attribute.name[2..], id); } _ => { @@ -526,7 +529,8 @@ impl VNode { // Initialize the mount information for this vnode if it isn't already mounted if !self.mount.get().mounted() { - let entry = dom.mounts.vacant_entry(); + let mut mounts = dom.runtime.mounts.borrow_mut(); + let entry = mounts.vacant_entry(); let mount = MountId(entry.key()); self.mount.set(mount); tracing::trace!(?self, ?mount, "creating template"); @@ -549,7 +553,7 @@ impl VNode { // Get the mounted id of this block // At this point, we should have already mounted the block debug_assert!( - dom.mounts.contains( + dom.runtime.mounts.borrow().contains( self.mount .get() .as_usize() @@ -752,7 +756,7 @@ impl VNode { for attr in &**attribute { self.write_attribute(attribute_path, attr, id, mount, dom, to); - dom.mounts[mount.0].mounted_attributes[attribute_idx] = id; + dom.set_mounted_dyn_attr(mount, attribute_idx, id); } } } @@ -766,7 +770,7 @@ impl VNode { ) -> ElementId { // Get an ID for this root since it's a real root let this_id = dom.next_element(); - dom.mounts[mount.0].root_ids[root_idx] = this_id; + dom.set_mounted_root_node(mount, root_idx, this_id); to.load_template(self.template, root_idx, this_id); @@ -789,7 +793,7 @@ impl VNode { ) -> ElementId { // This is just the root node. We already know it's id if let [root_idx] = path { - return dom.mounts[mount.0].root_ids[*root_idx as usize]; + return dom.get_mounted_root_node(mount, *root_idx as usize); } // The node is deeper in the template and we should create a new id for it @@ -835,7 +839,7 @@ impl VNode { impl MountId { fn mount_node(self, node_index: usize, dom: &mut VirtualDom) -> ElementId { let id = dom.next_element(); - dom.mounts[self.0].mounted_dynamic_nodes[node_index] = id.0; + dom.set_mounted_dyn_node(self, node_index, id.0); id } } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index a23f9c9ff..320fb96d9 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,10 +1,6 @@ use crate::{global_context::current_scope_id, properties::SuperFrom, Runtime, ScopeId}; use generational_box::GenerationalBox; -use std::{ - cell::{Cell, RefCell}, - marker::PhantomData, - rc::Rc, -}; +use std::{cell::RefCell, marker::PhantomData, rc::Rc}; /// A wrapper around some generic data that handles the event's state /// @@ -26,19 +22,29 @@ use std::{ pub struct Event { /// The data associated with this event pub data: Rc, - pub(crate) propagates: Rc>, + pub(crate) metadata: Rc>, +} + +#[derive(Clone, Copy)] +pub(crate) struct EventMetadata { + pub(crate) propagates: bool, + pub(crate) prevent_default: bool, } impl Event { - pub(crate) fn new(data: Rc, bubbles: bool) -> Self { + /// Create a new event from the inner data + pub fn new(data: Rc, propagates: bool) -> Self { Self { data, - propagates: Rc::new(Cell::new(bubbles)), + metadata: Rc::new(RefCell::new(EventMetadata { + propagates, + prevent_default: false, + })), } } } -impl Event { +impl Event { /// Map the event data to a new type /// /// # Example @@ -57,7 +63,7 @@ impl Event { pub fn map U>(&self, f: F) -> Event { Event { data: Rc::new(f(&self.data)), - propagates: self.propagates.clone(), + metadata: self.metadata.clone(), } } @@ -78,7 +84,12 @@ impl Event { /// ``` #[deprecated = "use stop_propagation instead"] pub fn cancel_bubble(&self) { - self.propagates.set(false); + self.metadata.borrow_mut().propagates = false; + } + + /// Check if the event propagates up the tree to parent elements + pub fn propagates(&self) -> bool { + self.metadata.borrow().propagates } /// Prevent this event from continuing to bubble up the tree to parent elements. @@ -96,7 +107,7 @@ impl Event { /// }; /// ``` pub fn stop_propagation(&self) { - self.propagates.set(false); + self.metadata.borrow_mut().propagates = false; } /// Get a reference to the inner data from this event @@ -117,12 +128,49 @@ impl Event { pub fn data(&self) -> Rc { self.data.clone() } + + /// Prevent the default action of the event. + /// + /// # Example + /// + /// ```rust + /// # use dioxus::prelude::*; + /// fn App() -> Element { + /// rsx! { + /// a { + /// // You can prevent the default action of the event with `prevent_default` + /// onclick: move |event| { + /// event.prevent_default(); + /// }, + /// href: "https://dioxuslabs.com", + /// "don't go to the link" + /// } + /// } + /// } + /// ``` + /// + /// Note: This must be called synchronously when handling the event. Calling it after the event has been handled will have no effect. + /// + ///
+ /// + /// This method is not available on the LiveView renderer because LiveView handles all events over a websocket which cannot block. + /// + ///
+ #[track_caller] + pub fn prevent_default(&self) { + self.metadata.borrow_mut().prevent_default = true; + } + + /// Check if the default action of the event is enabled. + pub fn default_action_enabled(&self) -> bool { + !self.metadata.borrow().prevent_default + } } impl Clone for Event { fn clone(&self) -> Self { Self { - propagates: self.propagates.clone(), + metadata: self.metadata.clone(), data: self.data.clone(), } } @@ -138,7 +186,8 @@ impl std::ops::Deref for Event { impl std::fmt::Debug for Event { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("UiEvent") - .field("bubble_state", &self.propagates) + .field("bubble_state", &self.propagates()) + .field("prevent_default", &!self.default_action_enabled()) .field("data", &self.data) .finish() } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 710e1fe20..8c4458642 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -300,12 +300,14 @@ impl VNode { let mount = self.mount.get().as_usize()?; match &self.dynamic_nodes[dynamic_node_idx] { - DynamicNode::Text(_) | DynamicNode::Placeholder(_) => dom - .mounts - .get(mount)? - .mounted_dynamic_nodes - .get(dynamic_node_idx) - .map(|id| ElementId(*id)), + DynamicNode::Text(_) | DynamicNode::Placeholder(_) => { + let mounts = dom.runtime.mounts.borrow(); + mounts + .get(mount)? + .mounted_dynamic_nodes + .get(dynamic_node_idx) + .map(|id| ElementId(*id)) + } _ => None, } } @@ -314,7 +316,8 @@ impl VNode { pub fn mounted_root(&self, root_idx: usize, dom: &VirtualDom) -> Option { let mount = self.mount.get().as_usize()?; - dom.mounts.get(mount)?.root_ids.get(root_idx).copied() + let mounts = dom.runtime.mounts.borrow(); + mounts.get(mount)?.root_ids.get(root_idx).copied() } /// Get the mounted id for a dynamic attribute index @@ -325,7 +328,8 @@ impl VNode { ) -> Option { let mount = self.mount.get().as_usize()?; - dom.mounts + let mounts = dom.runtime.mounts.borrow(); + mounts .get(mount)? .mounted_attributes .get(dynamic_attribute_idx) @@ -623,7 +627,8 @@ impl VComponent { ) -> Option { let mount = vnode.mount.get().as_usize()?; - let scope_id = dom.mounts.get(mount)?.mounted_dynamic_nodes[dynamic_node_index]; + let mounts = dom.runtime.mounts.borrow(); + let scope_id = mounts.get(mount)?.mounted_dynamic_nodes[dynamic_node_index]; Some(ScopeId(scope_id)) } @@ -641,7 +646,8 @@ impl VComponent { ) -> Option<&'a ScopeState> { let mount = vnode.mount.get().as_usize()?; - let scope_id = dom.mounts.get(mount)?.mounted_dynamic_nodes[dynamic_node_index]; + let mounts = dom.runtime.mounts.borrow(); + let scope_id = mounts.get(mount)?.mounted_dynamic_nodes[dynamic_node_index]; dom.scopes.get(scope_id) } @@ -791,7 +797,7 @@ impl AttributeValue { AttributeValue::Listener(EventHandler::leak(move |event: Event| { let data = event.data.downcast::().unwrap(); callback(Event { - propagates: event.propagates, + metadata: event.metadata.clone(), data, }); })) diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 610ff398d..c4aba1e4f 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -1,4 +1,6 @@ +use crate::arena::ElementRef; use crate::innerlude::{DirtyTasks, Effect}; +use crate::nodes::VNodeMount; use crate::scope_context::SuspenseLocation; use crate::{ innerlude::{LocalTask, SchedulerMsg}, @@ -6,13 +8,17 @@ use crate::{ scopes::ScopeId, Task, }; +use crate::{AttributeValue, ElementId, Event}; +use slab::Slab; use slotmap::DefaultKey; +use std::any::Any; use std::collections::BTreeSet; use std::fmt; use std::{ cell::{Cell, Ref, RefCell}, rc::Rc, }; +use tracing::instrument; thread_local! { static RUNTIMES: RefCell>> = const { RefCell::new(vec![]) }; @@ -48,10 +54,23 @@ pub struct Runtime { // Tasks that are waiting to be polled pub(crate) dirty_tasks: RefCell>, + + // The element ids that are used in the renderer + // These mark a specific place in a whole rsx block + pub(crate) elements: RefCell>>, + + // Once nodes are mounted, the information about where they are mounted is stored here + // We need to store this information on the virtual dom so that we know what nodes are mounted where when we bubble events + // Each mount is associated with a whole rsx block. [`VirtualDom::elements`] link to a specific node in the block + pub(crate) mounts: RefCell>, } impl Runtime { pub(crate) fn new(sender: futures_channel::mpsc::UnboundedSender) -> Rc { + let mut elements = Slab::default(); + // the root element is always given element ID 0 since it's the container for the entire tree + elements.insert(None); + Rc::new(Self { sender, rendering: Cell::new(true), @@ -63,6 +82,8 @@ impl Runtime { suspended_tasks: Default::default(), pending_effects: Default::default(), dirty_tasks: Default::default(), + elements: RefCell::new(elements), + mounts: Default::default(), }) } @@ -245,6 +266,150 @@ impl Runtime { let scope = &scopes[scope_id.0].as_ref().unwrap(); !matches!(scope.suspense_location(), SuspenseLocation::UnderSuspense(suspense) if suspense.is_suspended()) } + + /// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an element with a listener, not a static node or a text node.** + /// + /// This method will identify the appropriate element. The data must match up with the listener declared. Note that + /// this method does not give any indication as to the success of the listener call. If the listener is not found, + /// nothing will happen. + /// + /// It is up to the listeners themselves to mark nodes as dirty. + /// + /// If you have multiple events, you can call this method multiple times before calling "render_with_deadline" + #[instrument(skip(self, event), level = "trace", name = "Runtime::handle_event")] + pub fn handle_event(self: &Rc, name: &str, event: Event, element: ElementId) { + let _runtime = RuntimeGuard::new(self.clone()); + let elements = self.elements.borrow(); + + if let Some(Some(parent_path)) = elements.get(element.0).copied() { + if event.propagates() { + self.handle_bubbling_event(parent_path, name, event); + } else { + self.handle_non_bubbling_event(parent_path, name, event); + } + } + } + + /* + ------------------------ + The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when + we find the target path. + + With the target path, we try and move up to the parent until there is no parent. + Due to how bubbling works, we call the listeners before walking to the parent. + + If we wanted to do capturing, then we would accumulate all the listeners and call them in reverse order. + ---------------------- + + For a visual demonstration, here we present a tree on the left and whether or not a listener is collected on the + right. + + | <-- yes (is ascendant) + | | | <-- no (is not direct ascendant) + | | <-- yes (is ascendant) + | | | | | <--- target element, break early, don't check other listeners + | | | <-- no, broke early + | <-- no, broke early + */ + #[instrument( + skip(self, uievent), + level = "trace", + name = "VirtualDom::handle_bubbling_event" + )] + fn handle_bubbling_event(&self, parent: ElementRef, name: &str, uievent: Event) { + let mounts = self.mounts.borrow(); + + // If the event bubbles, we traverse through the tree until we find the target element. + // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent. + let mut parent = Some(parent); + while let Some(path) = parent { + let mut listeners = vec![]; + + let Some(mount) = mounts.get(path.mount.0) else { + // If the node is suspended and not mounted, we can just ignore the event + return; + }; + let el_ref = &mount.node; + let node_template = el_ref.template; + let target_path = path.path; + + // Accumulate listeners into the listener list bottom to top + for (idx, this_path) in node_template.attr_paths.iter().enumerate() { + let attrs = &*el_ref.dynamic_attrs[idx]; + + for attr in attrs.iter() { + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + if attr.name.get(2..) == Some(name) && target_path.is_descendant(this_path) { + listeners.push(&attr.value); + + // Break if this is the exact target element. + // This means we won't call two listeners with the same name on the same element. This should be + // documented, or be rejected from the rsx! macro outright + if target_path == this_path { + break; + } + } + } + } + + // Now that we've accumulated all the parent attributes for the target element, call them in reverse order + // We check the bubble state between each call to see if the event has been stopped from bubbling + tracing::event!( + tracing::Level::TRACE, + "Calling {} listeners", + listeners.len() + ); + for listener in listeners.into_iter().rev() { + if let AttributeValue::Listener(listener) = listener { + self.rendering.set(false); + listener.call(uievent.clone()); + self.rendering.set(true); + let metadata = uievent.metadata.borrow(); + + if !metadata.propagates { + return; + } + } + } + + let mount = el_ref.mount.get().as_usize(); + parent = mount.and_then(|id| mounts.get(id).and_then(|el| el.parent)); + } + } + + /// Call an event listener in the simplest way possible without bubbling upwards + #[instrument( + skip(self, uievent), + level = "trace", + name = "VirtualDom::handle_non_bubbling_event" + )] + fn handle_non_bubbling_event(&self, node: ElementRef, name: &str, uievent: Event) { + let mounts = self.mounts.borrow(); + let Some(mount) = mounts.get(node.mount.0) else { + // If the node is suspended and not mounted, we can just ignore the event + return; + }; + let el_ref = &mount.node; + let node_template = el_ref.template; + let target_path = node.path; + + for (idx, this_path) in node_template.attr_paths.iter().enumerate() { + let attrs = &*el_ref.dynamic_attrs[idx]; + + for attr in attrs.iter() { + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + // Only call the listener if this is the exact target element. + if attr.name.get(2..) == Some(name) && target_path == this_path { + if let AttributeValue::Listener(listener) = &attr.value { + self.rendering.set(false); + listener.call(uievent.clone()); + self.rendering.set(true); + break; + } + } + } + } + } } /// A guard for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime. diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 9efc928a6..8d55e6671 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -275,7 +275,7 @@ impl SuspenseBoundaryProps { dom: &mut VirtualDom, to: Option<&mut M>, ) -> usize { - let mut scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); + let mut scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx)); // If the ScopeId is a placeholder, we need to load up a new scope for this vcomponent. If it's already mounted, then we can just use that if scope_id.is_placeholder() { { @@ -297,7 +297,7 @@ impl SuspenseBoundaryProps { } // Store the scope id for the next render - dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope_id.0; + dom.set_mounted_dyn_node(mount, idx, scope_id.0); } dom.runtime.clone().with_scope_on_stack(scope_id, || { let scope_state = &mut dom.scopes[scope_id.0]; @@ -404,11 +404,13 @@ impl SuspenseBoundaryProps { // Get the parent of the suspense boundary to later create children with the right parent let currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone(); let mount = currently_rendered.mount.get(); - let parent = dom - .mounts - .get(mount.0) - .expect("suspense placeholder is not mounted") - .parent; + let parent = { + let mounts = dom.runtime.mounts.borrow(); + mounts + .get(mount.0) + .expect("suspense placeholder is not mounted") + .parent + }; let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); @@ -524,8 +526,7 @@ impl SuspenseBoundaryProps { // Move the children to the background let mount = old_children.mount.get(); - let mount = dom.mounts.get(mount.0).expect("mount should exist"); - let parent = mount.parent; + let parent = dom.get_mounted_parent(mount); suspense_context.in_suspense_placeholder(&dom.runtime(), || { old_children.move_node_to_background( @@ -566,8 +567,7 @@ impl SuspenseBoundaryProps { // Then replace the placeholder with the new children let mount = old_placeholder.mount.get(); - let mount = dom.mounts.get(mount.0).expect("mount should exist"); - let parent = mount.parent; + let parent = dom.get_mounted_parent(mount); old_placeholder.replace( std::slice::from_ref(&*new_children), parent, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index d1a07479d..8d520a25b 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -7,13 +7,10 @@ use crate::properties::RootProps; use crate::root_wrapper::RootScopeWrapper; use crate::{ arena::ElementId, - innerlude::{ - ElementRef, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, VNodeMount, VProps, - WriteMutations, - }, + innerlude::{NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, VProps, WriteMutations}, runtime::{Runtime, RuntimeGuard}, scopes::ScopeId, - AttributeValue, ComponentFunction, Element, Event, Mutations, + ComponentFunction, Element, Mutations, }; use crate::{Task, VComponent}; use futures_util::StreamExt; @@ -97,15 +94,16 @@ use tracing::instrument; /// let edits = vdom.rebuild_to_vec(); /// ``` /// -/// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data. +/// To call listeners inside the VirtualDom, call [`Runtime::handle_event`] with the appropriate event data. /// /// ```rust, no_run /// # use dioxus::prelude::*; /// # use dioxus_core::*; /// # fn app() -> Element { rsx! { div {} } } /// # let mut vdom = VirtualDom::new(app); -/// let event = std::rc::Rc::new(0); -/// vdom.handle_event("onclick", event, ElementId(0), true); +/// # let runtime = vdom.runtime(); +/// let event = Event::new(std::rc::Rc::new(0) as std::rc::Rc, true); +/// runtime.handle_event("onclick", event, ElementId(0)); /// ``` /// /// While no events are ready, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom. @@ -174,7 +172,10 @@ use tracing::instrument; /// loop { /// tokio::select! { /// _ = dom.wait_for_work() => {} -/// evt = real_dom.wait_for_event() => dom.handle_event("onclick", evt, ElementId(0), true), +/// evt = real_dom.wait_for_event() => { +/// let evt = dioxus_core::Event::new(evt, true); +/// dom.runtime().handle_event("onclick", evt, ElementId(0)) +/// }, /// } /// /// dom.render_immediate(&mut real_dom.apply()); @@ -206,15 +207,6 @@ pub struct VirtualDom { pub(crate) dirty_scopes: BTreeSet, - // The element ids that are used in the renderer - // These mark a specific place in a whole rsx block - pub(crate) elements: Slab>, - - // Once nodes are mounted, the information about where they are mounted is stored here - // We need to store this information on the virtual dom so that we know what nodes are mounted where when we bubble events - // Each mount is associated with a whole rsx block. [`VirtualDom::elements`] link to a specific node in the block - pub(crate) mounts: Slab, - pub(crate) runtime: Rc, // The scopes that have been resolved since the last render @@ -328,8 +320,6 @@ impl VirtualDom { runtime: Runtime::new(tx), scopes: Default::default(), dirty_scopes: Default::default(), - elements: Default::default(), - mounts: Default::default(), resolved_scopes: Default::default(), }; @@ -341,9 +331,6 @@ impl VirtualDom { ); dom.new_scope(Box::new(root), "app"); - // the root element is always given element ID 0 since it's the container for the entire tree - dom.elements.insert(None); - dom } @@ -423,34 +410,6 @@ impl VirtualDom { self.queue_task(task, order); } - /// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an element with a listener, not a static node or a text node.** - /// - /// This method will identify the appropriate element. The data must match up with the listener declared. Note that - /// this method does not give any indication as to the success of the listener call. If the listener is not found, - /// nothing will happen. - /// - /// It is up to the listeners themselves to mark nodes as dirty. - /// - /// If you have multiple events, you can call this method multiple times before calling "render_with_deadline" - #[instrument(skip(self), level = "trace", name = "VirtualDom::handle_event")] - pub fn handle_event( - &mut self, - name: &str, - data: Rc, - element: ElementId, - bubbles: bool, - ) { - let _runtime = RuntimeGuard::new(self.runtime.clone()); - - if let Some(Some(parent_path)) = self.elements.get(element.0).copied() { - if bubbles { - self.handle_bubbling_event(parent_path, name, Event::new(data, bubbles)); - } else { - self.handle_non_bubbling_event(parent_path, name, Event::new(data, bubbles)); - } - } - } - /// Wait for the scheduler to have any work. /// /// This method polls the internal future queue, waiting for suspense nodes, tasks, or other work. This completes when @@ -772,121 +731,11 @@ impl VirtualDom { self.runtime.clone() } - /* - ------------------------ - The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when - we find the target path. - - With the target path, we try and move up to the parent until there is no parent. - Due to how bubbling works, we call the listeners before walking to the parent. - - If we wanted to do capturing, then we would accumulate all the listeners and call them in reverse order. - ---------------------- - - For a visual demonstration, here we present a tree on the left and whether or not a listener is collected on the - right. - - | <-- yes (is ascendant) - | | | <-- no (is not direct ascendant) - | | <-- yes (is ascendant) - | | | | | <--- target element, break early, don't check other listeners - | | | <-- no, broke early - | <-- no, broke early - */ - #[instrument( - skip(self, uievent), - level = "trace", - name = "VirtualDom::handle_bubbling_event" - )] - fn handle_bubbling_event(&mut self, parent: ElementRef, name: &str, uievent: Event) { - // If the event bubbles, we traverse through the tree until we find the target element. - // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent. - let mut parent = Some(parent); - while let Some(path) = parent { - let mut listeners = vec![]; - - let Some(mount) = self.mounts.get(path.mount.0) else { - // If the node is suspended and not mounted, we can just ignore the event - return; - }; - let el_ref = &mount.node; - let node_template = el_ref.template; - let target_path = path.path; - - // Accumulate listeners into the listener list bottom to top - for (idx, this_path) in node_template.attr_paths.iter().enumerate() { - let attrs = &*el_ref.dynamic_attrs[idx]; - - for attr in attrs.iter() { - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - if attr.name.get(2..) == Some(name) && target_path.is_descendant(this_path) { - listeners.push(&attr.value); - - // Break if this is the exact target element. - // This means we won't call two listeners with the same name on the same element. This should be - // documented, or be rejected from the rsx! macro outright - if target_path == this_path { - break; - } - } - } - } - - // Now that we've accumulated all the parent attributes for the target element, call them in reverse order - // We check the bubble state between each call to see if the event has been stopped from bubbling - tracing::event!( - tracing::Level::TRACE, - "Calling {} listeners", - listeners.len() - ); - for listener in listeners.into_iter().rev() { - if let AttributeValue::Listener(listener) = listener { - self.runtime.rendering.set(false); - listener.call(uievent.clone()); - self.runtime.rendering.set(true); - - if !uievent.propagates.get() { - return; - } - } - } - - let mount = el_ref.mount.get().as_usize(); - parent = mount.and_then(|id| self.mounts.get(id).and_then(|el| el.parent)); - } - } - - /// Call an event listener in the simplest way possible without bubbling upwards - #[instrument( - skip(self, uievent), - level = "trace", - name = "VirtualDom::handle_non_bubbling_event" - )] - fn handle_non_bubbling_event(&mut self, node: ElementRef, name: &str, uievent: Event) { - let Some(mount) = self.mounts.get(node.mount.0) else { - // If the node is suspended and not mounted, we can just ignore the event - return; - }; - let el_ref = &mount.node; - let node_template = el_ref.template; - let target_path = node.path; - - for (idx, this_path) in node_template.attr_paths.iter().enumerate() { - let attrs = &*el_ref.dynamic_attrs[idx]; - - for attr in attrs.iter() { - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - // Only call the listener if this is the exact target element. - if attr.name.get(2..) == Some(name) && target_path == this_path { - if let AttributeValue::Listener(listener) = &attr.value { - self.runtime.rendering.set(false); - listener.call(uievent.clone()); - self.runtime.rendering.set(true); - break; - } - } - } - } + /// Handle an event with the Virtual Dom. This method is deprecated in favor of [VirtualDom::runtime().handle_event] and will be removed in a future release. + #[deprecated = "Use [VirtualDom::runtime().handle_event] instead"] + pub fn handle_event(&self, name: &str, event: Rc, element: ElementId, bubbling: bool) { + let event = crate::Event::new(event, bubbling); + self.runtime().handle_event(name, event, element); } } diff --git a/packages/core/tests/event_propagation.rs b/packages/core/tests/event_propagation.rs index 9be5a0b2a..a480d4990 100644 --- a/packages/core/tests/event_propagation.rs +++ b/packages/core/tests/event_propagation.rs @@ -1,6 +1,6 @@ use dioxus::prelude::*; use dioxus_core::ElementId; -use std::{rc::Rc, sync::Mutex}; +use std::{any::Any, rc::Rc, sync::Mutex}; static CLICKS: Mutex = Mutex::new(0); @@ -12,12 +12,11 @@ fn events_propagate() { dom.rebuild(&mut dioxus_core::NoOpMutations); // Top-level click is registered - dom.handle_event( - "click", - Rc::new(PlatformEventData::new(Box::::default())), - ElementId(1), + let event = Event::new( + Rc::new(PlatformEventData::new(Box::::default())) as Rc, true, ); + dom.runtime().handle_event("click", event, ElementId(1)); assert_eq!(*CLICKS.lock().unwrap(), 1); // break reference.... @@ -27,12 +26,11 @@ fn events_propagate() { } // Lower click is registered - dom.handle_event( - "click", - Rc::new(PlatformEventData::new(Box::::default())), - ElementId(2), + let event = Event::new( + Rc::new(PlatformEventData::new(Box::::default())) as Rc, true, ); + dom.runtime().handle_event("click", event, ElementId(2)); assert_eq!(*CLICKS.lock().unwrap(), 3); // break reference.... @@ -42,12 +40,11 @@ fn events_propagate() { } // Stop propagation occurs - dom.handle_event( - "click", - Rc::new(PlatformEventData::new(Box::::default())), - ElementId(2), + let event = Event::new( + Rc::new(PlatformEventData::new(Box::::default())) as Rc, true, ); + dom.runtime().handle_event("click", event, ElementId(2)); assert_eq!(*CLICKS.lock().unwrap(), 3); } diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index d5644f7de..b6c4e5431 100644 --- a/packages/core/tests/fuzzing.rs +++ b/packages/core/tests/fuzzing.rs @@ -1,8 +1,8 @@ #![cfg(not(miri))] use dioxus::prelude::*; -use dioxus_core::{AttributeValue, DynamicNode, NoOpMutations, Template, VComponent, VNode, *}; -use std::{cfg, collections::HashSet, default::Default}; +use dioxus_core::{AttributeValue, DynamicNode, NoOpMutations, VComponent, VNode, *}; +use std::{any::Any, cfg, collections::HashSet, default::Default, rc::Rc}; fn random_ns() -> Option<&'static str> { let namespace = rand::random::() % 2; @@ -279,12 +279,11 @@ fn diff() { for _ in 0..100 { for &id in &event_listeners { println!("firing event on {:?}", id); - vdom.handle_event( - "data", - std::rc::Rc::new(String::from("hello world")), - id, + let event = Event::new( + std::rc::Rc::new(String::from("hello world")) as Rc, true, ); + vdom.runtime().handle_event("data", event, id); } { vdom.render_immediate(&mut InsertEventListenerMutationHandler( diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 5030f90f5..2b7ead4dd 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -5,6 +5,7 @@ use dioxus::dioxus_core::{ElementId, Mutation::*}; use dioxus::html::SerializedHtmlEventConverter; use dioxus::prelude::*; +use std::any::Any; use std::rc::Rc; use std::sync::{Arc, Mutex}; @@ -60,12 +61,11 @@ fn events_generate() { let mut dom = VirtualDom::new(app); dom.rebuild(&mut dioxus_core::NoOpMutations); - dom.handle_event( - "click", - Rc::new(PlatformEventData::new(Box::::default())), - ElementId(1), + let event = Event::new( + Rc::new(PlatformEventData::new(Box::::default())) as Rc, true, ); + dom.runtime().handle_event("click", event, ElementId(1)); dom.mark_dirty(ScopeId::APP); let edits = dom.render_immediate_to_vec(); diff --git a/packages/core/tests/miri_full_app.rs b/packages/core/tests/miri_full_app.rs index e21138055..1196b4263 100644 --- a/packages/core/tests/miri_full_app.rs +++ b/packages/core/tests/miri_full_app.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; use dioxus_core::ElementId; use dioxus_elements::SerializedHtmlEventConverter; -use std::rc::Rc; +use std::{any::Any, rc::Rc}; #[test] fn miri_rollover() { @@ -11,12 +11,11 @@ fn miri_rollover() { dom.rebuild(&mut dioxus_core::NoOpMutations); for _ in 0..3 { - dom.handle_event( - "click", - Rc::new(PlatformEventData::new(Box::::default())), - ElementId(2), + let event = Event::new( + Rc::new(PlatformEventData::new(Box::::default())) as Rc, true, ); + dom.runtime().handle_event("click", event, ElementId(2)); dom.process_events(); _ = dom.render_immediate_to_vec(); } diff --git a/packages/core/tests/tracing.rs b/packages/core/tests/tracing.rs index 1fe04033d..e0791be38 100644 --- a/packages/core/tests/tracing.rs +++ b/packages/core/tests/tracing.rs @@ -1,7 +1,7 @@ use dioxus::html::SerializedHtmlEventConverter; use dioxus::prelude::*; use dioxus_core::ElementId; -use std::rc::Rc; +use std::{any::Any, rc::Rc}; use tracing_fluent_assertions::{AssertionRegistry, AssertionsLayer}; use tracing_subscriber::{layer::SubscriberExt, Registry}; @@ -42,12 +42,11 @@ fn basic_tracing() { edited_virtual_dom.assert(); for _ in 0..3 { - dom.handle_event( - "click", - Rc::new(PlatformEventData::new(Box::::default())), - ElementId(2), + let event = Event::new( + Rc::new(PlatformEventData::new(Box::::default())) as Rc, true, ); + dom.runtime().handle_event("click", event, ElementId(2)); dom.process_events(); _ = dom.render_immediate_to_vec(); } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 5153729f1..3e6f8f39f 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -18,7 +18,7 @@ dioxus-html = { workspace = true, features = [ "document", ] } dioxus-signals = { workspace = true, optional = true } -dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] } dioxus-cli-config = { workspace = true, features = ["read-config"] } generational-box = { workspace = true } # hotreload only works on desktop platforms.... mobile is still wip diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 1c3da6c02..9b60f83ba 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -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, 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 diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 1cdc9656b..415f4b73a 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -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, pub(crate) asset_handlers: AssetHandlerRegistry, pub(crate) file_hover: NativeFileHover, #[cfg(target_os = "ios")] - pub(crate) views: Rc>>, + pub(crate) views: Rc>>, } /// A smart pointer to the current window. @@ -86,7 +79,6 @@ impl DesktopService { webview: WebView, window: Window, shared: Rc, - 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. diff --git a/packages/desktop/src/edits.rs b/packages/desktop/src/edits.rs index b3532de07..79f6cb284 100644 --- a/packages/desktop/src/edits.rs +++ b/packages/desktop/src/edits.rs @@ -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>>>, - responder: Rc>>, - // 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>>, - // 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>, +pub(crate) struct WryQueue { + inner: Rc>, } -impl EditQueue { +#[derive(Default)] +pub(crate) struct WryQueueInner { + edit_queue: VecDeque>, + edit_responder: Option, + // 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, + // 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) { - 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(()) diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index 79c39399f..7d722e548 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -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(_) => {} diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index f2abb1394..b2625084b 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -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>, 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() {{ diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 75c2af759..1e762ef02 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -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, + pub wry_queue: WryQueue, + desktop_context: Rc>, +} + +impl WebviewEdits { + fn new(runtime: Rc, 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>, + 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>, + ) -> Result, 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| { - // 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| { + // 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 = 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(); } } diff --git a/packages/html/src/attribute_groups.rs b/packages/html/src/attribute_groups.rs index e46a03d32..17655af67 100644 --- a/packages/html/src/attribute_groups.rs +++ b/packages/html/src/attribute_groups.rs @@ -1,4 +1,5 @@ #![allow(non_upper_case_globals)] +#![allow(deprecated)] use dioxus_core::prelude::IntoAttributeValue; use dioxus_core::HasAttributes; @@ -215,10 +216,8 @@ mod_methods! { map_global_attributes; map_html_global_attributes_to_rsx; - /// Prevent the default action for this element. - /// - /// For more information, see the MDN docs: - /// + #[deprecated(note = "This attribute does nothing. For most renderers, you should prefer calling [`dioxus_core::Event::prevent_default`] on the event instead. For liveview, you can use `\"onclick\": (evt) => evt.prevent_default()` to prevent the default action for this element.")] + /// This attribute has been deprecated in favor of [`dioxus_core::Event::prevent_default`] prevent_default: "dioxus-prevent-default"; @@ -1746,7 +1745,12 @@ mod_methods! { map_svg_attributes; map_html_svg_attributes_to_rsx; - /// Prevent the default action for this element. + /// Prevent the default action for this element. This attribute is only recommended in the LiveView renderer + /// which does not support the prevent default method on events. + /// + /// + /// For most renderers, you should prefer calling [`dioxus_core::Event::prevent_default`] on the event instead. + /// /// /// For more information, see the MDN docs: /// diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index ab6a1189e..1d1db096b 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -[6449103750905854967, 12029349297046688094, 13069001215487072322, 8716623267269178440, 5336385715226370016, 14456089431355876478, 17683787366271106808, 5052021921702764563, 16478152596505612522, 5638004933879392817] \ No newline at end of file +[6449103750905854967, 12029349297046688094, 13069001215487072322, 8716623267269178440, 5336385715226370016, 14456089431355876478, 5618841090288840293, 5052021921702764563, 16478152596505612522, 5638004933879392817] \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 925a7faa6..52876ec73 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1 +1 @@ -function retrieveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){let files=void 0;if(event.dataTransfer&&event.dataTransfer.files&&event.dataTransfer.files.length>0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;kickStylesheets;queuedBytes=[];liveview;constructor(editsPath){super();this.editsPath=editsPath,this.kickStylesheets=!1}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message),event.preventDefault()}}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getScrollHeight(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollHeight}getScrollLeft(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollLeft}getScrollTop(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollTop}getScrollWidth(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollWidth}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k{this.flushQueuedBytes(),this.waitForRequest(headless)})}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{this.rafEdits(headless,bytes)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){let files=void 0;if(event.dataTransfer&&event.dataTransfer.files&&event.dataTransfer.files.length>0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}};var handleVirtualdomEventSync=function(endpoint,contents){const xhr=new XMLHttpRequest;return xhr.open("POST",endpoint,!1),xhr.setRequestHeader("Content-Type","application/json"),xhr.send(contents),JSON.parse(xhr.responseText)},getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;eventsPath;kickStylesheets;queuedBytes=[];liveview;constructor(editsPath,eventsPath){super();this.editsPath=editsPath,this.eventsPath=eventsPath,this.kickStylesheets=!1}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message),event.preventDefault()}}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getScrollHeight(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollHeight}getScrollLeft(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollLeft}getScrollTop(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollTop}getScrollWidth(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollWidth}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k{this.flushQueuedBytes(),this.waitForRequest(headless)})}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{this.rafEdits(headless,bytes)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i Self { + Self { + #[cfg(feature = "serialize")] + prevent_default, + } + } +} diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index c4b6ba461..e2e81e147 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -27,7 +27,7 @@ rustc-hash = { workspace = true } dioxus-core = { workspace = true, features = ["serialize"] } dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true, features = ["serve", "client"] } -dioxus-cli-config = { workspace = true, features = ["read-config"] } +dioxus-cli-config = { workspace = true, features = ["read-config", "read-from-args"] } generational-box = { workspace = true } # axum diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index 7b9eb7904..638e6cd9c 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -45,7 +45,7 @@ impl LiveviewRouter for Router { ) -> Self { let view = crate::LiveViewPool::new(); - let ws_path = format!("{}/ws", route); + let ws_path = format!("{}/ws", route.trim_start_matches('/')); let title = crate::app_title(); let index_page_with_glue = move |glue: &str| { @@ -53,8 +53,8 @@ impl LiveviewRouter for Router { r#" - {title} -
+ {title} +
{glue} "#, diff --git a/packages/liveview/src/config.rs b/packages/liveview/src/config.rs index 4d556eeb8..49194d692 100644 --- a/packages/liveview/src/config.rs +++ b/packages/liveview/src/config.rs @@ -1,4 +1,4 @@ -use dioxus_cli_config::CURRENT_CONFIG; +use dioxus_cli_config::{RuntimeCLIArguments, CURRENT_CONFIG}; use dioxus_core::VirtualDom; use crate::LiveviewRouter; @@ -19,9 +19,15 @@ pub struct Config { impl Default for Config { fn default() -> Self { + let address = RuntimeCLIArguments::from_cli() + .map(|args| args.fullstack_address().address()) + .unwrap_or(std::net::SocketAddr::V4(std::net::SocketAddrV4::new( + std::net::Ipv4Addr::new(127, 0, 0, 1), + 8080, + ))); Self { router: R::create_default_liveview_router(), - address: ([127, 0, 0, 1], 8080).into(), + address, route: "/".to_string(), } } diff --git a/packages/liveview/src/launch.rs b/packages/liveview/src/launch.rs index 21e8bb134..766a7a25f 100644 --- a/packages/liveview/src/launch.rs +++ b/packages/liveview/src/launch.rs @@ -6,7 +6,7 @@ pub type Config = crate::Config; /// Launches the WebView and runs the event loop, with configuration and root props. pub fn launch( root: fn() -> Element, - contexts: Vec Box + Send + Sync>>, + contexts: Vec Box + Send + Sync>>, platform_config: Config, ) -> ! { #[cfg(feature = "multi-thread")] diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 71d543911..ebe0c0357 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -60,7 +60,7 @@ fn handle_edits_code() -> String { if (realId === null) { return; } - const message = window.interpreter.serializeIpcMessage("user_event", { + const message = window.interpreter.sendSerializedEvent({ name: name, element: parseInt(realId), data: contents, diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 9fb1f0d8e..e8f5c5833 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -11,6 +11,7 @@ class IPC { constructor(root) { window.interpreter = new NativeInterpreter(); window.interpreter.initialize(root); + window.interpreter.liveview = true; window.interpreter.ipc = this; const ws = new WebSocket(WS_ADDR); ws.binaryType = "arraybuffer"; @@ -32,13 +33,12 @@ class IPC { ws.onmessage = (message) => { const u8view = new Uint8Array(message.data); const binaryFrame = u8view[0] == 1; - const messageData = message.data.slice(1) + const messageData = message.data.slice(1); // The first byte tells the shim if this is a binary of text frame if (binaryFrame) { // binary frame window.interpreter.run_from_bytes(messageData); - } - else { + } else { // text frame let decoder = new TextDecoder("utf-8"); diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index a180f6770..2f15de86b 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -11,7 +11,7 @@ use dioxus_html::{EventData, HtmlEvent, PlatformEventData}; use dioxus_interpreter_js::MutationState; use futures_util::{pin_mut, SinkExt, StreamExt}; use serde::Serialize; -use std::{rc::Rc, time::Duration}; +use std::{any::Any, rc::Rc, time::Duration}; use tokio_util::task::LocalPoolHandle; #[derive(Clone)] @@ -177,22 +177,23 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li match message { IpcMessage::Event(evt) => { // Intercept the mounted event and insert a custom element type - if let EventData::Mounted = &evt.data { + let event = if let EventData::Mounted = &evt.data { let element = LiveviewElement::new(evt.element, query_engine.clone()); - vdom.handle_event( - &evt.name, - Rc::new(PlatformEventData::new(Box::new(element))), - evt.element, + Event::new( + Rc::new(PlatformEventData::new(Box::new(element))) as Rc, evt.bubbles, - ); + ) } else { - vdom.handle_event( - &evt.name, + Event::new( evt.data.into_any(), - evt.element, evt.bubbles, - ); - } + ) + }; + vdom.runtime().handle_event( + &evt.name, + event, + evt.element, + ); } IpcMessage::Query(result) => { query_engine.send(result); diff --git a/packages/playwright-tests/web.spec.js b/packages/playwright-tests/web.spec.js index 7daf122b8..33a4cfcd3 100644 --- a/packages/playwright-tests/web.spec.js +++ b/packages/playwright-tests/web.spec.js @@ -94,4 +94,16 @@ test("eval", async ({ page }) => { await expect(div).toHaveText("returned eval value"); }); -// Shutdown the li +test("prevent default", async ({ page }) => { + await page.goto("http://localhost:9999"); + + // Expect the page to contain the div with the eval and have no text. + const a = page.locator("a.prevent-default"); + await expect(a).toHaveText("View source"); + + // Click the element to change the text + await a.click(); + + // Check that the element changed. + await expect(a).toHaveText("Psych!"); +}); diff --git a/packages/playwright-tests/web/src/main.rs b/packages/playwright-tests/web/src/main.rs index ae68f15d3..126f729e4 100644 --- a/packages/playwright-tests/web/src/main.rs +++ b/packages/playwright-tests/web/src/main.rs @@ -51,6 +51,23 @@ fn app() -> Element { "Eval" } div { class: "eval-result", "{eval_result}" } + PreventDefault {} + } +} + +#[component] +fn PreventDefault() -> Element { + let mut text = use_signal(|| "View source".to_string()); + rsx! { + a { + class: "prevent-default", + href: "https://github.com/DioxusLabs/dioxus/tree/main/packages/playwright-tests/web", + onclick: move |evt| { + evt.prevent_default(); + text.set("Psych!".to_string()); + }, + "{text}" + } } } diff --git a/packages/router/src/components/history_buttons.rs b/packages/router/src/components/history_buttons.rs index d0e720ea1..5466fdf1c 100644 --- a/packages/router/src/components/history_buttons.rs +++ b/packages/router/src/components/history_buttons.rs @@ -51,7 +51,7 @@ pub struct HistoryButtonProps { /// # vdom.rebuild_in_place(); /// # assert_eq!( /// # dioxus_ssr::render(&vdom), -/// # r#""# +/// # r#""# /// # ); /// ``` pub fn GoBackButton(props: HistoryButtonProps) -> Element { @@ -75,8 +75,10 @@ pub fn GoBackButton(props: HistoryButtonProps) -> Element { rsx! { button { disabled: "{disabled}", - prevent_default: "onclick", - onclick: move |_| router.go_back(), + onclick: move |evt| { + evt.prevent_default(); + router.go_back() + }, {children} } } @@ -122,7 +124,7 @@ pub fn GoBackButton(props: HistoryButtonProps) -> Element { /// # vdom.rebuild_in_place(); /// # assert_eq!( /// # dioxus_ssr::render(&vdom), -/// # r#""# +/// # r#""# /// # ); /// ``` pub fn GoForwardButton(props: HistoryButtonProps) -> Element { @@ -146,8 +148,10 @@ pub fn GoForwardButton(props: HistoryButtonProps) -> Element { rsx! { button { disabled: "{disabled}", - prevent_default: "onclick", - onclick: move |_| router.go_forward(), + onclick: move |evt| { + evt.prevent_default(); + router.go_forward() + }, {children} } } diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 09bd8b7ca..c8054cfcd 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -195,7 +195,7 @@ impl Debug for LinkProps { /// # vdom.rebuild_in_place(); /// # assert_eq!( /// # dioxus_ssr::render(&vdom), -/// # r#"A fully configured link"# +/// # r#"A fully configured link"# /// # ); /// ``` #[doc(alias = "")] @@ -259,12 +259,21 @@ pub fn Link(props: LinkProps) -> Element { let is_external = matches!(parsed_route, NavigationTarget::External(_)); let is_router_nav = !is_external && !new_tab; - let prevent_default = is_router_nav.then_some("onclick").unwrap_or_default(); let rel = rel.or_else(|| is_external.then_some("noopener noreferrer".to_string())); let do_default = onclick.is_none() || !onclick_only; - let action = move |event| { + let action = move |event: MouseEvent| { + // Only handle events without modifiers + if !event.modifiers().is_empty() { + return; + } + // only handle left clicks + if event.trigger_button() != Some(dioxus_elements::input_data::MouseButton::Primary) { + return; + } + event.prevent_default(); + if do_default && is_router_nav { router.push_any(router.resolve_into_routable(to.clone())); } @@ -280,12 +289,23 @@ pub fn Link(props: LinkProps) -> Element { } }; + // In liveview, we need to prevent the default action if the user clicks on the link with modifiers + // in javascript. The prevent_default method is not available in the liveview renderer because + // event handlers are handled over a websocket. + let liveview_prevent_default = { + // If the event is a click with the left mouse button and no modifiers, prevent the default action + // and navigate to the href with client side routing + router.is_liveview().then_some( + "if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { event.preventDefault() }" + ) + }; + rsx! { a { onclick: action, + "onclick": liveview_prevent_default, href, onmounted: onmounted, - prevent_default, class, rel, target: tag_target, diff --git a/packages/router/src/contexts/router.rs b/packages/router/src/contexts/router.rs index e97ee2aee..26341b07e 100644 --- a/packages/router/src/contexts/router.rs +++ b/packages/router/src/contexts/router.rs @@ -165,6 +165,19 @@ impl RouterContext { } } + /// Check if the router is running in a liveview context + /// We do some slightly weird things for liveview because of the network boundary + pub fn is_liveview(&self) -> bool { + #[cfg(feature = "liveview")] + { + self.inner.read().history.is_liveview() + } + #[cfg(not(feature = "liveview"))] + { + false + } + } + pub(crate) fn route_from_str(&self, route: &str) -> Result, String> { self.inner.read().history.parse_route(route) } diff --git a/packages/router/src/history/mod.rs b/packages/router/src/history/mod.rs index 0d0f5029e..1b5d18612 100644 --- a/packages/router/src/history/mod.rs +++ b/packages/router/src/history/mod.rs @@ -317,6 +317,9 @@ pub(crate) trait AnyHistoryProvider { #[allow(unused_variables)] fn updater(&mut self, callback: Arc) {} + + #[cfg(feature = "liveview")] + fn is_liveview(&self) -> bool; } pub(crate) struct AnyHistoryProviderImplWrapper { @@ -339,7 +342,7 @@ impl Default for AnyHistoryProviderImplWrapper { } } -impl AnyHistoryProvider for AnyHistoryProviderImplWrapper +impl AnyHistoryProvider for AnyHistoryProviderImplWrapper where R: Routable, ::Err: std::fmt::Display, @@ -389,4 +392,11 @@ where fn updater(&mut self, callback: Arc) { self.inner.updater(callback) } + + #[cfg(feature = "liveview")] + fn is_liveview(&self) -> bool { + use std::any::TypeId; + + TypeId::of::() == TypeId::of::>() + } } diff --git a/packages/router/tests/via_ssr/link.rs b/packages/router/tests/via_ssr/link.rs index dee5f8d76..0cdfb92eb 100644 --- a/packages/router/tests/via_ssr/link.rs +++ b/packages/router/tests/via_ssr/link.rs @@ -72,11 +72,7 @@ fn href_internal() { } } - let expected = format!( - "

App

Link", - href = r#"href="/test""#, - default = r#"dioxus-prevent-default="onclick""#, - ); + let expected = format!("

App

Link", href = r#"href="/test""#,); assert_eq!(prepare::(), expected); } @@ -107,9 +103,8 @@ fn href_external() { } let expected = format!( - "

App

Link", + "

App

Link", href = r#"href="https://dioxuslabs.com/""#, - default = r#"dioxus-prevent-default="""#, rel = r#"rel="noopener noreferrer""#, ); @@ -143,9 +138,8 @@ fn with_class() { } let expected = format!( - "

App

Link", + "

App

Link", href = r#"href="/test""#, - default = r#"dioxus-prevent-default="onclick""#, class = r#"class="test_class""#, ); @@ -173,9 +167,8 @@ fn with_active_class_active() { } let expected = format!( - "

App

Link", + "

App

Link", href = r#"href="/""#, - default = r#"dioxus-prevent-default="onclick""#, class = r#"class="test_class active_class""#, aria = r#"aria-current="page""#, ); @@ -211,9 +204,8 @@ fn with_active_class_inactive() { } let expected = format!( - "

App

Link", + "

App

Link", href = r#"href="/test""#, - default = r#"dioxus-prevent-default="onclick""#, class = r#"class="test_class""#, ); @@ -247,9 +239,8 @@ fn with_id() { } let expected = format!( - "

App

Link", + "

App

Link", href = r#"href="/test""#, - default = r#"dioxus-prevent-default="onclick""#, id = r#"id="test_id""#, ); @@ -283,9 +274,8 @@ fn with_new_tab() { } let expected = format!( - "

App

Link", + "

App

Link", href = r#"href="/test""#, - default = r#"dioxus-prevent-default="""#, target = r#"target="_blank""# ); @@ -312,9 +302,8 @@ fn with_new_tab_external() { } let expected = format!( - "

App

Link", + "

App

Link", href = r#"href="https://dioxuslabs.com/""#, - default = r#"dioxus-prevent-default="""#, rel = r#"rel="noopener noreferrer""#, target = r#"target="_blank""# ); @@ -349,9 +338,8 @@ fn with_rel() { } let expected = format!( - "

App

Link", + "

App

Link", href = r#"href="/test""#, - default = r#"dioxus-prevent-default="onclick""#, rel = r#"rel="test_rel""#, ); diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 6509f9204..4f41aee7e 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -6,10 +6,11 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation? +use std::{any::Any, rc::Rc}; + +use dioxus_core::Runtime; use dioxus_core::{ElementId, Template}; -use dioxus_html::PlatformEventData; use dioxus_interpreter_js::unified_bindings::Interpreter; -use futures_channel::mpsc; use rustc_hash::FxHashMap; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{Document, Element, Event}; @@ -24,7 +25,7 @@ pub struct WebsysDom { pub(crate) interpreter: Interpreter, #[cfg(feature = "mounted")] - pub(crate) event_channel: mpsc::UnboundedSender, + pub(crate) runtime: Rc, #[cfg(feature = "mounted")] pub(crate) queued_mounted_events: Vec, @@ -49,15 +50,8 @@ pub struct WebsysDom { pub(crate) suspense_hydration_ids: crate::hydration::SuspenseHydrationIds, } -pub struct UiEvent { - pub name: String, - pub bubbles: bool, - pub element: ElementId, - pub data: PlatformEventData, -} - impl WebsysDom { - pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender) -> Self { + pub fn new(cfg: Config, runtime: Rc) -> Self { let (document, root) = match cfg.root { crate::cfg::ConfigRoot::RootName(rootname) => { // eventually, we just want to let the interpreter do all the work of decoding events into our event type @@ -87,46 +81,32 @@ impl WebsysDom { let interpreter = Interpreter::default(); let handler: Closure = Closure::wrap(Box::new({ - let event_channel = event_channel.clone(); - move |event: &web_sys::Event| { - let name = event.type_(); - let element = walk_event_for_id(event); - let bubbles = event.bubbles(); + let runtime = runtime.clone(); + move |web_sys_event: &web_sys::Event| { + let name = web_sys_event.type_(); + let element = walk_event_for_id(web_sys_event); + let bubbles = web_sys_event.bubbles(); let Some((element, target)) = element else { return; }; - let prevent_event; - if let Some(prevent_requests) = target - .get_attribute("dioxus-prevent-default") - .as_deref() - .map(|f| f.split_whitespace()) - { - prevent_event = prevent_requests - .map(|f| f.strip_prefix("on").unwrap_or(f)) - .any(|f| f == name); - } else { - prevent_event = false; - } + let data = virtual_event_from_websys_event(web_sys_event.clone(), target); + let event = dioxus_core::Event::new(Rc::new(data) as Rc, bubbles); + runtime.handle_event(name.as_str(), event.clone(), element); + + // Prevent the default action if the user set prevent default on the event + let prevent_default = !event.default_action_enabled(); // Prevent forms from submitting and redirecting if name == "submit" { // On forms the default behavior is not to submit, if prevent default is set then we submit the form - if !prevent_event { - event.prevent_default(); + if !prevent_default { + web_sys_event.prevent_default(); } - } else if prevent_event { - event.prevent_default(); + } else if prevent_default { + web_sys_event.prevent_default(); } - - let data = virtual_event_from_websys_event(event.clone(), target); - let _ = event_channel.unbounded_send(UiEvent { - name, - bubbles, - element, - data, - }); } })); @@ -145,7 +125,7 @@ impl WebsysDom { interpreter, templates: FxHashMap::default(), #[cfg(feature = "mounted")] - event_channel, + runtime, #[cfg(feature = "mounted")] queued_mounted_events: Default::default(), #[cfg(feature = "hydrate")] diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 8b6cef599..44f7b94c5 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -20,8 +20,6 @@ //! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide //! validation of websys-specific features and not the general use of Dioxus. -use std::{panic, rc::Rc}; - pub use crate::cfg::Config; use crate::hydration::SuspenseMessage; use dioxus_core::VirtualDom; @@ -58,13 +56,11 @@ pub use hydration::*; /// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("foo") }); /// wasm_bindgen_futures::spawn_local(app_fut); /// ``` -pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { +pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! { tracing::info!("Starting up"); - let mut dom = virtual_dom; - #[cfg(feature = "document")] - dom.in_runtime(document::init_document); + virtual_dom.in_runtime(document::init_document); #[cfg(feature = "panic_hook")] if web_config.default_panic_hook { @@ -74,11 +70,11 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { #[cfg(all(feature = "hot_reload", debug_assertions))] let mut hotreload_rx = hot_reload::init(); - let (tx, mut rx) = futures_channel::mpsc::unbounded(); + let runtime = virtual_dom.runtime(); let should_hydrate = web_config.hydrate; - let mut websys_dom = WebsysDom::new(web_config, tx); + let mut websys_dom = WebsysDom::new(web_config, runtime); let mut hydration_receiver: Option> = None; @@ -101,14 +97,14 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { let server_data = HTMLDataCursor::from_serialized(&hydration_data); // If the server serialized an error into the root suspense boundary, throw it into the root scope if let Some(error) = server_data.error() { - dom.in_runtime(|| dioxus_core::ScopeId::APP.throw_error(error)); + virtual_dom.in_runtime(|| dioxus_core::ScopeId::APP.throw_error(error)); } with_server_data(server_data, || { - dom.rebuild(&mut websys_dom); + virtual_dom.rebuild(&mut websys_dom); }); websys_dom.skip_mutations = false; - let rx = websys_dom.rehydrate(&dom).unwrap(); + let rx = websys_dom.rehydrate(&virtual_dom).unwrap(); hydration_receiver = Some(rx); } #[cfg(not(feature = "hydrate"))] @@ -116,7 +112,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { panic!("Hydration is not enabled. Please enable the `hydrate` feature."); } } else { - dom.rebuild(&mut websys_dom); + virtual_dom.rebuild(&mut websys_dom); websys_dom.flush_edits(); } @@ -127,17 +123,15 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { loop { // if virtual dom has nothing, wait for it to have something before requesting idle time // if there is work then this future resolves immediately. - let mut res; #[cfg(all(feature = "hot_reload", debug_assertions))] let template; #[allow(unused)] let mut hydration_work: Option = None; { - let work = dom.wait_for_work().fuse(); + let work = virtual_dom.wait_for_work().fuse(); pin_mut!(work); - let mut rx_next = rx.select_next_some(); let mut hydration_receiver_iter = futures_util::stream::iter(&mut hydration_receiver) .fuse() .flatten(); @@ -149,19 +143,12 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { let mut hot_reload_next = hotreload_rx.select_next_some(); select! { _ = work => { - res = None; template = None; }, new_template = hot_reload_next => { - res = None; template = Some(new_template); }, - evt = rx_next => { - res = Some(evt); - template = None; - } hydration_data = rx_hydration => { - res = None; template = None; #[cfg(feature = "hydrate")] { @@ -175,10 +162,8 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { #[allow(unused)] { select! { - _ = work => res = None, - evt = rx_next => res = Some(evt), + _ = work => {}, hyd = rx_hydration => { - res = None; #[cfg(feature = "hydrate")] { hydration_work = Some(hyd); @@ -191,7 +176,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { #[cfg(all(feature = "hot_reload", debug_assertions))] if let Some(hr_msg) = template { // Replace all templates - dioxus_hot_reload::apply_changes(&mut dom, &hr_msg); + dioxus_hot_reload::apply_changes(&mut virtual_dom, &hr_msg); if !hr_msg.assets.is_empty() { crate::hot_reload::invalidate_browser_asset_cache(); @@ -200,19 +185,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { #[cfg(feature = "hydrate")] if let Some(hydration_data) = hydration_work { - websys_dom.rehydrate_streaming(hydration_data, &mut dom); - } - - // Dequeue all of the events from the channel in send order - // todo: we should re-order these if possible - while let Some(evt) = res { - dom.handle_event( - evt.name.as_str(), - Rc::new(evt.data), - evt.element, - evt.bubbles, - ); - res = rx.try_next().transpose().unwrap().ok(); + websys_dom.rehydrate_streaming(hydration_data, &mut virtual_dom); } // Todo: This is currently disabled because it has a negative impact on response times for events but it could be re-enabled for tasks @@ -227,7 +200,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { // let deadline = work_loop.wait_for_idle_time().await; // run the virtualdom work phase until the frame deadline is reached - dom.render_immediate(&mut websys_dom); + virtual_dom.render_immediate(&mut websys_dom); // wait for the animation frame to fire so we can apply our changes // work_loop.wait_for_raf().await; diff --git a/packages/web/src/mutations.rs b/packages/web/src/mutations.rs index 4fb6b91ab..592c2e5f4 100644 --- a/packages/web/src/mutations.rs +++ b/packages/web/src/mutations.rs @@ -63,12 +63,14 @@ impl WebsysDom { for id in self.queued_mounted_events.drain(..) { let node = self.interpreter.base().get_node(id.0 as u32); if let Some(element) = node.dyn_ref::() { - let _ = self.event_channel.unbounded_send(crate::dom::UiEvent { - name: "mounted".to_string(), - bubbles: false, - element: id, - data: dioxus_html::PlatformEventData::new(Box::new(element.clone())), - }); + let event = dioxus_core::Event::new( + std::rc::Rc::new(dioxus_html::PlatformEventData::new(Box::new( + element.clone(), + ))) as std::rc::Rc, + false, + ); + let name = "mounted"; + self.runtime.handle_event(name, event, id) } } }