mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
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:
parent
8b62b71e2d
commit
cab573eefd
54 changed files with 941 additions and 730 deletions
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}))
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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(_) => {}
|
||||
|
|
|
@ -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() {{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
"#,
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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!");
|
||||
});
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>>()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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""#,
|
||||
);
|
||||
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue