Synchronous prevent default (#2792)

* add prevent default methods to the event

* sync prevent default almost working

* sync prevent default working

* Move event handling into the runtime

* update core tests

* restore desktop file dialog

* implement prevent default on web

* add a hint about the new prevent default method

* fix web prevent default

* Fix CTRL+click on links

* fix values memorize in place test

* Fix a few more tests

* Add a playwright test for sync prevent default

* Fix core doc tests

* create a deprecated VirtualDom::handle_event

* fix macos imports in desktop

* Fix onmounted event

* Fix liveview support

* switch to RefCell for metadata

* Remove println

* remove prevent default attribute

* remove web specific link behavior

* Fix liveview links

* more liveview fixes for link

* Fix merge conflicts

* Fix clippy

* use the new prevent default in the file upload example
This commit is contained in:
Evan Almloff 2024-08-13 20:57:54 +02:00 committed by GitHub
parent 8b62b71e2d
commit cab573eefd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 941 additions and 730 deletions

View file

@ -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"

View file

@ -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;

View file

@ -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"
}
}

View file

@ -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 {

View file

@ -177,15 +177,15 @@ fn TodoEntry(mut todos: Signal<HashMap<u32, TodoItem>>, 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"
}
}
}
}
}

View file

@ -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),

View file

@ -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();

View file

@ -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::<SerializedMouseData>::default())),
ElementId(1),
let event = Event::new(
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())) as Rc<dyn Any>,
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::<SerializedMouseData>::default())),
ElementId(1),
let event = Event::new(
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())) as Rc<dyn Any>,
true,
);
dom.runtime().handle_event("click", event, ElementId(1));
dom.render_immediate(&mut dioxus_core::NoOpMutations);
}
dom.render_immediate(&mut dioxus_core::NoOpMutations);

View file

@ -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())

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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<ElementRef> {
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(

View file

@ -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
}
}

View file

@ -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<T: 'static + ?Sized> {
/// The data associated with this event
pub data: Rc<T>,
pub(crate) propagates: Rc<Cell<bool>>,
pub(crate) metadata: Rc<RefCell<EventMetadata>>,
}
#[derive(Clone, Copy)]
pub(crate) struct EventMetadata {
pub(crate) propagates: bool,
pub(crate) prevent_default: bool,
}
impl<T: ?Sized + 'static> Event<T> {
pub(crate) fn new(data: Rc<T>, bubbles: bool) -> Self {
/// Create a new event from the inner data
pub fn new(data: Rc<T>, propagates: bool) -> Self {
Self {
data,
propagates: Rc::new(Cell::new(bubbles)),
metadata: Rc::new(RefCell::new(EventMetadata {
propagates,
prevent_default: false,
})),
}
}
}
impl<T> Event<T> {
impl<T: ?Sized> Event<T> {
/// Map the event data to a new type
///
/// # Example
@ -57,7 +63,7 @@ impl<T> Event<T> {
pub fn map<U: 'static, F: FnOnce(&T) -> U>(&self, f: F) -> Event<U> {
Event {
data: Rc::new(f(&self.data)),
propagates: self.propagates.clone(),
metadata: self.metadata.clone(),
}
}
@ -78,7 +84,12 @@ impl<T> Event<T> {
/// ```
#[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<T> Event<T> {
/// };
/// ```
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<T> Event<T> {
pub fn data(&self) -> Rc<T> {
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.
///
/// <div class="warning">
///
/// This method is not available on the LiveView renderer because LiveView handles all events over a websocket which cannot block.
///
/// </div>
#[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<T: ?Sized> Clone for Event<T> {
fn clone(&self) -> Self {
Self {
propagates: self.propagates.clone(),
metadata: self.metadata.clone(),
data: self.data.clone(),
}
}
@ -138,7 +186,8 @@ impl<T> std::ops::Deref for Event<T> {
impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
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()
}

View file

@ -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<ElementId> {
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<ElementId> {
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<ScopeId> {
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<dyn Any>| {
let data = event.data.downcast::<T>().unwrap();
callback(Event {
propagates: event.propagates,
metadata: event.metadata.clone(),
data,
});
}))

View file

@ -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<Vec<Rc<Runtime>>> = const { RefCell::new(vec![]) };
@ -48,10 +54,23 @@ pub struct Runtime {
// Tasks that are waiting to be polled
pub(crate) dirty_tasks: RefCell<BTreeSet<DirtyTasks>>,
// The element ids that are used in the renderer
// These mark a specific place in a whole rsx block
pub(crate) elements: RefCell<Slab<Option<ElementRef>>>,
// 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<Slab<VNodeMount>>,
}
impl Runtime {
pub(crate) fn new(sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>) -> Rc<Self> {
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<Self>, name: &str, event: Event<dyn Any>, 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<dyn Any>) {
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<dyn Any>) {
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.

View file

@ -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,

View file

@ -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<dyn std::any::Any>, 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<ScopeOrder>,
// The element ids that are used in the renderer
// These mark a specific place in a whole rsx block
pub(crate) elements: Slab<Option<ElementRef>>,
// 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<VNodeMount>,
pub(crate) runtime: Rc<Runtime>,
// 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<dyn Any>,
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<dyn Any>) {
// 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<dyn Any>) {
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<dyn Any>, element: ElementId, bubbling: bool) {
let event = crate::Event::new(event, bubbling);
self.runtime().handle_event(name, event, element);
}
}

View file

@ -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<usize> = 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::<SerializedMouseData>::default())),
ElementId(1),
let event = Event::new(
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())) as Rc<dyn Any>,
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::<SerializedMouseData>::default())),
ElementId(2),
let event = Event::new(
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())) as Rc<dyn Any>,
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::<SerializedMouseData>::default())),
ElementId(2),
let event = Event::new(
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())) as Rc<dyn Any>,
true,
);
dom.runtime().handle_event("click", event, ElementId(2));
assert_eq!(*CLICKS.lock().unwrap(), 3);
}

View file

@ -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::<u8>() % 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<dyn Any>,
true,
);
vdom.runtime().handle_event("data", event, id);
}
{
vdom.render_immediate(&mut InsertEventListenerMutationHandler(

View file

@ -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::<SerializedMouseData>::default())),
ElementId(1),
let event = Event::new(
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())) as Rc<dyn Any>,
true,
);
dom.runtime().handle_event("click", event, ElementId(1));
dom.mark_dirty(ScopeId::APP);
let edits = dom.render_immediate_to_vec();

View file

@ -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::<SerializedMouseData>::default())),
ElementId(2),
let event = Event::new(
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())) as Rc<dyn Any>,
true,
);
dom.runtime().handle_event("click", event, ElementId(2));
dom.process_events();
_ = dom.render_immediate_to_vec();
}

View file

@ -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::<SerializedMouseData>::default())),
ElementId(2),
let event = Event::new(
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())) as Rc<dyn Any>,
true,
);
dom.runtime().handle_event("click", event, ElementId(2));
dom.process_events();
_ = dom.render_immediate_to_vec();
}

View file

@ -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

View file

@ -1,17 +1,16 @@
use crate::{
config::{Config, WindowCloseBehaviour},
element::DesktopElement,
event_handlers::WindowEventHandlers,
file_upload::{DesktopFileDragEvent, DesktopFileUploadForm, FileDialogRequest},
file_upload::{DesktopFileUploadForm, FileDialogRequest},
ipc::{IpcMessage, UserWindowEvent},
query::QueryResult,
shortcut::ShortcutRegistry,
webview::WebviewInstance,
};
use dioxus_core::ElementId;
use dioxus_core::VirtualDom;
use dioxus_html::{native_bind::NativeFileEngine, HasFileData, HtmlEvent, PlatformEventData};
use dioxus_core::{ElementId, VirtualDom};
use dioxus_html::{native_bind::NativeFileEngine, PlatformEventData};
use std::{
any::Any,
cell::{Cell, RefCell},
collections::HashMap,
rc::Rc,
@ -244,9 +243,9 @@ impl App {
let view = self.webviews.get_mut(&id).unwrap();
view.dom
.rebuild(&mut *view.desktop_context.mutation_state.borrow_mut());
.rebuild(&mut *view.edits.wry_queue.mutation_state_mut());
view.desktop_context.send_edits();
view.edits.wry_queue.send_edits();
view.desktop_context
.window
@ -277,55 +276,6 @@ impl App {
view.desktop_context.query.send(result);
}
pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) {
let parsed_params = serde_json::from_value(msg.params())
.map_err(|err| tracing::error!("Error parsing user_event: {:?}", err));
let Ok(evt) = parsed_params else { return };
let HtmlEvent {
element,
name,
bubbles,
data,
} = evt;
let view = self.webviews.get_mut(&id).unwrap();
let query = view.desktop_context.query.clone();
let recent_file = view.desktop_context.file_hover.clone();
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = match data {
dioxus_html::EventData::Mounted => {
let element = DesktopElement::new(element, view.desktop_context.clone(), query);
Rc::new(PlatformEventData::new(Box::new(element)))
}
dioxus_html::EventData::Drag(ref drag) => {
// we want to override this with a native file engine, provided by the most recent drag event
if drag.files().is_some() {
let file_event = recent_file.current().unwrap();
let paths = match file_event {
wry::DragDropEvent::Enter { paths, .. } => paths,
wry::DragDropEvent::Drop { paths, .. } => paths,
_ => vec![],
};
Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
mouse: drag.mouse.clone(),
files: Arc::new(NativeFileEngine::new(paths)),
})))
} else {
data.into_any()
}
}
_ => data.into_any(),
};
view.dom.handle_event(&name, as_any, element, bubbles);
view.dom
.render_immediate(&mut *view.desktop_context.mutation_state.borrow_mut());
view.desktop_context.send_edits();
}
#[cfg(all(
feature = "hot-reload",
debug_assertions,
@ -378,17 +328,13 @@ impl App {
let view = self.webviews.get_mut(&window).unwrap();
let event = dioxus_core::Event::new(data as Rc<dyn Any>, event_bubbles);
if event_name == "change&input" {
view.dom
.handle_event("input", data.clone(), id, event_bubbles);
view.dom.handle_event("change", data, id, event_bubbles);
view.dom.runtime().handle_event("input", event.clone(), id);
view.dom.runtime().handle_event("change", event, id);
} else {
view.dom.handle_event(event_name, data, id, event_bubbles);
view.dom.runtime().handle_event(event_name, event, id);
}
view.dom
.render_immediate(&mut *view.desktop_context.mutation_state.borrow_mut());
view.desktop_context.send_edits();
}
/// Poll the virtualdom until it's pending

View file

@ -1,7 +1,6 @@
use crate::{
app::SharedContext,
assets::AssetHandlerRegistry,
edits::EditQueue,
file_upload::NativeFileHover,
ipc::UserWindowEvent,
query::QueryEngine,
@ -13,11 +12,7 @@ use dioxus_core::{
prelude::{current_scope_id, ScopeId},
VirtualDom,
};
use dioxus_interpreter_js::MutationState;
use std::{
cell::RefCell,
rc::{Rc, Weak},
};
use std::rc::{Rc, Weak};
use tao::{
event::Event,
event_loop::EventLoopWindowTarget,
@ -63,13 +58,11 @@ pub struct DesktopService {
/// The receiver for queries about the current window
pub(super) query: QueryEngine,
pub(crate) edit_queue: EditQueue,
pub(crate) mutation_state: RefCell<MutationState>,
pub(crate) asset_handlers: AssetHandlerRegistry,
pub(crate) file_hover: NativeFileHover,
#[cfg(target_os = "ios")]
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
pub(crate) views: Rc<std::cell::RefCell<Vec<*mut objc::runtime::Object>>>,
}
/// A smart pointer to the current window.
@ -86,7 +79,6 @@ impl DesktopService {
webview: WebView,
window: Window,
shared: Rc<SharedContext>,
edit_queue: EditQueue,
asset_handlers: AssetHandlerRegistry,
file_hover: NativeFileHover,
) -> Self {
@ -94,23 +86,14 @@ impl DesktopService {
window,
webview,
shared,
edit_queue,
asset_handlers,
file_hover,
mutation_state: Default::default(),
query: Default::default(),
#[cfg(target_os = "ios")]
views: Default::default(),
}
}
/// Send a list of mutations to the webview
pub(crate) fn send_edits(&self) {
let mut mutations = self.mutation_state.borrow_mut();
let serialized_edits = mutations.export_memory();
self.edit_queue.add_edits(serialized_edits);
}
/// Create a new window using the props and window builder
///
/// Returns the webview handle for the new window.

View file

@ -1,64 +1,68 @@
use std::cell::Cell;
use std::{cell::RefCell, collections::VecDeque, rc::Rc, task::Waker};
use dioxus_interpreter_js::MutationState;
/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events.
/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made.
#[derive(Default, Clone)]
pub(crate) struct EditQueue {
queue: Rc<RefCell<VecDeque<Vec<u8>>>>,
responder: Rc<RefCell<Option<wry::RequestAsyncResponder>>>,
// Stores any futures waiting for edits to be applied to the webview
// NOTE: We don't use a Notify here because we need polling the notify to be cancel safe
waiting_for_edits_flushed: Rc<RefCell<Vec<Waker>>>,
// If this webview is currently waiting for an edit to be flushed. We don't run the virtual dom while this is true to avoid running effects before the dom has been updated
edits_in_progress: Rc<Cell<bool>>,
pub(crate) struct WryQueue {
inner: Rc<RefCell<WryQueueInner>>,
}
impl EditQueue {
#[derive(Default)]
pub(crate) struct WryQueueInner {
edit_queue: VecDeque<Vec<u8>>,
edit_responder: Option<wry::RequestAsyncResponder>,
// Stores any futures waiting for edits to be applied to the webview
// NOTE: We don't use a Notify here because we need polling the notify to be cancel safe
waiting_for_edits_flushed: Vec<Waker>,
// If this webview is currently waiting for an edit to be flushed. We don't run the virtual dom while this is true to avoid running effects before the dom has been updated
edits_in_progress: bool,
mutation_state: MutationState,
}
impl WryQueue {
pub fn handle_request(&self, responder: wry::RequestAsyncResponder) {
let mut queue = self.queue.borrow_mut();
if let Some(bytes) = queue.pop_back() {
let mut myself = self.inner.borrow_mut();
if let Some(bytes) = myself.edit_queue.pop_back() {
responder.respond(wry::http::Response::new(bytes));
} else {
// There are now no edits that need to be applied to the webview
self.edits_finished();
*self.responder.borrow_mut() = Some(responder);
for waker in myself.waiting_for_edits_flushed.drain(..) {
waker.wake();
}
myself.edits_in_progress = false;
myself.edit_responder = Some(responder);
}
}
pub fn add_edits(&self, edits: Vec<u8>) {
let mut responder = self.responder.borrow_mut();
pub fn mutation_state_mut(&self) -> std::cell::RefMut<'_, MutationState> {
std::cell::RefMut::map(self.inner.borrow_mut(), |myself| &mut myself.mutation_state)
}
/// Send a list of mutations to the webview
pub(crate) fn send_edits(&self) {
let mut myself = self.inner.borrow_mut();
let serialized_edits = myself.mutation_state.export_memory();
// There are pending edits that need to be applied to the webview before we run futures
self.start_edits();
if let Some(responder) = responder.take() {
responder.respond(wry::http::Response::new(edits));
myself.edits_in_progress = true;
if let Some(responder) = myself.edit_responder.take() {
responder.respond(wry::http::Response::new(serialized_edits));
} else {
self.queue.borrow_mut().push_front(edits);
myself.edit_queue.push_front(serialized_edits);
}
}
fn start_edits(&self) {
self.edits_in_progress.set(true);
}
fn edits_finished(&self) {
for waker in self.waiting_for_edits_flushed.borrow_mut().drain(..) {
waker.wake();
}
self.edits_in_progress.set(false);
}
fn edits_in_progress(&self) -> bool {
self.edits_in_progress.get()
self.inner.borrow().edits_in_progress
}
/// Wait until all pending edits have been rendered in the webview
pub fn poll_edits_flushed(&self, cx: &mut std::task::Context<'_>) -> std::task::Poll<()> {
if self.edits_in_progress() {
let mut myself = self.inner.borrow_mut();
let waker = cx.waker();
self.waiting_for_edits_flushed
.borrow_mut()
.push(waker.clone());
myself.waiting_for_edits_flushed.push(waker.clone());
std::task::Poll::Pending
} else {
std::task::Poll::Ready(())

View file

@ -52,7 +52,7 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf
UserWindowEvent::Ipc { id, msg } => match msg.method() {
IpcMethod::Initialize => app.handle_initialize_msg(id),
IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id),
IpcMethod::UserEvent => app.handle_user_event_msg(msg, id),
IpcMethod::UserEvent => {}
IpcMethod::Query => app.handle_query_msg(msg, id),
IpcMethod::BrowserOpen => app.handle_browser_open(msg),
IpcMethod::Other(_) => {}

View file

@ -1,4 +1,4 @@
use crate::{assets::*, edits::EditQueue};
use crate::{assets::*, webview::WebviewEdits};
use dioxus_html::document::NATIVE_EVAL_JS;
use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
use dioxus_interpreter_js::NATIVE_JS;
@ -14,10 +14,16 @@ use wry::{
};
#[cfg(any(target_os = "android", target_os = "windows"))]
const EDITS_PATH: &str = "http://dioxus.index.html/edits";
const EDITS_PATH: &str = "http://dioxus.index.html/__edits";
#[cfg(not(any(target_os = "android", target_os = "windows")))]
const EDITS_PATH: &str = "dioxus://index.html/edits";
const EDITS_PATH: &str = "dioxus://index.html/__edits";
#[cfg(any(target_os = "android", target_os = "windows"))]
const EVENTS_PATH: &str = "http://dioxus.index.html/__events";
#[cfg(not(any(target_os = "android", target_os = "windows")))]
const EVENTS_PATH: &str = "dioxus://index.html/__events";
static DEFAULT_INDEX: &str = include_str!("./index.html");
@ -149,12 +155,18 @@ fn resolve_resource(path: &Path) -> PathBuf {
pub(super) fn desktop_handler(
request: Request<Vec<u8>>,
asset_handlers: AssetHandlerRegistry,
edit_queue: &EditQueue,
responder: RequestAsyncResponder,
edit_state: &WebviewEdits,
) {
// If the request is asking for edits (ie binary protocol streaming, do that)
if request.uri().path().trim_matches('/') == "edits" {
return edit_queue.handle_request(responder);
// If the request is asking for edits (ie binary protocol streaming), do that
let trimmed_uri = request.uri().path().trim_matches('/');
if trimmed_uri == "__edits" {
return edit_state.wry_queue.handle_request(responder);
}
// If the request is asking for an event response, do that
if trimmed_uri == "__events" {
return edit_state.handle_event(request, responder);
}
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
@ -222,7 +234,7 @@ fn module_loader(root_id: &str, headless: bool) -> String {
{NATIVE_JS}
// The native interpreter extends the sledgehammer interpreter with a few extra methods that we use for IPC
window.interpreter = new NativeInterpreter("{EDITS_PATH}");
window.interpreter = new NativeInterpreter("{EDITS_PATH}", "{EVENTS_PATH}");
// Wait for the page to load before sending the initialize message
window.onload = function() {{

View file

@ -1,17 +1,129 @@
use crate::element::DesktopElement;
use crate::file_upload::DesktopFileDragEvent;
use crate::menubar::DioxusMenu;
use crate::{
app::SharedContext, assets::AssetHandlerRegistry, document::DesktopDocument, edits::EditQueue,
app::SharedContext, assets::AssetHandlerRegistry, document::DesktopDocument, edits::WryQueue,
file_upload::NativeFileHover, ipc::UserWindowEvent, protocol, waker::tao_waker, Config,
DesktopContext, DesktopService,
};
use dioxus_core::{ScopeId, VirtualDom};
use dioxus_core::{Runtime, ScopeId, VirtualDom};
use dioxus_hooks::to_owned;
use dioxus_html::document::Document;
use dioxus_html::native_bind::NativeFileEngine;
use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData};
use dioxus_interpreter_js::SynchronousEventResponse;
use futures_util::{pin_mut, FutureExt};
use std::cell::OnceCell;
use std::sync::Arc;
use std::{rc::Rc, task::Waker};
use wry::{RequestAsyncResponder, WebContext, WebViewBuilder};
#[derive(Clone)]
pub(crate) struct WebviewEdits {
runtime: Rc<Runtime>,
pub wry_queue: WryQueue,
desktop_context: Rc<OnceCell<DesktopContext>>,
}
impl WebviewEdits {
fn new(runtime: Rc<Runtime>, wry_queue: WryQueue) -> Self {
Self {
runtime,
wry_queue,
desktop_context: Default::default(),
}
}
fn set_desktop_context(&self, context: DesktopContext) {
_ = self.desktop_context.set(context);
}
pub fn handle_event(
&self,
request: wry::http::Request<Vec<u8>>,
responder: wry::RequestAsyncResponder,
) {
let body = self.try_handle_event(request).unwrap_or_default();
responder.respond(wry::http::Response::new(body))
}
pub fn try_handle_event(
&self,
request: wry::http::Request<Vec<u8>>,
) -> Result<Vec<u8>, serde_json::Error> {
let response = match serde_json::from_slice(request.body()) {
Ok(event) => self.handle_html_event(event),
Err(err) => {
tracing::error!("Error parsing user_event: {:?}", err);
SynchronousEventResponse::new(false)
}
};
let body = match serde_json::to_vec(&response) {
Ok(body) => body,
Err(err) => {
tracing::error!("failed to serialize SynchronousEventResponse: {err:?}");
return Err(err);
}
};
Ok(body)
}
pub fn handle_html_event(&self, event: HtmlEvent) -> SynchronousEventResponse {
let HtmlEvent {
element,
name,
bubbles,
data,
} = event;
let Some(desktop_context) = self.desktop_context.get() else {
tracing::error!(
"Tried to handle event before setting the desktop context on the event handler"
);
return Default::default();
};
let query = desktop_context.query.clone();
let recent_file = desktop_context.file_hover.clone();
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = match data {
dioxus_html::EventData::Mounted => {
let element = DesktopElement::new(element, desktop_context.clone(), query);
Rc::new(PlatformEventData::new(Box::new(element)))
}
dioxus_html::EventData::Drag(ref drag) => {
// we want to override this with a native file engine, provided by the most recent drag event
if drag.files().is_some() {
let file_event = recent_file.current().unwrap();
let paths = match file_event {
wry::DragDropEvent::Enter { paths, .. } => paths,
wry::DragDropEvent::Drop { paths, .. } => paths,
_ => vec![],
};
Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
mouse: drag.mouse.clone(),
files: Arc::new(NativeFileEngine::new(paths)),
})))
} else {
data.into_any()
}
}
_ => data.into_any(),
};
let event = dioxus_core::Event::new(as_any, bubbles);
self.runtime.handle_event(&name, event.clone(), element);
// Get the response from the event
SynchronousEventResponse::new(!event.default_action_enabled())
}
}
pub(crate) struct WebviewInstance {
pub dom: VirtualDom,
pub edits: WebviewEdits,
pub desktop_context: DesktopContext,
pub waker: Waker,
@ -69,56 +181,57 @@ impl WebviewInstance {
}
let mut web_context = WebContext::new(cfg.data_dir.clone());
let edit_queue = EditQueue::default();
let file_hover = NativeFileHover::default();
let edit_queue = WryQueue::default();
let asset_handlers = AssetHandlerRegistry::new(dom.runtime());
let edits = WebviewEdits::new(dom.runtime(), edit_queue.clone());
let file_hover = NativeFileHover::default();
let headless = !cfg.window.window.visible;
// Rust :(
let window_id = window.id();
let custom_head = cfg.custom_head.clone();
let index_file = cfg.custom_index.clone();
let root_name = cfg.root_name.clone();
let asset_handlers_ = asset_handlers.clone();
let edit_queue_ = edit_queue.clone();
let proxy_ = shared.proxy.clone();
let file_hover_ = file_hover.clone();
let request_handler = {
to_owned![
cfg.custom_head,
cfg.custom_index,
cfg.root_name,
asset_handlers,
edits
];
move |request, responder: RequestAsyncResponder| {
// Try to serve the index file first
if let Some(index_bytes) = protocol::index_request(
&request,
custom_head.clone(),
custom_index.clone(),
&root_name,
headless,
) {
return responder.respond(index_bytes);
}
let request_handler = move |request, responder: RequestAsyncResponder| {
// Try to serve the index file first
let index_bytes = protocol::index_request(
&request,
custom_head.clone(),
index_file.clone(),
&root_name,
headless,
);
// Otherwise, try to serve an asset, either from the user or the filesystem
match index_bytes {
Some(body) => responder.respond(body),
None => protocol::desktop_handler(
request,
asset_handlers_.clone(),
&edit_queue_,
responder,
),
// Otherwise, try to serve an asset, either from the user or the filesystem
protocol::desktop_handler(request, asset_handlers.clone(), responder, &edits);
}
};
let ipc_handler = move |payload: wry::http::Request<String>| {
// defer the event to the main thread
let body = payload.into_body();
if let Ok(msg) = serde_json::from_str(&body) {
_ = proxy_.send_event(UserWindowEvent::Ipc { id: window_id, msg });
let ipc_handler = {
let window_id = window.id();
to_owned![shared.proxy];
move |payload: wry::http::Request<String>| {
// defer the event to the main thread
let body = payload.into_body();
if let Ok(msg) = serde_json::from_str(&body) {
_ = proxy.send_event(UserWindowEvent::Ipc { id: window_id, msg });
}
}
};
let file_drop_handler = move |evt| {
// Update the most recent file drop event - when the event comes in from the webview we can use the
// most recent event to build a new event with the files in it.
file_hover_.set(evt);
false
let file_drop_handler = {
to_owned![file_hover];
move |evt| {
// Update the most recent file drop event - when the event comes in from the webview we can use the
// most recent event to build a new event with the files in it.
file_hover.set(evt);
false
}
};
#[cfg(any(
@ -216,22 +329,23 @@ impl WebviewInstance {
webview,
window,
shared.clone(),
edit_queue,
asset_handlers,
file_hover,
));
// Provide the desktop context to the virtual dom and edit handler
edits.set_desktop_context(desktop_context.clone());
let provider: Rc<dyn Document> = Rc::new(DesktopDocument::new(desktop_context.clone()));
dom.in_runtime(|| {
ScopeId::ROOT.provide_context(desktop_context.clone());
ScopeId::ROOT.provide_context(provider);
});
WebviewInstance {
dom,
edits,
waker: tao_waker(shared.proxy.clone(), desktop_context.window.id()),
desktop_context,
dom,
_menu: menu,
_web_context: web_context,
}
@ -245,7 +359,7 @@ impl WebviewInstance {
// It will return Pending when it needs to be polled again - nothing is ready
loop {
// If we're waiting for a render, wait for it to finish before we continue
let edits_flushed_poll = self.desktop_context.edit_queue.poll_edits_flushed(&mut cx);
let edits_flushed_poll = self.edits.wry_queue.poll_edits_flushed(&mut cx);
if edits_flushed_poll.is_pending() {
return;
}
@ -261,8 +375,8 @@ impl WebviewInstance {
}
self.dom
.render_immediate(&mut *self.desktop_context.mutation_state.borrow_mut());
self.desktop_context.send_edits();
.render_immediate(&mut *self.edits.wry_queue.mutation_state_mut());
self.edits.wry_queue.send_edits();
}
}

View file

@ -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:
/// <https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault>
#[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:
/// <https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault>

View file

@ -1 +1 @@
[6449103750905854967, 12029349297046688094, 13069001215487072322, 8716623267269178440, 5336385715226370016, 14456089431355876478, 17683787366271106808, 5052021921702764563, 16478152596505612522, 5638004933879392817]
[6449103750905854967, 12029349297046688094, 13069001215487072322, 8716623267269178440, 5336385715226370016, 14456089431355876478, 5618841090288840293, 5052021921702764563, 16478152596505612522, 5638004933879392817]

File diff suppressed because one or more lines are too long

View file

@ -21,16 +21,18 @@ export class NativeInterpreter extends JSChannel_ {
intercept_link_redirects: boolean;
ipc: any;
editsPath: string;
eventsPath: string;
kickStylesheets: boolean;
queuedBytes: ArrayBuffer[] = [];
// eventually we want to remove liveview and build it into the server-side-events of fullstack
// however, for now we need to support it since SSE in fullstack doesn't exist yet
// however, for now we need to support it since WebSockets in fullstack doesn't exist yet
liveview: boolean;
constructor(editsPath: string) {
constructor(editsPath: string, eventsPath: string) {
super();
this.editsPath = editsPath;
this.eventsPath = eventsPath;
this.kickStylesheets = false;
}
@ -218,23 +220,40 @@ export class NativeInterpreter extends JSChannel_ {
) {
if (target.getAttribute("type") === "file") {
this.readFiles(target, contents, bubbles, realId, name);
return;
}
}
} else {
}
const response = this.sendSerializedEvent(body);
// capture/prevent default of the event if the virtualdom wants to
if (response) {
if (response.preventDefault) {
event.preventDefault();
} else {
// Attempt to intercept if the event is a click and the default action was not prevented
if (target instanceof Element && event.type === "click") {
this.handleClickNavigate(event, target);
}
}
if (response.stopPropagation) {
event.stopPropagation();
}
}
}
sendSerializedEvent(body: {
name: string;
element: number;
data: any;
bubbles: boolean;
}): EventSyncResult | void {
if (this.liveview) {
const message = this.serializeIpcMessage("user_event", body);
this.ipc.postMessage(message);
// // Run the event handler on the virtualdom
// // capture/prevent default of the event if the virtualdom wants to
// const res = handleVirtualdomEventSync(JSON.stringify(body));
// if (res.preventDefault) {
// event.preventDefault();
// }
// if (res.stopPropagation) {
// event.stopPropagation();
// }
} else {
// Run the event handler on the virtualdom
return handleVirtualdomEventSync(this.eventsPath, JSON.stringify(body));
}
}
@ -244,35 +263,12 @@ export class NativeInterpreter extends JSChannel_ {
// - prevent buttons from submitting forms
// - let the virtualdom attempt to prevent the event
preventDefaults(event: Event, target: EventTarget) {
let preventDefaultRequests: string | null = null;
// Some events can be triggered on text nodes, which don't have attributes
if (target instanceof Element) {
preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`);
}
if (
preventDefaultRequests &&
preventDefaultRequests.includes(`on${event.type}`)
) {
event.preventDefault();
}
if (event.type === "submit") {
event.preventDefault();
}
// Attempt to intercept if the event is a click
if (target instanceof Element && event.type === "click") {
this.handleClickNavigate(event, target, preventDefaultRequests);
}
}
handleClickNavigate(
event: Event,
target: Element,
preventDefaultRequests: string
) {
handleClickNavigate(event: Event, target: Element) {
// todo call prevent default if it's the right type of event
if (!this.intercept_link_redirects) {
return;
@ -291,24 +287,9 @@ export class NativeInterpreter extends JSChannel_ {
event.preventDefault();
let elementShouldPreventDefault =
preventDefaultRequests && preventDefaultRequests.includes(`onclick`);
let aElementShouldPreventDefault = a_element.getAttribute(
`dioxus-prevent-default`
);
let linkShouldPreventDefault =
aElementShouldPreventDefault &&
aElementShouldPreventDefault.includes(`onclick`);
if (!elementShouldPreventDefault && !linkShouldPreventDefault) {
const href = a_element.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
this.ipc.postMessage(
this.serializeIpcMessage("browser_open", { href })
);
}
const href = a_element.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
this.ipc.postMessage(this.serializeIpcMessage("browser_open", { href }));
}
}
@ -387,7 +368,7 @@ export class NativeInterpreter extends JSChannel_ {
contents.files = { files: file_contents };
const message = this.serializeIpcMessage("user_event", {
const message = this.sendSerializedEvent({
name: name,
element: realId,
data: contents,
@ -401,8 +382,6 @@ export class NativeInterpreter extends JSChannel_ {
type EventSyncResult = {
preventDefault: boolean;
stopPropagation: boolean;
stopImmediatePropagation: boolean;
filesRequested: boolean;
};
// This function sends the event to the virtualdom and then waits for the virtualdom to process it
@ -410,13 +389,15 @@ type EventSyncResult = {
// However, it's not really suitable for liveview, because it's synchronous and will block the main thread
// We should definitely consider using a websocket if we want to block... or just not block on liveview
// Liveview is a little bit of a tricky beast
function handleVirtualdomEventSync(contents: string): EventSyncResult {
function handleVirtualdomEventSync(
endpoint: string,
contents: string
): EventSyncResult {
// Handle the event on the virtualdom and then process whatever its output was
const xhr = new XMLHttpRequest();
// Serialize the event and send it to the custom protocol in the Rust side of things
xhr.timeout = 1000;
xhr.open("GET", "/handle/event.please", false);
xhr.open("POST", endpoint, false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(contents);

View file

@ -185,7 +185,7 @@ mod js {
// if this is a mounted listener, we send the event immediately
if (event_name === "mounted") {
window.ipc.postMessage(
this.serializeIpcMessage("user_event", {
this.sendSerializedEvent({
name: event_name,
element: id,
data: null,

View file

@ -189,3 +189,23 @@ impl WriteMutations for MutationState {
self.channel.push_root(id.0 as _);
}
}
/// A synchronous response to a browser event which may prevent the default browser's action
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[derive(Default)]
pub struct SynchronousEventResponse {
#[cfg(feature = "serialize")]
#[serde(rename = "preventDefault")]
prevent_default: bool,
}
impl SynchronousEventResponse {
/// Create a new SynchronousEventResponse
#[allow(unused)]
pub fn new(prevent_default: bool) -> Self {
Self {
#[cfg(feature = "serialize")]
prevent_default,
}
}
}

View file

@ -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

View file

@ -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#"
<!DOCTYPE html>
<html>
<head> <title>{title}</title> </head>
<body> <div id="main"></div> </body>
<head><title>{title}</title></head>
<body><div id="main"></div></body>
{glue}
</html>
"#,

View file

@ -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<R: LiveviewRouter> {
impl<R: LiveviewRouter> Default for Config<R> {
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(),
}
}

View file

@ -6,7 +6,7 @@ pub type Config = crate::Config<axum::Router>;
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any + Send + Sync> + Send + Sync>>,
platform_config: Config,
) -> ! {
#[cfg(feature = "multi-thread")]

View file

@ -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,

View file

@ -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");

View file

@ -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<dyn Any>,
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);

View file

@ -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 <a> element to change the text
await a.click();
// Check that the <a> element changed.
await expect(a).toHaveText("Psych!");
});

View file

@ -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}"
}
}
}

View file

@ -51,7 +51,7 @@ pub struct HistoryButtonProps {
/// # vdom.rebuild_in_place();
/// # assert_eq!(
/// # dioxus_ssr::render(&vdom),
/// # r#"<button disabled="true" dioxus-prevent-default="onclick">go back</button>"#
/// # r#"<button disabled="true">go back</button>"#
/// # );
/// ```
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#"<button disabled="true" dioxus-prevent-default="onclick">go forward</button>"#
/// # r#"<button disabled="true">go forward</button>"#
/// # );
/// ```
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}
}
}

View file

@ -195,7 +195,7 @@ impl Debug for LinkProps {
/// # vdom.rebuild_in_place();
/// # assert_eq!(
/// # dioxus_ssr::render(&vdom),
/// # r#"<a href="/" dioxus-prevent-default="" class="link_class active" rel="link_rel" target="_blank" aria-current="page" id="link_id">A fully configured link</a>"#
/// # r#"<a href="/" class="link_class active" rel="link_rel" target="_blank" aria-current="page" id="link_id">A fully configured link</a>"#
/// # );
/// ```
#[doc(alias = "<a>")]
@ -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,

View file

@ -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<Rc<dyn Any>, String> {
self.inner.read().history.parse_route(route)
}

View file

@ -317,6 +317,9 @@ pub(crate) trait AnyHistoryProvider {
#[allow(unused_variables)]
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {}
#[cfg(feature = "liveview")]
fn is_liveview(&self) -> bool;
}
pub(crate) struct AnyHistoryProviderImplWrapper<R, H> {
@ -339,7 +342,7 @@ impl<R, H: Default> Default for AnyHistoryProviderImplWrapper<R, H> {
}
}
impl<R, H> AnyHistoryProvider for AnyHistoryProviderImplWrapper<R, H>
impl<R, H: 'static> AnyHistoryProvider for AnyHistoryProviderImplWrapper<R, H>
where
R: Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
@ -389,4 +392,11 @@ where
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
self.inner.updater(callback)
}
#[cfg(feature = "liveview")]
fn is_liveview(&self) -> bool {
use std::any::TypeId;
TypeId::of::<H>() == TypeId::of::<LiveviewHistory<R>>()
}
}

View file

@ -72,11 +72,7 @@ fn href_internal() {
}
}
let expected = format!(
"<h1>App</h1><a {href} {default}>Link</a>",
href = r#"href="/test""#,
default = r#"dioxus-prevent-default="onclick""#,
);
let expected = format!("<h1>App</h1><a {href}>Link</a>", href = r#"href="/test""#,);
assert_eq!(prepare::<Route>(), expected);
}
@ -107,9 +103,8 @@ fn href_external() {
}
let expected = format!(
"<h1>App</h1><a {href} {default} {rel}>Link</a>",
"<h1>App</h1><a {href} {rel}>Link</a>",
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!(
"<h1>App</h1><a {href} {default} {class}>Link</a>",
"<h1>App</h1><a {href} {class}>Link</a>",
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!(
"<h1>App</h1><a {href} {default} {class} {aria}>Link</a>",
"<h1>App</h1><a {href} {class} {aria}>Link</a>",
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!(
"<h1>App</h1><a {href} {default} {class}>Link</a>",
"<h1>App</h1><a {href} {class}>Link</a>",
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!(
"<h1>App</h1><a {href} {default} {id}>Link</a>",
"<h1>App</h1><a {href} {id}>Link</a>",
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!(
"<h1>App</h1><a {href} {default} {target}>Link</a>",
"<h1>App</h1><a {href} {target}>Link</a>",
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!(
"<h1>App</h1><a {href} {default} {rel} {target}>Link</a>",
"<h1>App</h1><a {href} {rel} {target}>Link</a>",
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!(
"<h1>App</h1><a {href} {default} {rel}>Link</a>",
"<h1>App</h1><a {href} {rel}>Link</a>",
href = r#"href="/test""#,
default = r#"dioxus-prevent-default="onclick""#,
rel = r#"rel="test_rel""#,
);

View file

@ -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<UiEvent>,
pub(crate) runtime: Rc<Runtime>,
#[cfg(feature = "mounted")]
pub(crate) queued_mounted_events: Vec<ElementId>,
@ -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<UiEvent>) -> Self {
pub fn new(cfg: Config, runtime: Rc<Runtime>) -> 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<dyn FnMut(&Event)> = 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<dyn Any>, 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")]

View file

@ -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<futures_channel::mpsc::UnboundedReceiver<SuspenseMessage>> =
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<SuspenseMessage> = 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;

View file

@ -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::<web_sys::Element>() {
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<dyn std::any::Any>,
false,
);
let name = "mounted";
self.runtime.handle_event(name, event, id)
}
}
}