Merge pull request #36 from DioxusLabs/jk/stack_dst

feat: event bubbling
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2021-11-16 01:28:06 -05:00
commit 34c9978ad1
32 files changed
+1587 -1420

No files matched your search

+143
View File
@@ -199,6 +199,149 @@ You shouldn't use Dioxus if:
- You want to support browsers where Wasm or asm.js are not supported. - You want to support browsers where Wasm or asm.js are not supported.
## Show me some examples!
In our collection of examples, guides, and tutorials, we have:
- The book (an introductory course)
- The guide (an in-depth analysis of everything in Dioxus)
- The reference (a collection of examples with heavy documentation)
- The general examples
- The platform-specific examples (web, ssr, desktop, mobile, server)
Here's what a few common tasks look like in Dioxus:
Nested components with children and internal state:
```rust
fn App(cx: Context, props: &()) -> Element {
cx.render(rsx!( Toggle { "Toggle me" } ))
}
#[derive(PartialEq, Props)]
struct ToggleProps { children: Element }
fn Toggle(cx: Context, props: &ToggleProps) -> Element {
let mut toggled = use_state(cx, || false);
cx.render(rsx!{
div {
{&props.children}
button { onclick: move |_| toggled.set(true),
{toggled.and_then(|| "On").or_else(|| "Off")}
}
}
})
}
```
Controlled inputs:
```rust
fn App(cx: Context, props: &()) -> Element {
let value = use_state(cx, String::new);
cx.render(rsx!(
input {
"type": "text",
value: "{value}",
oninput: move |evt| value.set(evt.value.clone())
}
))
}
```
Lists and Conditional rendering:
```rust
fn App(cx: Context, props: &()) -> Element {
let list = (0..10).map(|i| {
rsx!(li { key: "{i}", "Value: {i}" })
});
let title = match list.len() {
0 => rsx!("Not enough"),
_ => rsx!("Plenty!"),
};
if should_show {
cx.render(rsx!(
{title}
ul { {list} }
))
} else {
None
}
}
```
Tiny components:
```rust
static App: FC<()> = |cx, _| rsx!(cx, div {"hello world!"});
```
Borrowed prop contents:
```rust
fn App(cx: Context, props: &()) -> Element {
let name = use_state(cx, || String::from("example"));
rsx!(cx, Child { title: name.as_str() })
}
#[derive(Props)]
struct ChildProps<'a> { title: &'a str }
fn Child(cx: Context, props: &ChildProps) -> Element {
rsx!(cx, "Hello {props.title}")
}
```
Global State
```rust
struct GlobalState { name: String }
fn App(cx: Context, props: &()) -> Element {
use_provide_shared_state(cx, || GlobalState { name: String::from("Toby") })
rsx!(cx, Leaf {})
}
fn Leaf(cx: Context, props: &()) -> Element {
let state = use_consume_shared_state::<GlobalState>(cx)?;
rsx!(cx, "Hello {state.name}")
}
```
Router (inspired by Yew-Router)
```rust
#[derive(PartialEq, Clone, Hash, Eq, Routable)]
enum Route {
#[at("/")]
Home,
#[at("/post/{id}")]
Post(id)
}
fn App(cx: Context, props: &()) -> Element {
let route = use_router(cx, Route::parse);
cx.render(rsx!(div {
{match route {
Route::Home => rsx!( Home {} ),
Route::Post(id) => rsx!( Post { id: id })
}}
}))
}
```
Suspense
```rust
fn App(cx: Context, props: &()) -> Element {
let doggo = use_suspense(cx,
|| async { reqwest::get("https://dog.ceo/api/breeds/image/random").await.unwrap().json::<Response>().await.unwrap() },
|response| cx.render(rsx!( img { src: "{response.message}" }))
);
cx.render(rsx!{
div {
"One doggo coming right up:"
{doggo}
}
})
}
```
## License ## License
This project is licensed under the [MIT license]. This project is licensed under the [MIT license].
+14 -3
View File
@@ -2,6 +2,8 @@
//! //!
//! The example from the README.md. //! The example from the README.md.
use std::time::Duration;
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() { fn main() {
dioxus::desktop::launch(App, |c| c); dioxus::desktop::launch(App, |c| c);
@@ -10,14 +12,23 @@ fn main() {
static App: FC<()> = |cx, props| { static App: FC<()> = |cx, props| {
let mut count = use_state(cx, || 0); let mut count = use_state(cx, || 0);
cx.push_task(async { cx.push_task(async move {
panic!("polled future"); tokio::time::sleep(Duration::from_millis(100)).await;
// println!("setting count");
count += 1;
// count.set(10);
// *count += 1;
// let c = count.get() + 1;
// count.set(c);
}); });
cx.render(rsx! { cx.render(rsx! {
div { div {
h1 { "High-Five counter: {count}" } h1 { "High-Five counter: {count}" }
// button {
// onclick: move |_| count +=1 ,
// "Click me!"
// }
} }
}) })
}; };
+2
View File
@@ -202,6 +202,8 @@ pub fn rsx(s: TokenStream) -> TokenStream {
/// Home, /// Home,
/// #[at("/secure")] /// #[at("/secure")]
/// Secure, /// Secure,
/// #[at("/profile/{id}")]
/// Profile(u32),
/// #[at("/404")] /// #[at("/404")]
/// NotFound, /// NotFound,
/// } /// }
+25 -24
View File
@@ -200,11 +200,11 @@ pub fn routable_derive_impl(input: Routable) -> TokenStream {
); );
quote! { quote! {
::std::thread_local! { // ::std::thread_local! {
#[doc(hidden)] // #[doc(hidden)]
#[allow(non_upper_case_globals)] // #[allow(non_upper_case_globals)]
static #cache_thread_local_ident: ::std::cell::RefCell<::std::option::Option<#ident>> = ::std::cell::RefCell::new(::std::option::Option::None); // static #cache_thread_local_ident: ::std::cell::RefCell<::std::option::Option<#ident>> = ::std::cell::RefCell::new(::std::option::Option::None);
} // }
#[automatically_derived] #[automatically_derived]
impl ::dioxus::router::Routable for #ident { impl ::dioxus::router::Routable for #ident {
@@ -219,29 +219,30 @@ pub fn routable_derive_impl(input: Routable) -> TokenStream {
#not_found_route #not_found_route
} }
fn current_route() -> ::std::option::Option<Self> { // fn current_route() -> ::std::option::Option<Self> {
#cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow())) // #cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow()))
} // }
fn recognize(pathname: &str) -> ::std::option::Option<Self> { fn recognize(pathname: &str) -> ::std::option::Option<Self> {
::std::thread_local! { todo!()
static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>(); // ::std::thread_local! {
} // static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>();
let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname)); // }
{ // let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname));
let route = ::std::clone::Clone::clone(&route); // {
#cache_thread_local_ident.with(move |val| { // let route = ::std::clone::Clone::clone(&route);
*val.borrow_mut() = route; // #cache_thread_local_ident.with(move |val| {
}); // *val.borrow_mut() = route;
} // });
route // }
// route
} }
fn cleanup() { // fn cleanup() {
#cache_thread_local_ident.with(move |val| { // #cache_thread_local_ident.with(move |val| {
*val.borrow_mut() = ::std::option::Option::None; // *val.borrow_mut() = ::std::option::Option::None;
}); // });
} // }
} }
} }
} }
+1
View File
@@ -39,6 +39,7 @@ indexmap = "1.7.0"
# Serialize the Edits for use in Webview/Liveview instances # Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
backtrace = "0.3.63"
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.42" anyhow = "1.0.42"
+5 -5
View File
@@ -36,11 +36,11 @@ impl<const A: bool> FragmentBuilder<A> {
/// ///
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust, ignore
/// fn App(cx: Context, props: &()) -> Element { /// fn App(cx: Context, props: &()) -> Element {
/// cx.render(rsx!{ /// cx.render(rsx!{
/// CustomCard { /// CustomCard {
/// h1 {} /// h1 {}2
/// p {} /// p {}
/// } /// }
/// }) /// })
@@ -82,7 +82,7 @@ impl Properties for FragmentProps {
/// ///
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust, ignore
/// rsx!{ /// rsx!{
/// Fragment { key: "abc" } /// Fragment { key: "abc" }
/// } /// }
@@ -118,7 +118,7 @@ pub fn Fragment(cx: Context, props: &FragmentProps) -> Element {
/// ## Example /// ## Example
/// ///
/// For props that are 'static: /// For props that are 'static:
/// ```rust ignore /// ```rust, ignore ignore
/// #[derive(Props, PartialEq)] /// #[derive(Props, PartialEq)]
/// struct MyProps { /// struct MyProps {
/// data: String /// data: String
@@ -127,7 +127,7 @@ pub fn Fragment(cx: Context, props: &FragmentProps) -> Element {
/// ///
/// For props that borrow: /// For props that borrow:
/// ///
/// ```rust ignore /// ```rust, ignore ignore
/// #[derive(Props)] /// #[derive(Props)]
/// struct MyProps<'a >{ /// struct MyProps<'a >{
/// data: &'a str /// data: &'a str
+81 -56
View File
@@ -54,7 +54,7 @@
//! We also employ "subtree memoization" which saves us from having to check trees which hold no dynamic content. We can //! We also employ "subtree memoization" which saves us from having to check trees which hold no dynamic content. We can
//! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the //! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the
//! calls to "create" propagate this information upwards. Structures like the one below are entirely static: //! calls to "create" propagate this information upwards. Structures like the one below are entirely static:
//! ```rust //! ```rust, ignore
//! rsx!( div { class: "hello world", "this node is entirely static" } ) //! rsx!( div { class: "hello world", "this node is entirely static" } )
//! ``` //! ```
//! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so it's up to the reconciler to //! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so it's up to the reconciler to
@@ -147,6 +147,7 @@ pub(crate) enum DiffInstruction<'a> {
}, },
PopScope, PopScope,
PopElement,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -162,30 +163,32 @@ pub(crate) struct DiffStack<'bump> {
pub(crate) instructions: Vec<DiffInstruction<'bump>>, pub(crate) instructions: Vec<DiffInstruction<'bump>>,
nodes_created_stack: SmallVec<[usize; 10]>, nodes_created_stack: SmallVec<[usize; 10]>,
pub scope_stack: SmallVec<[ScopeId; 5]>, pub scope_stack: SmallVec<[ScopeId; 5]>,
pub element_stack: SmallVec<[ElementId; 10]>,
} }
impl<'bump> DiffStack<'bump> { impl<'bump> DiffStack<'bump> {
pub fn new() -> Self { fn new() -> Self {
Self { Self {
instructions: Vec::with_capacity(1000), instructions: Vec::with_capacity(1000),
nodes_created_stack: smallvec![], nodes_created_stack: smallvec![],
scope_stack: smallvec![], scope_stack: smallvec![],
element_stack: smallvec![],
} }
} }
pub fn pop(&mut self) -> Option<DiffInstruction<'bump>> { fn pop(&mut self) -> Option<DiffInstruction<'bump>> {
self.instructions.pop() self.instructions.pop()
} }
pub fn pop_off_scope(&mut self) { fn pop_off_scope(&mut self) {
self.scope_stack.pop(); self.scope_stack.pop();
} }
pub fn push(&mut self, instruction: DiffInstruction<'bump>) { pub(crate) fn push(&mut self, instruction: DiffInstruction<'bump>) {
self.instructions.push(instruction) self.instructions.push(instruction)
} }
pub fn create_children(&mut self, children: &'bump [VNode<'bump>], and: MountType<'bump>) { fn create_children(&mut self, children: &'bump [VNode<'bump>], and: MountType<'bump>) {
self.nodes_created_stack.push(0); self.nodes_created_stack.push(0);
self.instructions.push(DiffInstruction::Mount { and }); self.instructions.push(DiffInstruction::Mount { and });
@@ -195,36 +198,36 @@ impl<'bump> DiffStack<'bump> {
} }
} }
pub fn push_subtree(&mut self) { fn push_subtree(&mut self) {
self.nodes_created_stack.push(0); self.nodes_created_stack.push(0);
self.instructions.push(DiffInstruction::Mount { self.instructions.push(DiffInstruction::Mount {
and: MountType::Append, and: MountType::Append,
}); });
} }
pub fn push_nodes_created(&mut self, count: usize) { fn push_nodes_created(&mut self, count: usize) {
self.nodes_created_stack.push(count); self.nodes_created_stack.push(count);
} }
pub fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) { pub(crate) fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) {
self.nodes_created_stack.push(0); self.nodes_created_stack.push(0);
self.instructions.push(DiffInstruction::Mount { and }); self.instructions.push(DiffInstruction::Mount { and });
self.instructions.push(DiffInstruction::Create { node }); self.instructions.push(DiffInstruction::Create { node });
} }
pub fn add_child_count(&mut self, count: usize) { fn add_child_count(&mut self, count: usize) {
*self.nodes_created_stack.last_mut().unwrap() += count; *self.nodes_created_stack.last_mut().unwrap() += count;
} }
pub fn pop_nodes_created(&mut self) -> usize { fn pop_nodes_created(&mut self) -> usize {
self.nodes_created_stack.pop().unwrap() self.nodes_created_stack.pop().unwrap()
} }
pub fn current_scope(&self) -> Option<ScopeId> { fn current_scope(&self) -> Option<ScopeId> {
self.scope_stack.last().copied() self.scope_stack.last().copied()
} }
pub fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) { fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) {
// Push the new scope onto the stack // Push the new scope onto the stack
self.scope_stack.push(idx); self.scope_stack.push(idx);
@@ -241,7 +244,8 @@ impl<'bump> DiffState<'bump> {
let (old, new) = (self.scopes.wip_head(id), self.scopes.fin_head(id)); let (old, new) = (self.scopes.wip_head(id), self.scopes.fin_head(id));
self.stack.push(DiffInstruction::Diff { old, new }); self.stack.push(DiffInstruction::Diff { old, new });
self.stack.scope_stack.push(*id); self.stack.scope_stack.push(*id);
self.stack.push_nodes_created(0); let scope = self.scopes.get_scope(id).unwrap();
self.stack.element_stack.push(scope.container);
self.work(|| false); self.work(|| false);
} }
@@ -263,10 +267,13 @@ impl<'bump> DiffState<'bump> {
self.stack.add_child_count(num_on_stack); self.stack.add_child_count(num_on_stack);
} }
DiffInstruction::PopScope => self.stack.pop_off_scope(), DiffInstruction::PopScope => self.stack.pop_off_scope(),
DiffInstruction::PopElement => {
self.stack.element_stack.pop();
}
}; };
if deadline_expired() { if deadline_expired() {
log::debug!("Deadline expired before we could finished!"); log::debug!("Deadline expired before we could finish!");
return false; return false;
} }
} }
@@ -358,6 +365,7 @@ impl<'bump> DiffState<'bump> {
fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) { fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) {
let real_id = self.scopes.reserve_node(node); let real_id = self.scopes.reserve_node(node);
self.mutations.create_text_node(vtext.text, real_id); self.mutations.create_text_node(vtext.text, real_id);
vtext.dom_id.set(Some(real_id)); vtext.dom_id.set(Some(real_id));
self.stack.add_child_count(1); self.stack.add_child_count(1);
@@ -390,11 +398,19 @@ impl<'bump> DiffState<'bump> {
children, children,
namespace, namespace,
dom_id, dom_id,
parent_id,
.. ..
} = element; } = element;
let real_id = self.scopes.reserve_node(node); // set the parent ID for event bubbling
self.stack.instructions.push(DiffInstruction::PopElement);
let parent = self.stack.element_stack.last().unwrap();
parent_id.set(Some(*parent));
// set the id of the element
let real_id = self.scopes.reserve_node(node);
self.stack.element_stack.push(real_id);
dom_id.set(Some(real_id)); dom_id.set(Some(real_id));
self.mutations.create_element(tag_name, *namespace, real_id); self.mutations.create_element(tag_name, *namespace, real_id);
@@ -438,9 +454,13 @@ impl<'bump> DiffState<'bump> {
let caller = unsafe { std::mem::transmute(vcomponent.caller as *const _) }; let caller = unsafe { std::mem::transmute(vcomponent.caller as *const _) };
let fc_ptr = vcomponent.user_fc; let fc_ptr = vcomponent.user_fc;
let new_idx = self let container = *self.stack.element_stack.last().unwrap();
.scopes
.new_with_key(fc_ptr, caller, parent_scope, height, subtree); let new_idx =
self.scopes
.new_with_key(fc_ptr, caller, parent_scope, container, height, subtree);
log::debug!("container for scope {:?} is {:?}", new_idx, container);
// Actually initialize the caller's slot with the right address // Actually initialize the caller's slot with the right address
vcomponent.associated_scope.set(Some(new_idx)); vcomponent.associated_scope.set(Some(new_idx));
@@ -495,7 +515,9 @@ impl<'bump> DiffState<'bump> {
use VNode::*; use VNode::*;
match (old_node, new_node) { match (old_node, new_node) {
// Check the most common cases first // Check the most common cases first
(Text(old), Text(new)) => self.diff_text_nodes(old, new), (Text(old), Text(new)) => {
self.diff_text_nodes(old, new, old_node, new_node);
}
(Component(old), Component(new)) => { (Component(old), Component(new)) => {
self.diff_component_nodes(old_node, new_node, old, new) self.diff_component_nodes(old_node, new_node, old, new)
} }
@@ -517,11 +539,18 @@ impl<'bump> DiffState<'bump> {
} }
} }
fn diff_text_nodes(&mut self, old: &'bump VText<'bump>, new: &'bump VText<'bump>) { fn diff_text_nodes(
&mut self,
old: &'bump VText<'bump>,
new: &'bump VText<'bump>,
_old_node: &'bump VNode<'bump>,
new_node: &'bump VNode<'bump>,
) {
if let Some(root) = old.dom_id.get() { if let Some(root) = old.dom_id.get() {
if old.text != new.text { if old.text != new.text {
self.mutations.set_text(new.text, root.as_u64()); self.mutations.set_text(new.text, root.as_u64());
} }
self.scopes.update_node(new_node, root);
new.dom_id.set(Some(root)); new.dom_id.set(Some(root));
} }
@@ -551,7 +580,10 @@ impl<'bump> DiffState<'bump> {
return; return;
} }
self.scopes.update_node(new_node, root);
new.dom_id.set(Some(root)); new.dom_id.set(Some(root));
new.parent_id.set(old.parent_id.get());
// todo: attributes currently rely on the element on top of the stack, but in theory, we only need the id of the // todo: attributes currently rely on the element on top of the stack, but in theory, we only need the id of the
// element to modify its attributes. // element to modify its attributes.
@@ -617,8 +649,12 @@ impl<'bump> DiffState<'bump> {
self.mutations.edits.push(PushRoot { self.mutations.edits.push(PushRoot {
root: root.as_u64(), root: root.as_u64(),
}); });
self.stack.element_stack.push(root);
self.stack.instructions.push(DiffInstruction::PopElement);
self.stack.create_children(new.children, MountType::Append); self.stack.create_children(new.children, MountType::Append);
} else { } else {
self.stack.element_stack.push(root);
self.stack.instructions.push(DiffInstruction::PopElement);
self.diff_children(old.children, new.children); self.diff_children(old.children, new.children);
} }
} }
@@ -650,20 +686,18 @@ impl<'bump> DiffState<'bump> {
let scope = unsafe { let scope = unsafe {
self.scopes self.scopes
.get_scope_mut(&scope_addr) .get_scope_mut(&scope_addr)
.expect(&format!("could not find {:?}", scope_addr)) .unwrap_or_else(|| panic!("could not find {:?}", scope_addr))
}; };
scope.caller = unsafe { std::mem::transmute(new.caller) }; scope.caller = unsafe { std::mem::transmute(new.caller) };
// React doesn't automatically memoize, but we do. // React doesn't automatically memoize, but we do.
let props_are_the_same = old.comparator.unwrap(); let props_are_the_same = old.comparator.unwrap();
if self.force_diff || !props_are_the_same(new) { if (self.force_diff || !props_are_the_same(new)) && self.scopes.run_scope(&scope_addr) {
if self.scopes.run_scope(&scope_addr) { self.diff_node(
self.diff_node( self.scopes.wip_head(&scope_addr),
self.scopes.wip_head(&scope_addr), self.scopes.fin_head(&scope_addr),
self.scopes.fin_head(&scope_addr), );
);
}
} }
self.stack.scope_stack.pop(); self.stack.scope_stack.pop();
@@ -856,13 +890,7 @@ impl<'bump> DiffState<'bump> {
Some(count) => count, Some(count) => count,
None => return, None => return,
}; };
// log::debug!(
// "Left offset, right offset, {}, {}",
// left_offset,
// right_offset,
// );
// log::debug!("stack before lo is {:#?}", self.stack.instructions);
// Ok, we now hopefully have a smaller range of children in the middle // Ok, we now hopefully have a smaller range of children in the middle
// within which to re-order nodes with the same keys, remove old nodes with // within which to re-order nodes with the same keys, remove old nodes with
// now-unused keys, and create new nodes with fresh keys. // now-unused keys, and create new nodes with fresh keys.
@@ -911,8 +939,6 @@ impl<'bump> DiffState<'bump> {
} else { } else {
self.diff_keyed_middle(old_middle, new_middle); self.diff_keyed_middle(old_middle, new_middle);
} }
log::debug!("stack after km is {:#?}", self.stack.instructions);
} }
/// Diff both ends of the children that share keys. /// Diff both ends of the children that share keys.
@@ -1039,15 +1065,6 @@ impl<'bump> DiffState<'bump> {
// If none of the old keys are reused by the new children, then we remove all the remaining old children and // If none of the old keys are reused by the new children, then we remove all the remaining old children and
// create the new children afresh. // create the new children afresh.
if shared_keys.is_empty() { if shared_keys.is_empty() {
log::debug!(
"no shared keys, replacing and creating many with many, {:#?}, {:#?}",
old,
new
);
log::debug!("old_key_to_old_index, {:#?}", old_key_to_old_index);
log::debug!("new_index_to_old_index, {:#?}", new_index_to_old_index);
log::debug!("shared_keys, {:#?}", shared_keys);
if let Some(first_old) = old.get(0) { if let Some(first_old) = old.get(0) {
self.remove_nodes(&old[1..], true); self.remove_nodes(&old[1..], true);
self.stack self.stack
@@ -1206,13 +1223,21 @@ impl<'bump> DiffState<'bump> {
fn replace_node(&mut self, old: &'bump VNode<'bump>, nodes_created: usize) { fn replace_node(&mut self, old: &'bump VNode<'bump>, nodes_created: usize) {
match old { match old {
VNode::Element(el) => { VNode::Element(el) => {
let id = old.try_mounted_id().expect(&format!("broke on {:?}", old)); let id = old
.try_mounted_id()
.unwrap_or_else(|| panic!("broke on {:?}", old));
log::debug!("element parent is {:?}", el.parent_id.get());
self.mutations.replace_with(id, nodes_created as u32); self.mutations.replace_with(id, nodes_created as u32);
self.remove_nodes(el.children, false); self.remove_nodes(el.children, false);
} }
VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => { VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => {
let id = old.try_mounted_id().expect(&format!("broke on {:?}", old)); let id = old
.try_mounted_id()
.unwrap_or_else(|| panic!("broke on {:?}", old));
self.mutations.replace_with(id, nodes_created as u32); self.mutations.replace_with(id, nodes_created as u32);
} }
@@ -1226,7 +1251,6 @@ impl<'bump> DiffState<'bump> {
self.replace_node(node, nodes_created); self.replace_node(node, nodes_created);
let scope_id = c.associated_scope.get().unwrap(); let scope_id = c.associated_scope.get().unwrap();
println!("replacing c {:?} ", scope_id);
log::debug!("Destroying scope {:?}", scope_id); log::debug!("Destroying scope {:?}", scope_id);
self.scopes.try_remove(&scope_id).unwrap(); self.scopes.try_remove(&scope_id).unwrap();
} }
@@ -1320,13 +1344,14 @@ impl<'bump> DiffState<'bump> {
.current_scope() .current_scope()
.and_then(|id| self.scopes.get_scope(&id)) .and_then(|id| self.scopes.get_scope(&id))
{ {
// safety: this lifetime is managed by the logic on scope todo!()
let extended = unsafe { std::mem::transmute(suspended) }; // // safety: this lifetime is managed by the logic on scope
scope // let extended = unsafe { std::mem::transmute(suspended) };
.items // scope
.borrow_mut() // .items
.suspended_nodes // .borrow_mut()
.insert(suspended.task_id, extended); // .suspended_nodes
// .insert(suspended.task_id, extended);
} }
} }
} }
+16 -16
View File
@@ -20,7 +20,7 @@ use std::mem;
/// closures, but if we wrap the closure in a concrete type, we can maintain separate implementations of IntoVNode. /// closures, but if we wrap the closure in a concrete type, we can maintain separate implementations of IntoVNode.
/// ///
/// ///
/// ```rust /// ```rust, ignore
/// LazyNodes::new(|f| f.element("div", [], [], [] None)) /// LazyNodes::new(|f| f.element("div", [], [], [] None))
/// ``` /// ```
pub struct LazyNodes<'a, 'b> { pub struct LazyNodes<'a, 'b> {
@@ -47,7 +47,14 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
fac.map(inner) fac.map(inner)
}; };
unsafe { LazyNodes::new_inner(val) } // miri does not know how to work with mucking directly into bytes
if cfg!(miri) {
Self {
inner: StackNodeStorage::Heap(Box::new(val)),
}
} else {
unsafe { LazyNodes::new_inner(val) }
}
} }
unsafe fn new_inner<F>(val: F) -> Self unsafe fn new_inner<F>(val: F) -> Self
@@ -212,8 +219,6 @@ fn round_to_words(len: usize) -> usize {
fn it_works() { fn it_works() {
let bump = bumpalo::Bump::new(); let bump = bumpalo::Bump::new();
simple_logger::init().unwrap();
let factory = NodeFactory { bump: &bump }; let factory = NodeFactory { bump: &bump };
let caller = NodeFactory::annotate_lazy(|f| { let caller = NodeFactory::annotate_lazy(|f| {
@@ -232,9 +237,7 @@ fn it_drops() {
use std::rc::Rc; use std::rc::Rc;
let bump = bumpalo::Bump::new(); let bump = bumpalo::Bump::new();
simple_logger::init().unwrap(); let factory = NodeFactory { bump: &bump };
// let factory = NodeFactory { scope: &bump };
struct DropInner { struct DropInner {
id: i32, id: i32,
@@ -246,7 +249,7 @@ fn it_drops() {
} }
let val = Rc::new(10); let val = Rc::new(10);
{ let caller = {
let it = (0..10) let it = (0..10)
.map(|i| { .map(|i| {
let val = val.clone(); let val = val.clone();
@@ -259,16 +262,13 @@ fn it_drops() {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let caller = NodeFactory::annotate_lazy(|f| { NodeFactory::annotate_lazy(|f| {
log::debug!("main closure"); log::debug!("main closure");
f.fragment_from_iter(it) f.fragment_from_iter(it)
}) })
.unwrap(); .unwrap()
} };
// let nodes = caller.call(factory); let _ = caller.call(factory);
assert_eq!(Rc::strong_count(&val), 1);
// dbg!(nodes);
dbg!(Rc::strong_count(&val));
} }
+87 -102
View File
@@ -5,28 +5,111 @@
use crate::innerlude::*; use crate::innerlude::*;
use std::{any::Any, fmt::Debug}; use std::{any::Any, fmt::Debug};
/// ## Mutations
///
/// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also
/// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has
/// applied the edits.
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
pub struct Mutations<'a> { pub struct Mutations<'a> {
pub edits: Vec<DomEdit<'a>>, pub edits: Vec<DomEdit<'a>>,
pub noderefs: Vec<NodeRefMutation<'a>>, pub refs: Vec<NodeRefMutation<'a>>,
pub effects: Vec<&'a dyn FnMut()>, pub effects: Vec<&'a dyn FnMut()>,
} }
impl Debug for Mutations<'_> { impl Debug for Mutations<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Mutations") f.debug_struct("Mutations")
.field("edits", &self.edits) .field("edits", &self.edits)
.field("noderefs", &self.noderefs) .field("noderefs", &self.refs)
// .field("effects", &self.effects)
.finish() .finish()
} }
} }
/// A `DomEdit` represents a serialized form of the VirtualDom's trait-based API. This allows streaming edits across the
/// network or through FFI boundaries.
#[derive(Debug, PartialEq)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type")
)]
pub enum DomEdit<'bump> {
PushRoot {
root: u64,
},
PopRoot,
AppendChildren {
many: u32,
},
// "Root" refers to the item directly
// it's a waste of an instruction to push the root directly
ReplaceWith {
root: u64,
m: u32,
},
InsertAfter {
root: u64,
n: u32,
},
InsertBefore {
root: u64,
n: u32,
},
Remove {
root: u64,
},
CreateTextNode {
text: &'bump str,
root: u64,
},
CreateElement {
tag: &'bump str,
root: u64,
},
CreateElementNs {
tag: &'bump str,
root: u64,
ns: &'static str,
},
CreatePlaceholder {
root: u64,
},
NewEventListener {
event_name: &'static str,
scope: ScopeId,
root: u64,
},
RemoveEventListener {
root: u64,
event: &'static str,
},
SetText {
root: u64,
text: &'bump str,
},
SetAttribute {
root: u64,
field: &'static str,
value: &'bump str,
ns: Option<&'bump str>,
},
RemoveAttribute {
root: u64,
name: &'static str,
},
}
use DomEdit::*; use DomEdit::*;
impl<'a> Mutations<'a> { impl<'a> Mutations<'a> {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
Self { Self {
edits: Vec::new(), edits: Vec::new(),
noderefs: Vec::new(), refs: Vec::new(),
effects: Vec::new(), effects: Vec::new(),
} }
} }
@@ -156,101 +239,3 @@ impl<'a> NodeRefMutation<'a> {
.and_then(|f| f.downcast_mut::<T>()) .and_then(|f| f.downcast_mut::<T>())
} }
} }
/// A `DomEdit` represents a serialized form of the VirtualDom's trait-based API. This allows streaming edits across the
/// network or through FFI boundaries.
#[derive(Debug, PartialEq)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type")
)]
pub enum DomEdit<'bump> {
PushRoot {
root: u64,
},
PopRoot,
AppendChildren {
many: u32,
},
// "Root" refers to the item directly
// it's a waste of an instruction to push the root directly
ReplaceWith {
root: u64,
m: u32,
},
InsertAfter {
root: u64,
n: u32,
},
InsertBefore {
root: u64,
n: u32,
},
Remove {
root: u64,
},
CreateTextNode {
text: &'bump str,
root: u64,
},
CreateElement {
tag: &'bump str,
root: u64,
},
CreateElementNs {
tag: &'bump str,
root: u64,
ns: &'static str,
},
CreatePlaceholder {
root: u64,
},
NewEventListener {
event_name: &'static str,
scope: ScopeId,
root: u64,
},
RemoveEventListener {
root: u64,
event: &'static str,
},
SetText {
root: u64,
text: &'bump str,
},
SetAttribute {
root: u64,
field: &'static str,
value: &'bump str,
ns: Option<&'bump str>,
},
RemoveAttribute {
root: u64,
name: &'static str,
},
}
impl DomEdit<'_> {
pub fn is(&self, id: &'static str) -> bool {
match self {
DomEdit::InsertAfter { .. } => id == "InsertAfter",
DomEdit::InsertBefore { .. } => id == "InsertBefore",
DomEdit::PushRoot { .. } => id == "PushRoot",
DomEdit::PopRoot => id == "PopRoot",
DomEdit::AppendChildren { .. } => id == "AppendChildren",
DomEdit::ReplaceWith { .. } => id == "ReplaceWith",
DomEdit::Remove { .. } => id == "Remove",
DomEdit::CreateTextNode { .. } => id == "CreateTextNode",
DomEdit::CreateElement { .. } => id == "CreateElement",
DomEdit::CreateElementNs { .. } => id == "CreateElementNs",
DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder",
DomEdit::NewEventListener { .. } => id == "NewEventListener",
DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener",
DomEdit::SetText { .. } => id == "SetText",
DomEdit::SetAttribute { .. } => id == "SetAttribute",
DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute",
}
}
}
+50 -70
View File
@@ -12,6 +12,7 @@ use std::{
any::Any, any::Any,
cell::{Cell, RefCell}, cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter}, fmt::{Arguments, Debug, Formatter},
sync::Arc,
}; };
/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM. /// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM.
@@ -25,7 +26,7 @@ pub enum VNode<'src> {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```rust, ignore
/// let mut vdom = VirtualDom::new(); /// let mut vdom = VirtualDom::new();
/// let node = vdom.render_vnode(rsx!( "hello" )); /// let node = vdom.render_vnode(rsx!( "hello" ));
/// ///
@@ -41,7 +42,7 @@ pub enum VNode<'src> {
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// let mut vdom = VirtualDom::new(); /// let mut vdom = VirtualDom::new();
/// ///
/// let node = vdom.render_vnode(rsx!{ /// let node = vdom.render_vnode(rsx!{
@@ -66,7 +67,7 @@ pub enum VNode<'src> {
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// rsx!{ /// rsx!{
/// a {} /// a {}
/// link {} /// link {}
@@ -81,7 +82,7 @@ pub enum VNode<'src> {
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// fn Example(cx: Context, props: &()) -> Element { /// fn Example(cx: Context, props: &()) -> Element {
/// todo!() /// todo!()
/// } /// }
@@ -100,11 +101,11 @@ pub enum VNode<'src> {
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// ///
/// ///
/// ``` /// ```
Suspended(&'src VSuspended<'src>), Suspended(&'src VSuspended),
/// Anchors are a type of placeholder VNode used when fragments don't contain any children. /// Anchors are a type of placeholder VNode used when fragments don't contain any children.
/// ///
@@ -112,7 +113,7 @@ pub enum VNode<'src> {
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// let mut vdom = VirtualDom::new(); /// let mut vdom = VirtualDom::new();
/// ///
/// let node = vdom.render_vnode(rsx!( Fragment {} )); /// let node = vdom.render_vnode(rsx!( Fragment {} ));
@@ -133,7 +134,7 @@ pub enum VNode<'src> {
/// an `rsx!` call. /// an `rsx!` call.
/// ///
/// # Example /// # Example
/// ```rust /// ```rust, ignore
/// let mut vdom = VirtualDom::new(); /// let mut vdom = VirtualDom::new();
/// ///
/// let node: NodeLink = vdom.render_vnode(rsx!( "hello" )); /// let node: NodeLink = vdom.render_vnode(rsx!( "hello" ));
@@ -202,7 +203,7 @@ impl<'src> VNode<'src> {
VNode::Linked(c) => VNode::Linked(NodeLink { VNode::Linked(c) => VNode::Linked(NodeLink {
scope_id: c.scope_id.clone(), scope_id: c.scope_id.clone(),
link_idx: c.link_idx.clone(), link_idx: c.link_idx.clone(),
node: c.node.clone(), node: c.node,
}), }),
} }
} }
@@ -260,9 +261,7 @@ pub struct VAnchor {
/// A bump-allocated string slice and metadata. /// A bump-allocated string slice and metadata.
pub struct VText<'src> { pub struct VText<'src> {
pub text: &'src str, pub text: &'src str,
pub dom_id: Cell<Option<ElementId>>, pub dom_id: Cell<Option<ElementId>>,
pub is_static: bool, pub is_static: bool,
} }
@@ -275,19 +274,12 @@ pub struct VFragment<'src> {
/// An element like a "div" with children, listeners, and attributes. /// An element like a "div" with children, listeners, and attributes.
pub struct VElement<'a> { pub struct VElement<'a> {
pub tag_name: &'static str, pub tag_name: &'static str,
pub namespace: Option<&'static str>, pub namespace: Option<&'static str>,
pub key: Option<&'a str>, pub key: Option<&'a str>,
pub dom_id: Cell<Option<ElementId>>, pub dom_id: Cell<Option<ElementId>>,
pub parent_id: Cell<Option<ElementId>>, pub parent_id: Cell<Option<ElementId>>,
pub listeners: &'a [Listener<'a>], pub listeners: &'a [Listener<'a>],
pub attributes: &'a [Attribute<'a>], pub attributes: &'a [Attribute<'a>],
pub children: &'a [VNode<'a>], pub children: &'a [VNode<'a>],
} }
@@ -310,7 +302,7 @@ impl Debug for VElement<'_> {
/// ///
/// This trait provides the ability to use custom elements in the `rsx!` macro. /// This trait provides the ability to use custom elements in the `rsx!` macro.
/// ///
/// ```rust /// ```rust, ignore
/// struct my_element; /// struct my_element;
/// ///
/// impl DioxusElement for my_element { /// impl DioxusElement for my_element {
@@ -365,10 +357,10 @@ pub struct Listener<'bump> {
pub event: &'static str, pub event: &'static str,
/// The actual callback that the user specified /// The actual callback that the user specified
pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(Box<dyn Any + Send>) + 'bump>>>, pub(crate) callback:
RefCell<Option<BumpBox<'bump, dyn FnMut(std::sync::Arc<dyn Any + Send + Sync>) + 'bump>>>,
} }
pub type VCompCaller<'src> = BumpBox<'src, dyn Fn(Context) -> Element + 'src>;
/// Virtual Components for custom user-defined components /// Virtual Components for custom user-defined components
/// Only supports the functional syntax /// Only supports the functional syntax
pub struct VComponent<'src> { pub struct VComponent<'src> {
@@ -381,7 +373,7 @@ pub struct VComponent<'src> {
pub(crate) can_memoize: bool, pub(crate) can_memoize: bool,
pub(crate) hard_allocation: Cell<Option<*const ()>>, pub(crate) _hard_allocation: Cell<Option<*const ()>>,
// Raw pointer into the bump arena for the props of the component // Raw pointer into the bump arena for the props of the component
pub(crate) bump_props: *const (), pub(crate) bump_props: *const (),
@@ -395,12 +387,11 @@ pub struct VComponent<'src> {
pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>, pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>,
} }
pub struct VSuspended<'a> { pub struct VSuspended {
pub task_id: u64, pub task_id: usize,
pub scope: Cell<Option<ScopeId>>,
pub dom_id: Cell<Option<ElementId>>, pub dom_id: Cell<Option<ElementId>>,
pub parent: Cell<Option<ElementId>>,
#[allow(clippy::type_complexity)]
pub callback: RefCell<Option<BumpBox<'a, dyn FnMut() -> Element + 'a>>>,
} }
/// A cached node is a "pointer" to a "rendered" node in a particular scope /// A cached node is a "pointer" to a "rendered" node in a particular scope
@@ -426,16 +417,6 @@ impl PartialEq for NodeLink {
self.node == other.node self.node == other.node
} }
} }
impl NodeLink {
// we don't want to let users clone NodeLinks
pub(crate) fn clone_inner(&self) -> Self {
Self {
link_idx: self.link_idx.clone(),
scope_id: self.scope_id.clone(),
node: self.node.clone(),
}
}
}
/// This struct provides an ergonomic API to quickly build VNodes. /// This struct provides an ergonomic API to quickly build VNodes.
/// ///
@@ -453,7 +434,7 @@ impl<'a> NodeFactory<'a> {
#[inline] #[inline]
pub fn bump(&self) -> &'a bumpalo::Bump { pub fn bump(&self) -> &'a bumpalo::Bump {
&self.bump self.bump
} }
/// Directly pass in text blocks without the need to use the format_args macro. /// Directly pass in text blocks without the need to use the format_args macro.
@@ -591,14 +572,13 @@ impl<'a> NodeFactory<'a> {
// - Because FC<P> is the same, then P must be the same (even with generics) // - Because FC<P> is the same, then P must be the same (even with generics)
// - Non-static P are autoderived to memoize as false // - Non-static P are autoderived to memoize as false
// - This comparator is only called on a corresponding set of bumpframes // - This comparator is only called on a corresponding set of bumpframes
let props_memoized = unsafe { //
let real_other: &P = &*(other.bump_props as *const _ as *const P);
props.memoize(real_other)
};
// It's only okay to memoize if there are no children and the props can be memoized // It's only okay to memoize if there are no children and the props can be memoized
// Implementing memoize is unsafe and done automatically with the props trait // Implementing memoize is unsafe and done automatically with the props trait
props_memoized unsafe {
let real_other: &P = &*(other.bump_props as *const _ as *const P);
props.memoize(real_other)
}
} else { } else {
false false
} }
@@ -633,8 +613,7 @@ impl<'a> NodeFactory<'a> {
let caller: &'a mut dyn Fn(&'a Scope) -> Element = let caller: &'a mut dyn Fn(&'a Scope) -> Element =
bump.alloc(move |scope: &Scope| -> Element { bump.alloc(move |scope: &Scope| -> Element {
let props: &'_ P = unsafe { &*(bump_props as *const P) }; let props: &'_ P = unsafe { &*(bump_props as *const P) };
let res = component(scope, props); component(scope, props)
res
}); });
let can_memoize = P::IS_STATIC; let can_memoize = P::IS_STATIC;
@@ -648,14 +627,14 @@ impl<'a> NodeFactory<'a> {
can_memoize, can_memoize,
drop_props, drop_props,
associated_scope: Cell::new(None), associated_scope: Cell::new(None),
hard_allocation: Cell::new(None), _hard_allocation: Cell::new(None),
})) }))
} }
pub fn listener( pub fn listener(
self, self,
event: &'static str, event: &'static str,
callback: BumpBox<'a, dyn FnMut(Box<dyn Any + Send>) + 'a>, callback: BumpBox<'a, dyn FnMut(Arc<dyn Any + Send + Sync>) + 'a>,
) -> Listener<'a> { ) -> Listener<'a> {
Listener { Listener {
mounted_node: Cell::new(None), mounted_node: Cell::new(None),
@@ -664,9 +643,9 @@ impl<'a> NodeFactory<'a> {
} }
} }
pub fn fragment_from_iter( pub fn fragment_from_iter<'b, 'c>(
self, self,
node_iter: impl IntoIterator<Item = impl IntoVNode<'a>>, node_iter: impl IntoIterator<Item = impl IntoVNode<'a> + 'c> + 'b,
) -> VNode<'a> { ) -> VNode<'a> {
let bump = self.bump; let bump = self.bump;
let mut nodes = bumpalo::collections::Vec::new_in(bump); let mut nodes = bumpalo::collections::Vec::new_in(bump);
@@ -683,24 +662,25 @@ impl<'a> NodeFactory<'a> {
let children = nodes.into_bump_slice(); let children = nodes.into_bump_slice();
// TODO // todo: add a backtrace
// We need a dedicated path in the rsx! macro that will trigger the "you need keys" warning if cfg!(debug_assertions) && children.len() > 1 && children.last().unwrap().key().is_none()
// {
// if cfg!(debug_assertions) { // todo: rsx! calls get turned into fragments which means they always trip this error
// if children.len() > 1 { //
// if children.last().unwrap().key().is_none() { //
// log::error!( // use backtrace::Backtrace;
// r#" // let bt = Backtrace::new();
// Warning: Each child in an array or iterator should have a unique "key" prop.
// Not providing a key will lead to poor performance with lists. log::error!(
// See docs.rs/dioxus for more information. r#"
// --- Warning: Each child in an array or iterator should have a unique "key" prop.
// To help you identify where this error is coming from, we've generated a backtrace. Not providing a key will lead to poor performance with lists.
// "#, See docs.rs/dioxus for more information.
// ); -------------
// } "#,
// } // bt
// } );
}
VNode::Fragment(VFragment { VNode::Fragment(VFragment {
children, children,
@@ -782,7 +762,7 @@ impl Debug for NodeFactory<'_> {
/// ///
/// All dynamic content in the macros must flow in through `fragment_from_iter`. Everything else must be statically layed out. /// All dynamic content in the macros must flow in through `fragment_from_iter`. Everything else must be statically layed out.
/// We pipe basically everything through `fragment_from_iter`, so we expect a very specific type: /// We pipe basically everything through `fragment_from_iter`, so we expect a very specific type:
/// ``` /// ```rust, ignore
/// impl IntoIterator<Item = impl IntoVNode<'a>> /// impl IntoIterator<Item = impl IntoVNode<'a>>
/// ``` /// ```
/// ///
@@ -841,7 +821,7 @@ impl<'a> IntoVNode<'a> for Option<LazyNodes<'a, '_>> {
} }
} }
impl<'a> IntoVNode<'a> for LazyNodes<'a, '_> { impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> {
fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> {
self.call(cx) self.call(cx)
} }
+171 -180
View File
@@ -43,12 +43,8 @@ pub type Context<'a> = &'a Scope;
/// We expose the `Scope` type so downstream users can traverse the Dioxus VirtualDOM for whatever /// We expose the `Scope` type so downstream users can traverse the Dioxus VirtualDOM for whatever
/// use case they might have. /// use case they might have.
pub struct Scope { pub struct Scope {
// safety:
//
// pointers to scopes are *always* valid since they are bump allocated and never freed until this scope is also freed
// this is just a bit of a hack to not need an Rc to the ScopeArena.
// todo: replace this will ScopeId and provide a connection to scope arena directly
pub(crate) parent_scope: Option<*mut Scope>, pub(crate) parent_scope: Option<*mut Scope>,
pub(crate) container: ElementId,
pub(crate) our_arena_idx: ScopeId, pub(crate) our_arena_idx: ScopeId,
@@ -60,23 +56,14 @@ pub struct Scope {
pub(crate) generation: Cell<u32>, pub(crate) generation: Cell<u32>,
// The double-buffering situation that we will use
pub(crate) frames: [BumpFrame; 2], pub(crate) frames: [BumpFrame; 2],
pub(crate) caller: *const dyn Fn(&Scope) -> Element, pub(crate) caller: *const dyn Fn(&Scope) -> Element,
/*
we care about:
- listeners (and how to call them when an event is triggered)
- borrowed props (and how to drop them when the parent is dropped)
- suspended nodes (and how to call their callback when their associated tasks are complete)
*/
pub(crate) items: RefCell<SelfReferentialItems<'static>>, pub(crate) items: RefCell<SelfReferentialItems<'static>>,
// State
pub(crate) hooks: HookList, pub(crate) hooks: HookList,
// todo: move this into a centralized place - is more memory efficient
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>, pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
pub(crate) sender: UnboundedSender<SchedulerMsg>, pub(crate) sender: UnboundedSender<SchedulerMsg>,
@@ -85,7 +72,7 @@ pub struct Scope {
pub struct SelfReferentialItems<'a> { pub struct SelfReferentialItems<'a> {
pub(crate) listeners: Vec<&'a Listener<'a>>, pub(crate) listeners: Vec<&'a Listener<'a>>,
pub(crate) borrowed_props: Vec<&'a VComponent<'a>>, pub(crate) borrowed_props: Vec<&'a VComponent<'a>>,
pub(crate) suspended_nodes: FxHashMap<u64, &'a VSuspended<'a>>, pub(crate) suspended_nodes: FxHashMap<u64, &'a VSuspended>,
pub(crate) tasks: Vec<BumpBox<'a, dyn Future<Output = ()>>>, pub(crate) tasks: Vec<BumpBox<'a, dyn Future<Output = ()>>>,
pub(crate) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>, pub(crate) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>,
} }
@@ -108,7 +95,7 @@ impl Scope {
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild(); /// dom.rebuild();
/// ///
@@ -120,13 +107,39 @@ impl Scope {
self.subtree.get() self.subtree.get()
} }
/// Create a new subtree with this scope as the root of the subtree.
///
/// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
/// the mutations to the correct window/portal/subtree.
///
/// This method
///
/// # Example
///
/// ```rust, ignore
/// fn App(cx: Context, props: &()) -> Element {
/// todo!();
/// rsx!(cx, div { "Subtree {id}"})
/// };
/// ```
pub fn create_subtree(&self) -> Option<u32> {
if self.is_subtree_root.get() {
None
} else {
todo!()
// let cur = self.subtree().get();
// self.shared.cur_subtree.set(cur + 1);
// Some(cur)
}
}
/// Get the height of this Scope - IE the number of scopes above it. /// Get the height of this Scope - IE the number of scopes above it.
/// ///
/// A Scope with a height of `0` is the root scope - there are no other scopes above it. /// A Scope with a height of `0` is the root scope - there are no other scopes above it.
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild(); /// dom.rebuild();
/// ///
@@ -146,7 +159,7 @@ impl Scope {
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild(); /// dom.rebuild();
/// ///
@@ -155,10 +168,7 @@ impl Scope {
/// assert_eq!(base.parent(), None); /// assert_eq!(base.parent(), None);
/// ``` /// ```
pub fn parent(&self) -> Option<ScopeId> { pub fn parent(&self) -> Option<ScopeId> {
match self.parent_scope { self.parent_scope.map(|p| unsafe { &*p }.our_arena_idx)
Some(p) => Some(unsafe { &*p }.our_arena_idx),
None => None,
}
} }
/// Get the ID of this Scope within this Dioxus VirtualDOM. /// Get the ID of this Scope within this Dioxus VirtualDOM.
@@ -167,7 +177,7 @@ impl Scope {
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild(); /// dom.rebuild();
/// let base = dom.base_scope(); /// let base = dom.base_scope();
@@ -186,6 +196,7 @@ impl Scope {
let chan = self.sender.clone(); let chan = self.sender.clone();
let id = self.scope_id(); let id = self.scope_id();
Rc::new(move || { Rc::new(move || {
log::debug!("set on channel an update for scope {:?}", id);
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id)); let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
}) })
} }
@@ -223,6 +234,104 @@ impl Scope {
&self.wip_frame().bump &self.wip_frame().bump
} }
/// This method enables the ability to expose state to children further down the VirtualDOM Tree.
///
/// This is a "fundamental" operation and should only be called during initialization of a hook.
///
/// For a hook that provides the same functionality, use `use_provide_state` and `use_consume_state` instead.
///
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
/// the context via Rc/Weak.
///
/// # Example
///
/// ```rust, ignore
/// struct SharedState(&'static str);
///
/// static App: FC<()> = |cx, props|{
/// cx.use_hook(|_| cx.provide_state(SharedState("world")), |_| {}, |_| {});
/// rsx!(cx, Child {})
/// }
///
/// static Child: FC<()> = |cx, props| {
/// let state = cx.consume_state::<SharedState>();
/// rsx!(cx, div { "hello {state.0}" })
/// }
/// ```
pub fn provide_state<T: 'static>(&self, value: T) {
self.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), Rc::new(value))
.map(|f| f.downcast::<T>().ok())
.flatten();
}
/// Try to retrieve a SharedState with type T from the any parent Scope.
pub fn consume_state<T: 'static>(&self) -> Option<Rc<T>> {
if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
Some(shared.clone().downcast::<T>().unwrap())
} else {
let mut search_parent = self.parent_scope;
while let Some(parent_ptr) = search_parent {
let parent = unsafe { &*parent_ptr };
if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
return Some(shared.clone().downcast::<T>().unwrap());
}
search_parent = parent.parent_scope;
}
None
}
}
/// Push an effect to be ran after the component has been successfully mounted to the dom
/// Returns the effect's position in the stack
pub fn push_effect<'src>(&'src self, effect: impl FnOnce() + 'src) -> usize {
// this is some tricker to get around not being able to actually call fnonces
let mut slot = Some(effect);
let fut: &mut dyn FnMut() = self.bump().alloc(move || slot.take().unwrap()());
// wrap it in a type that will actually drop the contents
let boxed_fut = unsafe { BumpBox::from_raw(fut) };
// erase the 'src lifetime for self-referential storage
let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
let mut items = self.items.borrow_mut();
items.pending_effects.push(self_ref_fut);
items.pending_effects.len() - 1
}
/// Pushes the future onto the poll queue to be polled
/// The future is forcibly dropped if the component is not ready by the next render
pub fn push_task<'src, F: Future<Output = ()>>(
&'src self,
fut: impl FnOnce() -> F + 'src,
) -> usize
where
F::Output: 'src,
F: 'src,
{
self.sender
.unbounded_send(SchedulerMsg::TaskPushed(self.our_arena_idx))
.unwrap();
// allocate the future
let fut = fut();
let fut: &mut dyn Future<Output = ()> = self.bump().alloc(fut);
// wrap it in a type that will actually drop the contents
let boxed_fut: BumpBox<dyn Future<Output = ()>> = unsafe { BumpBox::from_raw(fut) };
// erase the 'src lifetime for self-referential storage
let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
let mut items = self.items.borrow_mut();
items.tasks.push(self_ref_fut);
items.tasks.len() - 1
}
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
/// ///
/// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned. /// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned.
@@ -257,132 +366,29 @@ impl Scope {
Some(link) Some(link)
} }
/// Push an effect to be ran after the component has been successfully mounted to the dom pub fn suspend<'src, F: Future<Output = Element> + 'src>(
/// Returns the effect's position in the stack &'src self,
pub fn push_effect<'src>(&'src self, effect: impl FnOnce() + 'src) -> usize { mut fut: impl FnMut() -> F,
// this is some tricker to get around not being able to actually call fnonces ) -> Option<VNode> {
let mut slot = Some(effect); let channel = self.sender.clone();
let fut: &mut dyn FnMut() = self.bump().alloc(move || slot.take().unwrap()()); let node_fut = fut();
// wrap it in a type that will actually drop the contents let scope = self.scope_id();
let boxed_fut = unsafe { BumpBox::from_raw(fut) };
// erase the 'src lifetime for self-referential storage // self.push_task(move || {
let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; //
// async move {
// //
// let r = node_fut.await;
// if let Some(node) = r {
// channel
// .unbounded_send(SchedulerMsg::Suspended { node, scope })
// .unwrap();
// }
// }
// });
let mut items = self.items.borrow_mut(); todo!()
items.pending_effects.push(self_ref_fut);
items.pending_effects.len() - 1
}
/// Pushes the future onto the poll queue to be polled
/// The future is forcibly dropped if the component is not ready by the next render
pub fn push_task<'src>(&'src self, fut: impl Future<Output = ()> + 'src) -> usize {
// allocate the future
let fut: &mut dyn Future<Output = ()> = self.bump().alloc(fut);
// wrap it in a type that will actually drop the contents
let boxed_fut: BumpBox<dyn Future<Output = ()>> = unsafe { BumpBox::from_raw(fut) };
// erase the 'src lifetime for self-referential storage
let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
let mut items = self.items.borrow_mut();
items.tasks.push(self_ref_fut);
items.tasks.len() - 1
}
/// This method enables the ability to expose state to children further down the VirtualDOM Tree.
///
/// This is a "fundamental" operation and should only be called during initialization of a hook.
///
/// For a hook that provides the same functionality, use `use_provide_state` and `use_consume_state` instead.
///
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
/// the context via Rc/Weak.
///
/// # Example
///
/// ```
/// struct SharedState(&'static str);
///
/// static App: FC<()> = |cx, props|{
/// cx.use_hook(|_| cx.provide_state(SharedState("world")), |_| {}, |_| {});
/// rsx!(cx, Child {})
/// }
///
/// static Child: FC<()> = |cx, props|{
/// let state = cx.consume_state::<SharedState>();
/// rsx!(cx, div { "hello {state.0}" })
/// }
/// ```
pub fn provide_state<T: 'static>(&self, value: T) {
self.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), Rc::new(value))
.map(|f| f.downcast::<T>().ok())
.flatten();
}
/// Try to retrieve a SharedState with type T from the any parent Scope.
pub fn consume_state<T: 'static>(&self) -> Option<Rc<T>> {
if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
Some(shared.clone().downcast::<T>().unwrap())
} else {
let mut search_parent = self.parent_scope;
while let Some(parent_ptr) = search_parent {
let parent = unsafe { &*parent_ptr };
if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
return Some(shared.clone().downcast::<T>().unwrap());
}
search_parent = parent.parent_scope;
}
None
}
}
/// Create a new subtree with this scope as the root of the subtree.
///
/// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
/// the mutations to the correct window/portal/subtree.
///
/// This method
///
/// # Example
///
/// ```rust
/// fn App(cx: Context, props: &()) -> Element {
/// todo!();
/// rsx!(cx, div { "Subtree {id}"})
/// };
/// ```
pub fn create_subtree(&self) -> Option<u32> {
if self.is_subtree_root.get() {
None
} else {
todo!()
// let cur = self.subtree().get();
// self.shared.cur_subtree.set(cur + 1);
// Some(cur)
}
}
/// Get the subtree ID that this scope belongs to.
///
/// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
/// the mutations to the correct window/portal/subtree.
///
/// # Example
///
/// ```rust
/// fn App(cx: Context, props: &()) -> Element {
/// let id = cx.get_current_subtree();
/// rsx!(cx, div { "Subtree {id}"})
/// };
/// ```
pub fn get_current_subtree(&self) -> u32 {
self.subtree()
} }
/// Store a value between renders /// Store a value between renders
@@ -437,6 +443,13 @@ impl Scope {
} }
} }
pub(crate) fn wip_frame_mut(&mut self) -> &mut BumpFrame {
match self.generation.get() & 1 == 0 {
true => &mut self.frames[0],
false => &mut self.frames[1],
}
}
pub(crate) fn fin_frame(&self) -> &BumpFrame { pub(crate) fn fin_frame(&self) -> &BumpFrame {
match self.generation.get() & 1 == 1 { match self.generation.get() & 1 == 1 {
true => &self.frames[0], true => &self.frames[0],
@@ -444,51 +457,29 @@ impl Scope {
} }
} }
pub unsafe fn reset_wip_frame(&self) { /// Reset this component's frame
///
/// # Safety:
/// This method breaks every reference of VNodes in the current frame.
pub(crate) unsafe fn reset_wip_frame(&mut self) {
// todo: unsafecell or something // todo: unsafecell or something
let bump = self.wip_frame() as *const _ as *mut Bump; let bump = self.wip_frame_mut();
let g = &mut *bump; bump.bump.reset();
g.reset();
} }
pub fn cycle_frame(&self) { pub(crate) fn cycle_frame(&self) {
self.generation.set(self.generation.get() + 1); self.generation.set(self.generation.get() + 1);
} }
/// A safe wrapper around calling listeners
pub(crate) fn call_listener(&self, event: UserEvent, element: ElementId) {
let listners = &mut self.items.borrow_mut().listeners;
let listener = listners.iter().find(|lis| {
let search = lis;
if search.event == event.name {
let search_id = search.mounted_node.get();
search_id.map(|f| f == element).unwrap_or(false)
} else {
false
}
});
if let Some(listener) = listener {
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
(cb)(event.event);
}
} else {
log::warn!("An event was triggered but there was no listener to handle it");
}
}
// General strategy here is to load up the appropriate suspended task and then run it. // General strategy here is to load up the appropriate suspended task and then run it.
// Suspended nodes cannot be called repeatedly. // Suspended nodes cannot be called repeatedly.
pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) { pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) {
let mut nodes = &mut self.items.get_mut().suspended_nodes; let mut nodes = &mut self.items.get_mut().suspended_nodes;
if let Some(suspended) = nodes.remove(&task_id) { if let Some(suspended) = nodes.remove(&task_id) {
let sus: &'a VSuspended<'static> = unsafe { &*suspended }; let sus: &'a VSuspended = suspended;
let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) }; // let mut boxed = sus.callback.borrow_mut().take().unwrap();
let mut boxed = sus.callback.borrow_mut().take().unwrap(); // let new_node: Element = boxed();
let new_node: Element = boxed();
} }
} }
@@ -499,7 +490,7 @@ impl Scope {
} }
} }
pub fn root_node<'a>(&'a self) -> &'a VNode<'a> { pub fn root_node(&self) -> &VNode {
let node = *self.wip_frame().nodes.borrow().get(0).unwrap(); let node = *self.wip_frame().nodes.borrow().get(0).unwrap();
unsafe { std::mem::transmute(&*node) } unsafe { std::mem::transmute(&*node) }
} }
+88 -35
View File
@@ -1,8 +1,11 @@
use bumpalo::Bump; use bumpalo::Bump;
use futures_channel::mpsc::UnboundedSender; use futures_channel::mpsc::UnboundedSender;
use fxhash::FxHashMap; use fxhash::{FxHashMap, FxHashSet};
use slab::Slab; use slab::Slab;
use std::cell::{Cell, RefCell}; use std::{
borrow::Borrow,
cell::{Cell, RefCell},
};
use crate::innerlude::*; use crate::innerlude::*;
@@ -19,8 +22,9 @@ pub struct Heuristic {
// has an internal heuristics engine to pre-allocate arenas to the right size // has an internal heuristics engine to pre-allocate arenas to the right size
pub(crate) struct ScopeArena { pub(crate) struct ScopeArena {
bump: Bump, bump: Bump,
pub pending_futures: RefCell<FxHashSet<ScopeId>>,
scope_counter: Cell<usize>, scope_counter: Cell<usize>,
scopes: RefCell<FxHashMap<ScopeId, *mut Scope>>, pub scopes: RefCell<FxHashMap<ScopeId, *mut Scope>>,
pub heuristics: RefCell<FxHashMap<FcSlot, Heuristic>>, pub heuristics: RefCell<FxHashMap<FcSlot, Heuristic>>,
free_scopes: RefCell<Vec<*mut Scope>>, free_scopes: RefCell<Vec<*mut Scope>>,
nodes: RefCell<Slab<*const VNode<'static>>>, nodes: RefCell<Slab<*const VNode<'static>>>,
@@ -28,45 +32,68 @@ pub(crate) struct ScopeArena {
} }
impl ScopeArena { impl ScopeArena {
pub fn new(sender: UnboundedSender<SchedulerMsg>) -> Self { pub(crate) fn new(sender: UnboundedSender<SchedulerMsg>) -> Self {
let bump = Bump::new();
// allocate a container for the root element
// this will *never* show up in the diffing process
let el = bump.alloc(VElement {
tag_name: "root",
namespace: None,
key: None,
dom_id: Cell::new(Some(ElementId(0))),
parent_id: Default::default(),
listeners: &[],
attributes: &[],
children: &[],
});
let node = bump.alloc(VNode::Element(el));
let mut nodes = Slab::new();
let root_id = nodes.insert(unsafe { std::mem::transmute(node as *const _) });
debug_assert_eq!(root_id, 0);
Self { Self {
scope_counter: Cell::new(0), scope_counter: Cell::new(0),
bump: Bump::new(), bump,
pending_futures: RefCell::new(FxHashSet::default()),
scopes: RefCell::new(FxHashMap::default()), scopes: RefCell::new(FxHashMap::default()),
heuristics: RefCell::new(FxHashMap::default()), heuristics: RefCell::new(FxHashMap::default()),
free_scopes: RefCell::new(Vec::new()), free_scopes: RefCell::new(Vec::new()),
nodes: RefCell::new(Slab::new()), nodes: RefCell::new(nodes),
sender, sender,
} }
} }
pub fn get_scope(&self, id: &ScopeId) -> Option<&Scope> { /// Safety:
/// - Obtaining a mutable refernece to any Scope is unsafe
/// - Scopes use interior mutability when sharing data into components
pub(crate) fn get_scope(&self, id: &ScopeId) -> Option<&Scope> {
unsafe { self.scopes.borrow().get(id).map(|f| &**f) } unsafe { self.scopes.borrow().get(id).map(|f| &**f) }
} }
// this is unsafe pub(crate) unsafe fn get_scope_raw(&self, id: &ScopeId) -> Option<*mut Scope> {
pub unsafe fn get_scope_raw(&self, id: &ScopeId) -> Option<*mut Scope> { self.scopes.borrow().get(id).copied()
self.scopes.borrow().get(id).map(|f| *f)
} }
// this is unsafe
pub unsafe fn get_scope_mut(&self, id: &ScopeId) -> Option<&mut Scope> { pub(crate) unsafe fn get_scope_mut(&self, id: &ScopeId) -> Option<&mut Scope> {
self.scopes.borrow().get(id).map(|s| &mut **s) self.scopes.borrow().get(id).map(|s| &mut **s)
} }
pub fn new_with_key( pub(crate) fn new_with_key(
&self, &self,
fc_ptr: *const (), fc_ptr: *const (),
caller: *const dyn Fn(&Scope) -> Element, caller: *const dyn Fn(&Scope) -> Element,
parent_scope: Option<*mut Scope>, parent_scope: Option<*mut Scope>,
container: ElementId,
height: u32, height: u32,
subtree: u32, subtree: u32,
) -> ScopeId { ) -> ScopeId {
let new_scope_id = ScopeId(self.scope_counter.get()); let new_scope_id = ScopeId(self.scope_counter.get());
self.scope_counter.set(self.scope_counter.get() + 1); self.scope_counter.set(self.scope_counter.get() + 1);
// log::debug!("new scope {:?} with parent {:?}", new_scope_id, container);
//
if let Some(old_scope) = self.free_scopes.borrow_mut().pop() { if let Some(old_scope) = self.free_scopes.borrow_mut().pop() {
let scope = unsafe { &mut *old_scope }; let scope = unsafe { &mut *old_scope };
log::debug!( log::debug!(
@@ -80,6 +107,7 @@ impl ScopeArena {
scope.height = height; scope.height = height;
scope.subtree = Cell::new(subtree); scope.subtree = Cell::new(subtree);
scope.our_arena_idx = new_scope_id; scope.our_arena_idx = new_scope_id;
scope.container = container;
scope.frames[0].nodes.get_mut().push({ scope.frames[0].nodes.get_mut().push({
let vnode = scope.frames[0] let vnode = scope.frames[0]
@@ -103,11 +131,8 @@ impl ScopeArena {
unsafe { std::mem::transmute(vnode as *mut VNode) } unsafe { std::mem::transmute(vnode as *mut VNode) }
}); });
let r = self.scopes.borrow_mut().insert(new_scope_id, scope); let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope);
debug_assert!(any_item.is_none());
assert!(r.is_none());
new_scope_id
} else { } else {
let (node_capacity, hook_capacity) = { let (node_capacity, hook_capacity) = {
let heuristics = self.heuristics.borrow(); let heuristics = self.heuristics.borrow();
@@ -144,6 +169,7 @@ impl ScopeArena {
let scope = self.bump.alloc(Scope { let scope = self.bump.alloc(Scope {
sender: self.sender.clone(), sender: self.sender.clone(),
container,
our_arena_idx: new_scope_id, our_arena_idx: new_scope_id,
parent_scope, parent_scope,
height, height,
@@ -166,24 +192,22 @@ impl ScopeArena {
}), }),
}); });
dbg!(self.scopes.borrow()); let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope);
debug_assert!(any_item.is_none());
let r = self.scopes.borrow_mut().insert(new_scope_id, scope);
assert!(r.is_none());
// .expect(&format!("scope shouldnt exist, {:?}", new_scope_id));
new_scope_id
} }
new_scope_id
} }
pub fn try_remove(&self, id: &ScopeId) -> Option<()> { pub fn try_remove(&self, id: &ScopeId) -> Option<()> {
self.ensure_drop_safety(id); self.ensure_drop_safety(id);
log::debug!("removing scope {:?}", id); log::debug!("removing scope {:?}", id);
println!("removing scope {:?}", id);
let scope = unsafe { &mut *self.scopes.borrow_mut().remove(&id).unwrap() }; // Safety:
// - ensure_drop_safety ensures that no references to this scope are in use
// - this raw pointer is removed from the map
let scope = unsafe { &mut *self.scopes.borrow_mut().remove(id).unwrap() };
// we're just reusing scopes so we need to clear it out // we're just reusing scopes so we need to clear it out
scope.hooks.clear(); scope.hooks.clear();
@@ -229,6 +253,11 @@ impl ScopeArena {
id id
} }
pub fn update_node(&self, node: &VNode, id: ElementId) {
let node = unsafe { std::mem::transmute::<*const VNode, *const VNode>(node) };
*self.nodes.borrow_mut().get_mut(id.0).unwrap() = node;
}
pub fn collect_garbage(&self, id: ElementId) { pub fn collect_garbage(&self, id: ElementId) {
self.nodes.borrow_mut().remove(id.0); self.nodes.borrow_mut().remove(id.0);
} }
@@ -275,15 +304,15 @@ impl ScopeArena {
} }
pub(crate) fn run_scope(&self, id: &ScopeId) -> bool { pub(crate) fn run_scope(&self, id: &ScopeId) -> bool {
let scope = unsafe { &mut *self.get_scope_mut(id).expect("could not find scope") };
log::debug!("found scope, about to run: {:?}", id);
// Cycle to the next frame and then reset it // Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it. // This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners // Remove all the outdated listeners
self.ensure_drop_safety(id); self.ensure_drop_safety(id);
let scope = unsafe { &mut *self.get_scope_mut(id).expect("could not find scope") };
log::debug!("found scope, about to run: {:?}", id);
// Safety: // Safety:
// - We dropped the listeners, so no more &mut T can be used while these are held // - We dropped the listeners, so no more &mut T can be used while these are held
// - All children nodes that rely on &mut T are replaced with a new reference // - All children nodes that rely on &mut T are replaced with a new reference
@@ -325,7 +354,7 @@ impl ScopeArena {
debug_assert_eq!(scope.wip_frame().nodes.borrow().len(), 1); debug_assert_eq!(scope.wip_frame().nodes.borrow().len(), 1);
if !scope.items.borrow().tasks.is_empty() { if !scope.items.borrow().tasks.is_empty() {
// self. self.pending_futures.borrow_mut().insert(*id);
} }
// make the "wip frame" contents the "finished frame" // make the "wip frame" contents the "finished frame"
@@ -337,6 +366,30 @@ impl ScopeArena {
} }
} }
pub fn call_listener_with_bubbling(&self, event: UserEvent, element: ElementId) {
let nodes = self.nodes.borrow();
let mut cur_el = Some(element);
while let Some(id) = cur_el.take() {
if let Some(el) = nodes.get(id.0) {
let real_el = unsafe { &**el };
if let VNode::Element(real_el) = real_el {
//
for listener in real_el.listeners.borrow().iter() {
if listener.event == event.name {
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
(cb)(event.data.clone());
}
}
}
cur_el = real_el.parent_id.get();
}
}
}
}
// The head of the bumpframe is the first linked NodeLink // The head of the bumpframe is the first linked NodeLink
pub fn wip_head(&self, id: &ScopeId) -> &VNode { pub fn wip_head(&self, id: &ScopeId) -> &VNode {
let scope = self.get_scope(id).unwrap(); let scope = self.get_scope(id).unwrap();
+174 -161
View File
@@ -7,7 +7,9 @@ use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures_util::{Future, StreamExt}; use futures_util::{Future, StreamExt};
use fxhash::FxHashSet; use fxhash::FxHashSet;
use indexmap::IndexSet; use indexmap::IndexSet;
use smallvec::SmallVec;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll; use std::task::Poll;
use std::{any::Any, collections::VecDeque}; use std::{any::Any, collections::VecDeque};
@@ -18,7 +20,7 @@ use std::{any::Any, collections::VecDeque};
/// ///
/// Components are defined as simple functions that take [`Context`] and a [`Properties`] type and return an [`Element`]. /// Components are defined as simple functions that take [`Context`] and a [`Properties`] type and return an [`Element`].
/// ///
/// ```rust /// ```rust, ignore
/// #[derive(Props, PartialEq)] /// #[derive(Props, PartialEq)]
/// struct AppProps { /// struct AppProps {
/// title: String /// title: String
@@ -33,7 +35,7 @@ use std::{any::Any, collections::VecDeque};
/// ///
/// Components may be composed to make complex apps. /// Components may be composed to make complex apps.
/// ///
/// ```rust /// ```rust, ignore
/// fn App(cx: Context, props: &AppProps) -> Element { /// fn App(cx: Context, props: &AppProps) -> Element {
/// cx.render(rsx!( /// cx.render(rsx!(
/// NavBar { routes: ROUTES } /// NavBar { routes: ROUTES }
@@ -46,14 +48,14 @@ use std::{any::Any, collections::VecDeque};
/// To start an app, create a [`VirtualDom`] and call [`VirtualDom::rebuild`] to get the list of edits required to /// To start an app, create a [`VirtualDom`] and call [`VirtualDom::rebuild`] to get the list of edits required to
/// draw the UI. /// draw the UI.
/// ///
/// ```rust /// ```rust, ignore
/// let mut vdom = VirtualDom::new(App); /// let mut vdom = VirtualDom::new(App);
/// let edits = vdom.rebuild(); /// let edits = vdom.rebuild();
/// ``` /// ```
/// ///
/// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler. /// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler.
/// ///
/// ```rust /// ```rust, ignore
/// let channel = vdom.get_scheduler_channel(); /// let channel = vdom.get_scheduler_channel();
/// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent { /// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent {
/// // ... /// // ...
@@ -62,7 +64,7 @@ use std::{any::Any, collections::VecDeque};
/// ///
/// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom. /// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom.
/// ///
/// ```rust /// ```rust, ignore
/// vdom.wait_for_work().await; /// vdom.wait_for_work().await;
/// ``` /// ```
/// ///
@@ -70,7 +72,7 @@ use std::{any::Any, collections::VecDeque};
/// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be /// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be
/// handled by the renderer. /// handled by the renderer.
/// ///
/// ```rust /// ```rust, ignore
/// let mutations = vdom.work_with_deadline(|| false); /// let mutations = vdom.work_with_deadline(|| false);
/// for edit in mutations { /// for edit in mutations {
/// apply(edit); /// apply(edit);
@@ -81,7 +83,7 @@ use std::{any::Any, collections::VecDeque};
/// ///
/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above. /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
/// ///
/// ```rust /// ```rust, ignore
/// fn App(cx: Context, props: &()) -> Element { /// fn App(cx: Context, props: &()) -> Element {
/// cx.render(rsx!{ /// cx.render(rsx!{
/// div { "Hello World" } /// div { "Hello World" }
@@ -106,7 +108,7 @@ use std::{any::Any, collections::VecDeque};
pub struct VirtualDom { pub struct VirtualDom {
base_scope: ScopeId, base_scope: ScopeId,
_root_caller: *mut dyn Fn(&Scope) -> Element, _root_props: Box<dyn Any>,
scopes: Box<ScopeArena>, scopes: Box<ScopeArena>,
@@ -114,8 +116,6 @@ pub struct VirtualDom {
sender: UnboundedSender<SchedulerMsg>, sender: UnboundedSender<SchedulerMsg>,
pending_futures: FxHashSet<ScopeId>,
pending_messages: VecDeque<SchedulerMsg>, pending_messages: VecDeque<SchedulerMsg>,
dirty_scopes: IndexSet<ScopeId>, dirty_scopes: IndexSet<ScopeId>,
@@ -134,7 +134,7 @@ impl VirtualDom {
/// ///
/// ///
/// # Example /// # Example
/// ``` /// ```rust, ignore
/// fn Example(cx: Context, props: &()) -> Element { /// fn Example(cx: Context, props: &()) -> Element {
/// cx.render(rsx!( div { "hello world" } )) /// cx.render(rsx!( div { "hello world" } ))
/// } /// }
@@ -158,7 +158,7 @@ impl VirtualDom {
/// ///
/// ///
/// # Example /// # Example
/// ``` /// ```rust, ignore
/// #[derive(PartialEq, Props)] /// #[derive(PartialEq, Props)]
/// struct SomeProps { /// struct SomeProps {
/// name: &'static str /// name: &'static str
@@ -173,7 +173,7 @@ impl VirtualDom {
/// ///
/// Note: the VirtualDOM is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it. /// Note: the VirtualDOM is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it.
/// ///
/// ```rust /// ```rust, ignore
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" }); /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
/// let mutations = dom.rebuild(); /// let mutations = dom.rebuild();
/// ``` /// ```
@@ -194,9 +194,9 @@ impl VirtualDom {
) -> Self { ) -> Self {
let scopes = ScopeArena::new(sender.clone()); let scopes = ScopeArena::new(sender.clone());
let caller = Box::new(move |scp: &Scope| -> Element { root(scp, &root_props) }); let mut caller = Box::new(move |scp: &Scope| -> Element { root(scp, &root_props) });
let caller_ref: *mut dyn Fn(&Scope) -> Element = Box::into_raw(caller); let caller_ref: *mut dyn Fn(&Scope) -> Element = caller.as_mut() as *mut _;
let base_scope = scopes.new_with_key(root as _, caller_ref, None, 0, 0); let base_scope = scopes.new_with_key(root as _, caller_ref, None, ElementId(0), 0, 0);
let pending_messages = VecDeque::new(); let pending_messages = VecDeque::new();
let mut dirty_scopes = IndexSet::new(); let mut dirty_scopes = IndexSet::new();
@@ -206,10 +206,8 @@ impl VirtualDom {
scopes: Box::new(scopes), scopes: Box::new(scopes),
base_scope, base_scope,
receiver, receiver,
// todo: clean this up manually? _root_props: caller,
_root_caller: caller_ref,
pending_messages, pending_messages,
pending_futures: Default::default(),
dirty_scopes, dirty_scopes,
sender, sender,
} }
@@ -239,7 +237,7 @@ impl VirtualDom {
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// ///
/// ///
/// ``` /// ```
@@ -251,7 +249,7 @@ impl VirtualDom {
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust, ignore
/// ///
/// ///
/// ``` /// ```
@@ -262,104 +260,79 @@ impl VirtualDom {
/// Waits for the scheduler to have work /// Waits for the scheduler to have work
/// This lets us poll async tasks during idle periods without blocking the main thread. /// This lets us poll async tasks during idle periods without blocking the main thread.
pub async fn wait_for_work(&mut self) { pub async fn wait_for_work(&mut self) {
// todo: poll the events once even if there is work to do to prevent starvation loop {
if !self.dirty_scopes.is_empty() && self.pending_messages.is_empty() {
// if there's no futures in the virtualdom, just wait for a scheduler message and put it into the queue to be processed break;
if self.pending_futures.is_empty() {
self.pending_messages
.push_front(self.receiver.next().await.unwrap());
} else {
struct PollTasks<'a> {
pending_futures: &'a FxHashSet<ScopeId>,
scopes: &'a ScopeArena,
} }
impl<'a> Future for PollTasks<'a> { if self.pending_messages.is_empty() {
type Output = (); if self.scopes.pending_futures.borrow().is_empty() {
self.pending_messages
.push_front(self.receiver.next().await.unwrap());
} else {
use futures_util::future::{select, Either};
fn poll( match select(PollTasks(&mut self.scopes), self.receiver.next()).await {
self: Pin<&mut Self>, Either::Left((_, _)) => {}
cx: &mut std::task::Context<'_>, Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()),
) -> Poll<Self::Output> {
let mut all_pending = true;
// Poll every scope manually
for fut in self.pending_futures.iter() {
let scope = self
.scopes
.get_scope(fut)
.expect("Scope should never be moved");
let mut items = scope.items.borrow_mut();
for task in items.tasks.iter_mut() {
let task = task.as_mut();
// todo: does this make sense?
// I don't usually write futures by hand
// I think the futures neeed to be pinned using bumpbox or something
// right now, they're bump allocated so this shouldn't matter anyway - they're not going to move
let unpinned = unsafe { Pin::new_unchecked(task) };
if unpinned.poll(cx).is_ready() {
all_pending = false
}
}
}
// Resolve the future if any singular task is ready
match all_pending {
true => Poll::Pending,
false => Poll::Ready(()),
} }
} }
} }
// Poll both the futures and the scheduler message queue simulataneously while let Ok(Some(msg)) = self.receiver.try_next() {
use futures_util::future::{select, Either}; self.pending_messages.push_front(msg);
}
let scheduler_fut = self.receiver.next(); if let Some(msg) = self.pending_messages.pop_back() {
let tasks_fut = PollTasks { match msg {
pending_futures: &self.pending_futures, // just keep looping, the task is now saved but we should actually poll it
scopes: &self.scopes, SchedulerMsg::TaskPushed(id) => {
}; self.scopes.pending_futures.borrow_mut().insert(id);
}
match select(tasks_fut, scheduler_fut).await { SchedulerMsg::UiEvent(event) => {
// Futures don't generate work if let Some(element) = event.element {
Either::Left((_, _)) => {} self.scopes.call_listener_with_bubbling(event, element);
}
// Save these messages in FIFO to be processed later }
Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()), SchedulerMsg::Immediate(s) => {
self.dirty_scopes.insert(s);
}
SchedulerMsg::Suspended { scope } => todo!(),
}
} }
} }
} }
/// Run the virtualdom with a deadline. /// Run the virtualdom with a deadline.
/// ///
/// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline, /// This method will perform any outstanding diffing work and try to return as many mutations as possible before the
/// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will /// deadline is reached. This method accepts a closure that returns `true` if the deadline has been reached. To wrap
/// exhaust the deadline working on them. /// your future into a deadline, consider the `now_or_never` method from `future_utils`.
///
/// ```rust, ignore
/// let mut vdom = VirtualDom::new(App);
///
/// let timeout = TimeoutFuture::from_ms(16);
/// let deadline = || (&mut timeout).now_or_never();
///
/// let mutations = vdom.work_with_deadline(deadline);
/// ```
/// ///
/// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent /// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent
/// "jank". It will try to finish whatever work it has by the deadline to free up time for other work. /// "jank". It will try to finish whatever work it has by the deadline to free up time for other work.
/// ///
/// Due to platform differences in how time is handled, this method accepts a future that resolves when the deadline /// If the work is not finished by the deadline, Dioxus will store it for later and return when work_with_deadline
/// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the /// is called again. This means you can ensure some level of free time on the VirtualDom's thread during the work phase.
/// deadline closure manually.
///
/// The deadline is polled before starting to diff components. This strikes a balance between the overhead of checking
/// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then
/// the screen will "jank" up. In debug, this will trigger an alert.
///
/// If there are no in-flight fibers when this method is called, it will await any possible tasks, aborting early if
/// the provided deadline future resolves.
/// ///
/// For use in the web, it is expected that this method will be called to be executed during "idle times" and the /// For use in the web, it is expected that this method will be called to be executed during "idle times" and the
/// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft /// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft
/// entirely jank-free applications that perform a ton of work. /// entirely jank-free applications that perform a ton of work.
/// ///
/// In general use, Dioxus is plenty fast enough to not need to worry about this.
///
/// # Example /// # Example
/// ///
/// ```no_run /// ```rust, ignore
/// fn App(cx: Context, props: &()) -> Element { /// fn App(cx: Context, props: &()) -> Element {
/// cx.render(rsx!( div {"hello"} )) /// cx.render(rsx!( div {"hello"} ))
/// } /// }
@@ -375,58 +348,19 @@ impl VirtualDom {
/// apply_mutations(mutations); /// apply_mutations(mutations);
/// } /// }
/// ``` /// ```
///
/// ## Mutations
///
/// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also
/// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has
/// applied the edits.
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
///
pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec<Mutations> { pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec<Mutations> {
let mut committed_mutations = vec![]; let mut committed_mutations = vec![];
while self.has_any_work() { while !self.dirty_scopes.is_empty() {
while let Ok(Some(msg)) = self.receiver.try_next() { log::debug!("working with deadline");
self.pending_messages.push_front(msg);
}
while let Some(msg) = self.pending_messages.pop_back() {
match msg {
SchedulerMsg::Immediate(id) => {
self.dirty_scopes.insert(id);
}
SchedulerMsg::UiEvent(event) => {
if let Some(element) = event.mounted_dom_id {
log::info!("Calling listener {:?}, {:?}", event.scope_id, element);
if let Some(scope) = self.scopes.get_scope(&event.scope_id) {
// TODO: bubble properly here
scope.call_listener(event, element);
while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
self.pending_messages.push_front(dirty_scope);
}
}
} else {
log::debug!("User event without a targetted ElementId. Not currently supported.\nUnsure how to proceed. {:?}", event);
}
}
}
}
let scopes = &self.scopes; let scopes = &self.scopes;
let mut diff_state = DiffState::new(scopes); let mut diff_state = DiffState::new(scopes);
let mut ran_scopes = FxHashSet::default(); let mut ran_scopes = FxHashSet::default();
// todo: the 2021 version of rust will let us not have to force the borrow
// let scopes = &self.scopes;
// Sort the scopes by height. Theoretically, we'll de-duplicate scopes by height // Sort the scopes by height. Theoretically, we'll de-duplicate scopes by height
self.dirty_scopes self.dirty_scopes
.retain(|id| scopes.get_scope(id).is_some()); .retain(|id| scopes.get_scope(id).is_some());
self.dirty_scopes.sort_by(|a, b| { self.dirty_scopes.sort_by(|a, b| {
let h1 = scopes.get_scope(a).unwrap().height; let h1 = scopes.get_scope(a).unwrap().height;
let h2 = scopes.get_scope(b).unwrap().height; let h2 = scopes.get_scope(b).unwrap().height;
@@ -434,31 +368,27 @@ impl VirtualDom {
}); });
if let Some(scopeid) = self.dirty_scopes.pop() { if let Some(scopeid) = self.dirty_scopes.pop() {
log::info!("handling dirty scope {:?}", scopeid);
if !ran_scopes.contains(&scopeid) { if !ran_scopes.contains(&scopeid) {
ran_scopes.insert(scopeid); ran_scopes.insert(scopeid);
log::debug!("about to run scope {:?}", scopeid);
if self.scopes.run_scope(&scopeid) { if self.scopes.run_scope(&scopeid) {
let (old, new) = ( let (old, new) = (
self.scopes.wip_head(&scopeid), self.scopes.wip_head(&scopeid),
self.scopes.fin_head(&scopeid), self.scopes.fin_head(&scopeid),
); );
diff_state.stack.scope_stack.push(scopeid);
diff_state.stack.push(DiffInstruction::Diff { new, old }); diff_state.stack.push(DiffInstruction::Diff { new, old });
diff_state.stack.scope_stack.push(scopeid);
let scope = scopes.get_scope(&scopeid).unwrap();
diff_state.stack.element_stack.push(scope.container);
} }
} }
} }
let work_completed = diff_state.work(&mut deadline); if diff_state.work(&mut deadline) {
if work_completed {
let DiffState { let DiffState {
mutations, mutations,
seen_scopes, seen_scopes,
stack,
.. ..
} = diff_state; } = diff_state;
@@ -466,13 +396,9 @@ impl VirtualDom {
self.dirty_scopes.remove(&scope); self.dirty_scopes.remove(&scope);
} }
// // I think the stack should be empty at the end of diffing?
// debug_assert_eq!(stack.scope_stack.len(), 1);
committed_mutations.push(mutations); committed_mutations.push(mutations);
} else { } else {
// leave the work in an incomplete state // leave the work in an incomplete state
log::debug!("don't have a mechanism to pause work (yet)");
return committed_mutations; return committed_mutations;
} }
} }
@@ -490,7 +416,7 @@ impl VirtualDom {
/// All state stored in components will be completely wiped away. /// All state stored in components will be completely wiped away.
/// ///
/// # Example /// # Example
/// ``` /// ```rust, ignore
/// static App: FC<()> = |cx, props| cx.render(rsx!{ "hello world" }); /// static App: FC<()> = |cx, props| cx.render(rsx!{ "hello world" });
/// let mut dom = VirtualDom::new(); /// let mut dom = VirtualDom::new();
/// let edits = dom.rebuild(); /// let edits = dom.rebuild();
@@ -506,6 +432,7 @@ impl VirtualDom {
.stack .stack
.create_node(self.scopes.fin_head(&scope_id), MountType::Append); .create_node(self.scopes.fin_head(&scope_id), MountType::Append);
diff_state.stack.element_stack.push(ElementId(0));
diff_state.stack.scope_stack.push(scope_id); diff_state.stack.scope_stack.push(scope_id);
diff_state.work(|| false); diff_state.work(|| false);
@@ -521,9 +448,8 @@ impl VirtualDom {
/// In this case, every component will be diffed, even if their props are memoized. This method is intended to be used /// In this case, every component will be diffed, even if their props are memoized. This method is intended to be used
/// to force an update of the DOM when the state of the app is changed outside of the app. /// to force an update of the DOM when the state of the app is changed outside of the app.
/// ///
///
/// # Example /// # Example
/// ```rust /// ```rust, ignore
/// #[derive(PartialEq, Props)] /// #[derive(PartialEq, Props)]
/// struct AppProps { /// struct AppProps {
/// value: Shared<&'static str>, /// value: Shared<&'static str>,
@@ -535,12 +461,7 @@ impl VirtualDom {
/// }; /// };
/// ///
/// let value = Rc::new(RefCell::new("Hello")); /// let value = Rc::new(RefCell::new("Hello"));
/// let mut dom = VirtualDom::new_with_props( /// let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone(), });
/// App,
/// AppProps {
/// value: value.clone(),
/// },
/// );
/// ///
/// let _ = dom.rebuild(); /// let _ = dom.rebuild();
/// ///
@@ -574,6 +495,7 @@ impl VirtualDom {
pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> {
let mut machine = DiffState::new(&self.scopes); let mut machine = DiffState::new(&self.scopes);
machine.stack.push(DiffInstruction::Diff { new, old }); machine.stack.push(DiffInstruction::Diff { new, old });
machine.stack.element_stack.push(ElementId(0));
machine.stack.scope_stack.push(self.base_scope); machine.stack.scope_stack.push(self.base_scope);
machine.work(|| false); machine.work(|| false);
machine.mutations machine.mutations
@@ -585,6 +507,7 @@ impl VirtualDom {
pub fn create_vnodes<'a>(&'a self, left: Option<LazyNodes<'a, '_>>) -> Mutations<'a> { pub fn create_vnodes<'a>(&'a self, left: Option<LazyNodes<'a, '_>>) -> Mutations<'a> {
let nodes = self.render_vnodes(left); let nodes = self.render_vnodes(left);
let mut machine = DiffState::new(&self.scopes); let mut machine = DiffState::new(&self.scopes);
machine.stack.element_stack.push(ElementId(0));
machine.stack.create_node(nodes, MountType::Append); machine.stack.create_node(nodes, MountType::Append);
machine.work(|| false); machine.work(|| false);
machine.mutations machine.mutations
@@ -602,11 +525,13 @@ impl VirtualDom {
let mut create = DiffState::new(&self.scopes); let mut create = DiffState::new(&self.scopes);
create.stack.scope_stack.push(self.base_scope); create.stack.scope_stack.push(self.base_scope);
create.stack.element_stack.push(ElementId(0));
create.stack.create_node(old, MountType::Append); create.stack.create_node(old, MountType::Append);
create.work(|| false); create.work(|| false);
let mut edit = DiffState::new(&self.scopes); let mut edit = DiffState::new(&self.scopes);
create.stack.scope_stack.push(self.base_scope); edit.stack.scope_stack.push(self.base_scope);
edit.stack.element_stack.push(ElementId(0));
edit.stack.push(DiffInstruction::Diff { old, new }); edit.stack.push(DiffInstruction::Diff { old, new });
edit.work(&mut || false); edit.work(&mut || false);
@@ -620,17 +545,53 @@ pub enum SchedulerMsg {
// setstate // setstate
Immediate(ScopeId), Immediate(ScopeId),
// an async task pushed from an event handler (or just spawned)
TaskPushed(ScopeId),
Suspended { scope: ScopeId },
} }
/// User Events are events that are shuttled from the renderer into the VirtualDom trhough the scheduler channel.
///
/// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree
/// where each listener is checked and fired if the event name matches.
///
/// It is the expectation that the event name matches the corresponding event listener, otherwise Dioxus will panic in
/// attempting to downcast the event data.
///
/// Because Event Data is sent across threads, it must be `Send + Sync`. We are hoping to lift the `Sync` restriction but
/// `Send` will not be lifted. The entire `UserEvent` must also be `Send + Sync` due to its use in the scheduler channel.
///
/// # Example
/// ```rust
/// fn App(cx: Context, props: &()) -> Element {
/// rsx!(cx, div {
/// onclick: move |_| println!("Clicked!")
/// })
/// }
///
/// let mut dom = VirtualDom::new(App);
/// let mut scheduler = dom.get_scheduler_channel();
/// scheduler.unbounded_send(SchedulerMsg::UiEvent(
/// UserEvent {
/// scope_id: None,
/// priority: EventPriority::Medium,
/// name: "click",
/// element: Some(ElementId(0)),
/// data: Arc::new(ClickEvent { .. })
/// }
/// )).unwrap();
/// ```
#[derive(Debug)] #[derive(Debug)]
pub struct UserEvent { pub struct UserEvent {
/// The originator of the event trigger /// The originator of the event trigger
pub scope_id: ScopeId, pub scope_id: Option<ScopeId>,
pub priority: EventPriority, pub priority: EventPriority,
/// The optional real node associated with the trigger /// The optional real node associated with the trigger
pub mounted_dom_id: Option<ElementId>, pub element: Option<ElementId>,
/// The event type IE "onclick" or "onmouseover" /// The event type IE "onclick" or "onmouseover"
/// ///
@@ -638,7 +599,7 @@ pub struct UserEvent {
pub name: &'static str, pub name: &'static str,
/// Event Data /// Event Data
pub event: Box<dyn Any + Send>, pub data: Arc<dyn Any + Send + Sync>,
} }
/// Priority of Event Triggers. /// Priority of Event Triggers.
@@ -692,3 +653,55 @@ pub enum EventPriority {
/// This is considered "idle" work or "background" work. /// This is considered "idle" work or "background" work.
Low = 0, Low = 0,
} }
struct PollTasks<'a>(&'a mut ScopeArena);
impl<'a> Future for PollTasks<'a> {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
let mut all_pending = true;
let mut unfinished_tasks: SmallVec<[_; 10]> = smallvec::smallvec![];
let mut scopes_to_clear: SmallVec<[_; 10]> = smallvec::smallvec![];
// Poll every scope manually
for fut in self.0.pending_futures.borrow().iter() {
let scope = self.0.get_scope(fut).expect("Scope should never be moved");
let mut items = scope.items.borrow_mut();
// really this should just be retain_mut but that doesn't exist yet
while let Some(mut task) = items.tasks.pop() {
// todo: does this make sense?
// I don't usually write futures by hand
// I think the futures neeed to be pinned using bumpbox or something
// right now, they're bump allocated so this shouldn't matter anyway - they're not going to move
let task_mut = task.as_mut();
let unpinned = unsafe { Pin::new_unchecked(task_mut) };
if unpinned.poll(cx).is_ready() {
all_pending = false
} else {
unfinished_tasks.push(task);
}
}
if unfinished_tasks.is_empty() {
scopes_to_clear.push(*fut);
}
items.tasks.extend(unfinished_tasks.drain(..));
}
for scope in scopes_to_clear {
self.0.pending_futures.borrow_mut().remove(&scope);
}
// Resolve the future if any singular task is ready
match all_pending {
true => Poll::Pending,
false => Poll::Ready(()),
}
}
}
+47 -47
View File
@@ -36,16 +36,16 @@ fn test_original_diff() {
assert_eq!( assert_eq!(
mutations.edits, mutations.edits,
[ [
CreateElement {
root: 0,
tag: "div"
},
CreateElement { CreateElement {
root: 1, root: 1,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateElement {
root: 2, root: 2,
tag: "div"
},
CreateTextNode {
root: 3,
text: "Hello, world!" text: "Hello, world!"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
@@ -81,32 +81,32 @@ fn create() {
assert_eq!( assert_eq!(
mutations.edits, mutations.edits,
[ [
CreateElement {
root: 0,
tag: "div"
},
CreateElement { CreateElement {
root: 1, root: 1,
tag: "div" tag: "div"
}, },
CreateTextNode {
root: 2,
text: "Hello, world!"
},
CreateElement { CreateElement {
root: 3, root: 2,
tag: "div" tag: "div"
}, },
CreateTextNode {
root: 3,
text: "Hello, world!"
},
CreateElement { CreateElement {
root: 4, root: 4,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateElement {
root: 5, root: 5,
text: "hello" tag: "div"
}, },
CreateTextNode { CreateTextNode {
root: 6, root: 6,
text: "hello"
},
CreateTextNode {
root: 7,
text: "world" text: "world"
}, },
AppendChildren { many: 2 }, AppendChildren { many: 2 },
@@ -136,29 +136,29 @@ fn create_list() {
mutations.edits, mutations.edits,
[ [
CreateElement { CreateElement {
root: 0,
tag: "div"
},
CreateTextNode {
root: 1, root: 1,
text: "hello" tag: "div"
}, },
AppendChildren { many: 1 }, CreateTextNode {
CreateElement {
root: 2, root: 2,
tag: "div"
},
CreateTextNode {
root: 3,
text: "hello" text: "hello"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreateElement { CreateElement {
root: 4, root: 3,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
root: 4,
text: "hello"
},
AppendChildren { many: 1 },
CreateElement {
root: 5, root: 5,
tag: "div"
},
CreateTextNode {
root: 6,
text: "hello" text: "hello"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
@@ -185,10 +185,6 @@ fn create_simple() {
assert_eq!( assert_eq!(
mutations.edits, mutations.edits,
[ [
CreateElement {
root: 0,
tag: "div"
},
CreateElement { CreateElement {
root: 1, root: 1,
tag: "div" tag: "div"
@@ -201,6 +197,10 @@ fn create_simple() {
root: 3, root: 3,
tag: "div" tag: "div"
}, },
CreateElement {
root: 4,
tag: "div"
},
AppendChildren { many: 4 }, AppendChildren { many: 4 },
] ]
); );
@@ -234,39 +234,39 @@ fn create_components() {
assert_eq!( assert_eq!(
mutations.edits, mutations.edits,
[ [
CreateElement { root: 0, tag: "h1" }, CreateElement { root: 1, tag: "h1" },
CreateElement { CreateElement {
root: 1, root: 2,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
root: 2, root: 3,
text: "abc1" text: "abc1"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreateElement { root: 3, tag: "p" }, CreateElement { root: 4, tag: "p" },
CreateElement { root: 4, tag: "h1" }, CreateElement { root: 5, tag: "h1" },
CreateElement { CreateElement {
root: 5, root: 6,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
root: 6, root: 7,
text: "abc2" text: "abc2"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreateElement { root: 7, tag: "p" }, CreateElement { root: 8, tag: "p" },
CreateElement { root: 8, tag: "h1" }, CreateElement { root: 9, tag: "h1" },
CreateElement { CreateElement {
root: 9, root: 10,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
root: 10, root: 11,
text: "abc3" text: "abc3"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreateElement { root: 11, tag: "p" }, CreateElement { root: 12, tag: "p" },
AppendChildren { many: 9 }, AppendChildren { many: 9 },
] ]
); );
@@ -286,15 +286,15 @@ fn anchors() {
mutations.edits, mutations.edits,
[ [
CreateElement { CreateElement {
root: 0, root: 1,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
root: 1, root: 2,
text: "hello" text: "hello"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreatePlaceholder { root: 2 }, CreatePlaceholder { root: 3 },
AppendChildren { many: 2 }, AppendChildren { many: 2 },
] ]
); );
+98 -98
View File
@@ -32,11 +32,11 @@ fn html_and_rsx_generate_the_same_output() {
create.edits, create.edits,
[ [
CreateElement { CreateElement {
root: 0, root: 1,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
root: 1, root: 2,
text: "Hello world" text: "Hello world"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
@@ -48,7 +48,7 @@ fn html_and_rsx_generate_the_same_output() {
change.edits, change.edits,
[SetText { [SetText {
text: "Goodbye world", text: "Goodbye world",
root: 1 root: 2
},] },]
); );
} }
@@ -68,29 +68,29 @@ fn fragments_create_properly() {
create.edits, create.edits,
[ [
CreateElement { CreateElement {
root: 0, root: 1,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
root: 1, root: 2,
text: "Hello a" text: "Hello a"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreateElement { CreateElement {
root: 2, root: 3,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
root: 3, root: 4,
text: "Hello b" text: "Hello b"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreateElement { CreateElement {
root: 4, root: 5,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
root: 5, root: 6,
text: "Hello c" text: "Hello c"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
@@ -111,16 +111,16 @@ fn empty_fragments_create_anchors() {
assert_eq!( assert_eq!(
create.edits, create.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }] [CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }]
); );
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
CreateElement { CreateElement {
root: 1, root: 2,
tag: "div" tag: "div"
}, },
ReplaceWith { m: 1, root: 0 } ReplaceWith { m: 1, root: 1 }
] ]
); );
} }
@@ -136,15 +136,11 @@ fn empty_fragments_create_many_anchors() {
let (create, change) = dom.diff_lazynodes(left, right); let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!( assert_eq!(
create.edits, create.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }] [CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }]
); );
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
CreateElement {
root: 1,
tag: "div"
},
CreateElement { CreateElement {
root: 2, root: 2,
tag: "div" tag: "div"
@@ -161,7 +157,11 @@ fn empty_fragments_create_many_anchors() {
root: 5, root: 5,
tag: "div" tag: "div"
}, },
ReplaceWith { m: 5, root: 0 } CreateElement {
root: 6,
tag: "div"
},
ReplaceWith { m: 5, root: 1 }
] ]
); );
} }
@@ -182,39 +182,39 @@ fn empty_fragments_create_anchors_with_many_children() {
let (create, change) = dom.diff_lazynodes(left, right); let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!( assert_eq!(
create.edits, create.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }] [CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }]
); );
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
CreateElement { CreateElement {
root: 1, root: 2,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
text: "hello: 0", text: "hello: 0",
root: 2 root: 3
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreateElement { CreateElement {
root: 3, root: 4,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
text: "hello: 1", text: "hello: 1",
root: 4 root: 5
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreateElement { CreateElement {
root: 5, root: 6,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
text: "hello: 2", text: "hello: 2",
root: 6 root: 7
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
ReplaceWith { m: 3, root: 0 } ReplaceWith { m: 3, root: 1 }
] ]
); );
} }
@@ -236,21 +236,21 @@ fn many_items_become_fragment() {
create.edits, create.edits,
[ [
CreateElement { CreateElement {
root: 0, root: 1,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
text: "hello", text: "hello",
root: 1 root: 2
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreateElement { CreateElement {
root: 2, root: 3,
tag: "div" tag: "div"
}, },
CreateTextNode { CreateTextNode {
text: "hello", text: "hello",
root: 3 root: 4
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
AppendChildren { many: 2 }, AppendChildren { many: 2 },
@@ -261,9 +261,9 @@ fn many_items_become_fragment() {
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
Remove { root: 2 }, Remove { root: 3 },
CreatePlaceholder { root: 3 }, CreatePlaceholder { root: 4 },
ReplaceWith { root: 0, m: 1 }, ReplaceWith { root: 1, m: 1 },
] ]
); );
} }
@@ -308,15 +308,15 @@ fn two_fragments_with_differrent_elements_are_differet() {
changes.edits, changes.edits,
[ [
// create the new h1s // create the new h1s
CreateElement { tag: "h1", root: 3 },
CreateElement { tag: "h1", root: 4 }, CreateElement { tag: "h1", root: 4 },
CreateElement { tag: "h1", root: 5 }, CreateElement { tag: "h1", root: 5 },
InsertAfter { root: 1, n: 3 },
// replace the divs with new h1s
CreateElement { tag: "h1", root: 6 }, CreateElement { tag: "h1", root: 6 },
ReplaceWith { root: 0, m: 1 }, InsertAfter { root: 2, n: 3 },
// replace the divs with new h1s
CreateElement { tag: "h1", root: 7 }, CreateElement { tag: "h1", root: 7 },
ReplaceWith { root: 1, m: 1 }, ReplaceWith { root: 1, m: 1 },
CreateElement { tag: "h1", root: 8 },
ReplaceWith { root: 2, m: 1 },
] ]
); );
} }
@@ -339,10 +339,6 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() {
assert_eq!( assert_eq!(
create.edits, create.edits,
[ [
CreateElement {
root: 0,
tag: "div"
},
CreateElement { CreateElement {
root: 1, root: 1,
tag: "div" tag: "div"
@@ -359,20 +355,24 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() {
root: 4, root: 4,
tag: "div" tag: "div"
}, },
CreateElement { root: 5, tag: "p" }, CreateElement {
root: 5,
tag: "div"
},
CreateElement { root: 6, tag: "p" },
AppendChildren { many: 6 }, AppendChildren { many: 6 },
] ]
); );
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
Remove { root: 2 },
Remove { root: 3 }, Remove { root: 3 },
Remove { root: 4 }, Remove { root: 4 },
CreateElement { root: 6, tag: "h1" }, Remove { root: 5 },
ReplaceWith { root: 0, m: 1 },
CreateElement { root: 7, tag: "h1" }, CreateElement { root: 7, tag: "h1" },
ReplaceWith { root: 1, m: 1 }, ReplaceWith { root: 1, m: 1 },
CreateElement { root: 8, tag: "h1" },
ReplaceWith { root: 2, m: 1 },
] ]
); );
} }
@@ -395,25 +395,21 @@ fn two_fragments_with_same_elements_are_differet() {
assert_eq!( assert_eq!(
create.edits, create.edits,
[ [
CreateElement {
root: 0,
tag: "div"
},
CreateElement { CreateElement {
root: 1, root: 1,
tag: "div" tag: "div"
}, },
CreateElement { root: 2, tag: "p" }, CreateElement {
root: 2,
tag: "div"
},
CreateElement { root: 3, tag: "p" },
AppendChildren { many: 3 }, AppendChildren { many: 3 },
] ]
); );
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
CreateElement {
root: 3,
tag: "div"
},
CreateElement { CreateElement {
root: 4, root: 4,
tag: "div" tag: "div"
@@ -422,7 +418,11 @@ fn two_fragments_with_same_elements_are_differet() {
root: 5, root: 5,
tag: "div" tag: "div"
}, },
InsertAfter { root: 1, n: 3 }, CreateElement {
root: 6,
tag: "div"
},
InsertAfter { root: 2, n: 3 },
] ]
); );
} }
@@ -444,7 +444,7 @@ fn keyed_diffing_order() {
let (create, change) = dom.diff_lazynodes(left, right); let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!( assert_eq!(
change.edits, change.edits,
[Remove { root: 2 }, Remove { root: 3 }, Remove { root: 4 },] [Remove { root: 3 }, Remove { root: 4 }, Remove { root: 5 },]
); );
} }
@@ -469,7 +469,7 @@ fn keyed_diffing_out_of_order() {
log::debug!("{:?}", &changes); log::debug!("{:?}", &changes);
assert_eq!( assert_eq!(
changes.edits, changes.edits,
[PushRoot { root: 6 }, InsertBefore { root: 4, n: 1 }] [PushRoot { root: 7 }, InsertBefore { root: 5, n: 1 }]
); );
} }
@@ -494,9 +494,9 @@ fn keyed_diffing_out_of_order_adds() {
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
PushRoot { root: 5 },
PushRoot { root: 4 }, PushRoot { root: 4 },
PushRoot { root: 3 }, InsertBefore { n: 2, root: 1 }
InsertBefore { n: 2, root: 0 }
] ]
); );
} }
@@ -521,9 +521,9 @@ fn keyed_diffing_out_of_order_adds_2() {
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
PushRoot { root: 3 },
PushRoot { root: 4 }, PushRoot { root: 4 },
InsertBefore { n: 2, root: 0 } PushRoot { root: 5 },
InsertBefore { n: 2, root: 1 }
] ]
); );
} }
@@ -549,9 +549,9 @@ fn keyed_diffing_out_of_order_adds_3() {
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
PushRoot { root: 5 },
PushRoot { root: 4 }, PushRoot { root: 4 },
PushRoot { root: 3 }, InsertBefore { n: 2, root: 2 }
InsertBefore { n: 2, root: 1 }
] ]
); );
} }
@@ -577,9 +577,9 @@ fn keyed_diffing_out_of_order_adds_4() {
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
PushRoot { root: 5 },
PushRoot { root: 4 }, PushRoot { root: 4 },
PushRoot { root: 3 }, InsertBefore { n: 2, root: 3 }
InsertBefore { n: 2, root: 2 }
] ]
); );
} }
@@ -604,7 +604,7 @@ fn keyed_diffing_out_of_order_adds_5() {
let (_, change) = dom.diff_lazynodes(left, right); let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!( assert_eq!(
change.edits, change.edits,
[PushRoot { root: 4 }, InsertBefore { n: 1, root: 3 }] [PushRoot { root: 5 }, InsertBefore { n: 1, root: 4 }]
); );
} }
@@ -628,15 +628,15 @@ fn keyed_diffing_additions() {
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [
CreateElement {
root: 5,
tag: "div"
},
CreateElement { CreateElement {
root: 6, root: 6,
tag: "div" tag: "div"
}, },
InsertAfter { n: 2, root: 4 } CreateElement {
root: 7,
tag: "div"
},
InsertAfter { n: 2, root: 5 }
] ]
); );
} }
@@ -665,16 +665,16 @@ fn keyed_diffing_additions_and_moves_on_ends() {
// create 11, 12 // create 11, 12
CreateElement { CreateElement {
tag: "div", tag: "div",
root: 4 root: 5
}, },
CreateElement { CreateElement {
tag: "div", tag: "div",
root: 5 root: 6
}, },
InsertAfter { root: 2, n: 2 }, InsertAfter { root: 3, n: 2 },
// move 7 to the front // move 7 to the front
PushRoot { root: 3 }, PushRoot { root: 4 },
InsertBefore { root: 0, n: 1 } InsertBefore { root: 1, n: 1 }
] ]
); );
} }
@@ -702,28 +702,28 @@ fn keyed_diffing_additions_and_moves_in_middle() {
change.edits, change.edits,
[ [
// create 13, 17 // create 13, 17
CreateElement {
tag: "div",
root: 4
},
CreateElement { CreateElement {
tag: "div", tag: "div",
root: 5 root: 5
}, },
InsertBefore { root: 1, n: 2 },
// create 11, 12
CreateElement { CreateElement {
tag: "div", tag: "div",
root: 6 root: 6
}, },
InsertBefore { root: 2, n: 2 },
// create 11, 12
CreateElement { CreateElement {
tag: "div", tag: "div",
root: 7 root: 7
}, },
InsertBefore { root: 2, n: 2 }, CreateElement {
tag: "div",
root: 8
},
InsertBefore { root: 3, n: 2 },
// move 7 // move 7
PushRoot { root: 3 }, PushRoot { root: 4 },
InsertBefore { root: 0, n: 1 } InsertBefore { root: 1, n: 1 }
] ]
); );
} }
@@ -751,22 +751,22 @@ fn controlled_keyed_diffing_out_of_order() {
changes.edits, changes.edits,
[ [
// move 4 to after 6 // move 4 to after 6
PushRoot { root: 0 }, PushRoot { root: 1 },
InsertAfter { n: 1, root: 2 }, InsertAfter { n: 1, root: 3 },
// remove 7 // remove 7
// create 9 and insert before 6 // create 9 and insert before 6
CreateElement {
root: 4,
tag: "div"
},
InsertBefore { n: 1, root: 2 },
// create 0 and insert before 5
CreateElement { CreateElement {
root: 5, root: 5,
tag: "div" tag: "div"
}, },
InsertBefore { n: 1, root: 1 }, InsertBefore { n: 1, root: 3 },
// create 0 and insert before 5
CreateElement {
root: 6,
tag: "div"
},
InsertBefore { n: 1, root: 2 },
] ]
); );
} }
@@ -793,12 +793,12 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
changes.edits, changes.edits,
[ [
CreateElement { CreateElement {
root: 5, root: 6,
tag: "div" tag: "div"
}, },
InsertBefore { n: 1, root: 2 }, InsertBefore { n: 1, root: 3 },
PushRoot { root: 3 }, PushRoot { root: 4 },
InsertBefore { n: 1, root: 0 }, InsertBefore { n: 1, root: 1 },
] ]
); );
} }
+37 -39
View File
@@ -18,8 +18,6 @@ type Shared<T> = Arc<Mutex<T>>;
#[test] #[test]
fn manual_diffing() { fn manual_diffing() {
test_logging::set_up_logging(IS_LOGGING_ENABLED);
struct AppProps { struct AppProps {
value: Shared<&'static str>, value: Shared<&'static str>,
} }
@@ -73,33 +71,34 @@ fn events_generate() {
let mut channel = dom.get_scheduler_channel(); let mut channel = dom.get_scheduler_channel();
assert!(dom.has_any_work()); assert!(dom.has_any_work());
let edits = dom.work_with_deadline(|| false); let edits = dom.rebuild();
assert_eq!( assert_eq!(
edits[0].edits, edits.edits,
[ [
CreateElement {
tag: "div",
root: 0,
},
NewEventListener {
event_name: "click",
scope: ScopeId(0),
root: 0,
},
CreateElement { CreateElement {
tag: "div", tag: "div",
root: 1, root: 1,
}, },
NewEventListener {
event_name: "click",
scope: ScopeId(0),
root: 1,
},
CreateElement {
tag: "div",
root: 2,
},
CreateTextNode { CreateTextNode {
text: "nested", text: "nested",
root: 2, root: 3,
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
CreateTextNode { CreateTextNode {
text: "Click me!", text: "Click me!",
root: 3, root: 4,
}, },
AppendChildren { many: 2 }, AppendChildren { many: 2 },
AppendChildren { many: 1 },
] ]
) )
} }
@@ -124,7 +123,6 @@ fn components_generate() {
}; };
static Child: FC<()> = |cx, _| { static Child: FC<()> = |cx, _| {
println!("running child");
cx.render(rsx! { cx.render(rsx! {
h1 {} h1 {}
}) })
@@ -137,7 +135,7 @@ fn components_generate() {
[ [
CreateTextNode { CreateTextNode {
text: "Text0", text: "Text0",
root: 0, root: 1,
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
] ]
@@ -149,18 +147,6 @@ fn components_generate() {
[ [
CreateElement { CreateElement {
tag: "div", tag: "div",
root: 1,
},
ReplaceWith { root: 0, m: 1 },
]
);
let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!(
edits.edits,
[
CreateTextNode {
text: "Text2",
root: 2, root: 2,
}, },
ReplaceWith { root: 1, m: 1 }, ReplaceWith { root: 1, m: 1 },
@@ -171,7 +157,10 @@ fn components_generate() {
assert_eq!( assert_eq!(
edits.edits, edits.edits,
[ [
CreateElement { tag: "h1", root: 3 }, CreateTextNode {
text: "Text2",
root: 3,
},
ReplaceWith { root: 2, m: 1 }, ReplaceWith { root: 2, m: 1 },
] ]
); );
@@ -179,7 +168,16 @@ fn components_generate() {
let edits = dom.hard_diff(&ScopeId(0)).unwrap(); let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!( assert_eq!(
edits.edits, edits.edits,
[CreatePlaceholder { root: 4 }, ReplaceWith { root: 3, m: 1 },] [
CreateElement { tag: "h1", root: 4 },
ReplaceWith { root: 3, m: 1 },
]
);
let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!(
edits.edits,
[CreatePlaceholder { root: 5 }, ReplaceWith { root: 4, m: 1 },]
); );
let edits = dom.hard_diff(&ScopeId(0)).unwrap(); let edits = dom.hard_diff(&ScopeId(0)).unwrap();
@@ -188,9 +186,9 @@ fn components_generate() {
[ [
CreateTextNode { CreateTextNode {
text: "text 3", text: "text 3",
root: 5, root: 6,
}, },
ReplaceWith { root: 4, m: 1 }, ReplaceWith { root: 5, m: 1 },
] ]
); );
@@ -200,13 +198,13 @@ fn components_generate() {
[ [
CreateTextNode { CreateTextNode {
text: "text 0", text: "text 0",
root: 6, root: 7,
}, },
CreateTextNode { CreateTextNode {
text: "text 1", text: "text 1",
root: 7, root: 8,
}, },
ReplaceWith { root: 5, m: 2 }, ReplaceWith { root: 6, m: 2 },
] ]
); );
@@ -214,9 +212,9 @@ fn components_generate() {
assert_eq!( assert_eq!(
edits.edits, edits.edits,
[ [
CreateElement { tag: "h1", root: 8 }, CreateElement { tag: "h1", root: 9 },
ReplaceWith { root: 6, m: 1 }, ReplaceWith { root: 7, m: 1 },
Remove { root: 7 }, Remove { root: 8 },
] ]
); );
} }
-41
View File
@@ -1,41 +0,0 @@
use futures_util::StreamExt;
/*
furtures_channel provides us some batching simply due to how Rust's async works.
Any hook that uses schedule_update is simply deferring to unbounded_send. Multiple
unbounded_sends can be linked together in succession provided there isn't an "await"
between them. Our internal batching mechanism simply waits for the "schedule_update"
to fire and then pulls any messages off the unbounded_send queue.
Additionally, due to how our "time slicing" works we'll always come back and check
in for new work if the deadline hasn't expired. On average, our deadline should be
about 10ms, which is way more than enough for diffing/creating to happen.
*/
#[async_std::test]
async fn batch() {
let (sender, mut recver) = futures_channel::mpsc::unbounded::<i32>();
let _handle = async_std::task::spawn(async move {
let _msg = recver.next().await;
while let Ok(msg) = recver.try_next() {
println!("{:#?}", msg);
}
let _msg = recver.next().await;
while let Ok(msg) = recver.try_next() {
println!("{:#?}", msg);
}
});
sender.unbounded_send(1).unwrap();
sender.unbounded_send(2).unwrap();
sender.unbounded_send(3).unwrap();
sender.unbounded_send(4).unwrap();
async_std::task::sleep(std::time::Duration::from_millis(100)).await;
sender.unbounded_send(5).unwrap();
sender.unbounded_send(6).unwrap();
sender.unbounded_send(7).unwrap();
sender.unbounded_send(8).unwrap();
}
+1 -1
View File
@@ -30,7 +30,7 @@ fn shared_state_test() {
edits, edits,
[ [
CreateTextNode { CreateTextNode {
root: 0, root: 1,
text: "Hello, world!" text: "Hello, world!"
}, },
AppendChildren { many: 1 }, AppendChildren { many: 1 },
+23 -22
View File
@@ -11,19 +11,18 @@
//! Don't have a good way to validate, everything is done manually ATM //! Don't have a good way to validate, everything is done manually ATM
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::DomEdit;
use dioxus_core as dioxus; use dioxus_core as dioxus;
use dioxus_core_macro::*; use dioxus_core_macro::*;
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
use DomEdit::*;
#[test] #[test]
fn app_runs() { fn app_runs() {
static App: FC<()> = |cx, props| { static App: FC<()> = |cx, props| rsx!(cx, div{"hello"} );
//
cx.render(rsx!( div{"hello"} ))
};
let mut vdom = VirtualDom::new(App); let mut vdom = VirtualDom::new(App);
let edits = vdom.rebuild(); let edits = vdom.rebuild();
dbg!(edits);
} }
#[test] #[test]
@@ -65,9 +64,25 @@ fn conditional_rendering() {
let mut vdom = VirtualDom::new(App); let mut vdom = VirtualDom::new(App);
let mutations = vdom.rebuild(); let mutations = vdom.rebuild();
dbg!(&mutations); assert_eq!(
// the "false" fragment should generate an empty placeholder to re-visit mutations.edits,
assert!(mutations.edits[mutations.edits.len() - 2].is("CreatePlaceholder")); [
CreateElement { root: 1, tag: "h1" },
CreateTextNode {
root: 2,
text: "hello"
},
AppendChildren { many: 1 },
CreateElement {
root: 3,
tag: "span"
},
CreateTextNode { root: 4, text: "a" },
AppendChildren { many: 1 },
CreatePlaceholder { root: 5 },
AppendChildren { many: 3 },
]
)
} }
#[test] #[test]
@@ -88,17 +103,3 @@ fn child_components() {
let edits = vdom.rebuild(); let edits = vdom.rebuild();
dbg!(edits); dbg!(edits);
} }
#[test]
fn suspended_works() {
todo!()
// static App: FC<()> = |cx, props| {
// let title = use_suspense(cx, || async { "bob" }, move |cx, f| todo!());
// // let title = use_suspense(cx, || async { "bob" }, move |cx, f| rsx! { "{f}"});
// cx.render(rsx!("hello" { title }))
// };
// let mut vdom = VirtualDom::new(App);
// let edits = vdom.rebuild();
// dbg!(edits);
}
+1
View File
@@ -22,6 +22,7 @@ tokio = { version = "1.12.0", features = [
"sync", "sync",
"rt-multi-thread", "rt-multi-thread",
"rt", "rt",
"time",
], optional = true, default-features = false } ], optional = true, default-features = false }
dioxus-core-macro = { path = "../core-macro" } dioxus-core-macro = { path = "../core-macro" }
dioxus-html = { path = "../html", features = ["serialize"] } dioxus-html = { path = "../html", features = ["serialize"] }
+34
View File
@@ -0,0 +1,34 @@
//! Example: README.md showcase
//!
//! The example from the README.md.
use std::time::Duration;
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
fn main() {
dioxus_desktop::launch(App, |c| c);
}
static App: FC<()> = |cx, props| {
let mut count = use_state(cx, || 0);
cx.push_task(|| async move {
tokio::time::sleep(Duration::from_millis(100)).await;
count += 1;
});
cx.render(rsx! {
div {
h1 { "High-Five counter: {count}" }
button {
onclick: move |_| count.set(0),
"Click me!"
}
}
})
};
+23 -23
View File
@@ -11,7 +11,7 @@ use dioxus_html::on::*;
struct ImEvent { struct ImEvent {
event: String, event: String,
mounted_dom_id: u64, mounted_dom_id: u64,
scope: u64, // scope: u64,
contents: serde_json::Value, contents: serde_json::Value,
} }
@@ -20,11 +20,11 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
let ImEvent { let ImEvent {
event, event,
mounted_dom_id, mounted_dom_id,
scope, // scope,
contents, contents,
} = ims.into_iter().next().unwrap(); } = ims.into_iter().next().unwrap();
let scope_id = ScopeId(scope as usize); // let scope_id = ScopeId(scope as usize);
let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
let name = event_name_from_typ(&event); let name = event_name_from_typ(&event);
@@ -33,75 +33,75 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
UserEvent { UserEvent {
name, name,
priority: EventPriority::Low, priority: EventPriority::Low,
scope_id, scope_id: None,
mounted_dom_id, element: mounted_dom_id,
event, data: event,
} }
} }
fn make_synthetic_event(name: &str, val: serde_json::Value) -> Box<dyn Any + Send> { fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Send + Sync> {
match name { match name {
"copy" | "cut" | "paste" => { "copy" | "cut" | "paste" => {
// //
Box::new(ClipboardEvent {}) Arc::new(ClipboardEvent {})
} }
"compositionend" | "compositionstart" | "compositionupdate" => { "compositionend" | "compositionstart" | "compositionupdate" => {
Box::new(serde_json::from_value::<CompositionEvent>(val).unwrap()) Arc::new(serde_json::from_value::<CompositionEvent>(val).unwrap())
} }
"keydown" | "keypress" | "keyup" => { "keydown" | "keypress" | "keyup" => {
let evt = serde_json::from_value::<KeyboardEvent>(val).unwrap(); let evt = serde_json::from_value::<KeyboardEvent>(val).unwrap();
Box::new(evt) Arc::new(evt)
} }
"focus" | "blur" => { "focus" | "blur" => {
// //
Box::new(FocusEvent {}) Arc::new(FocusEvent {})
} }
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
// don't have a good solution with the serialized event problem // don't have a good solution with the serialized event problem
"change" | "input" | "invalid" | "reset" | "submit" => { "change" | "input" | "invalid" | "reset" | "submit" => {
Box::new(serde_json::from_value::<FormEvent>(val).unwrap()) Arc::new(serde_json::from_value::<FormEvent>(val).unwrap())
} }
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
Box::new(serde_json::from_value::<MouseEvent>(val).unwrap()) Arc::new(serde_json::from_value::<MouseEvent>(val).unwrap())
} }
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
Box::new(serde_json::from_value::<PointerEvent>(val).unwrap()) Arc::new(serde_json::from_value::<PointerEvent>(val).unwrap())
} }
"select" => { "select" => {
// //
Box::new(serde_json::from_value::<SelectionEvent>(val).unwrap()) Arc::new(serde_json::from_value::<SelectionEvent>(val).unwrap())
} }
"touchcancel" | "touchend" | "touchmove" | "touchstart" => { "touchcancel" | "touchend" | "touchmove" | "touchstart" => {
Box::new(serde_json::from_value::<TouchEvent>(val).unwrap()) Arc::new(serde_json::from_value::<TouchEvent>(val).unwrap())
} }
"scroll" => Box::new(()), "scroll" => Arc::new(()),
"wheel" => Box::new(serde_json::from_value::<WheelEvent>(val).unwrap()), "wheel" => Arc::new(serde_json::from_value::<WheelEvent>(val).unwrap()),
"animationstart" | "animationend" | "animationiteration" => { "animationstart" | "animationend" | "animationiteration" => {
Box::new(serde_json::from_value::<AnimationEvent>(val).unwrap()) Arc::new(serde_json::from_value::<AnimationEvent>(val).unwrap())
} }
"transitionend" => Box::new(serde_json::from_value::<TransitionEvent>(val).unwrap()), "transitionend" => Arc::new(serde_json::from_value::<TransitionEvent>(val).unwrap()),
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => { | "timeupdate" | "volumechange" | "waiting" => {
// //
Box::new(MediaEvent {}) Arc::new(MediaEvent {})
} }
"toggle" => Box::new(ToggleEvent {}), "toggle" => Arc::new(ToggleEvent {}),
_ => Box::new(()), _ => Arc::new(()),
} }
} }
+237 -304
View File
@@ -1,4 +1,213 @@
function serialize_event(event) {
switch (event.type) {
case "copy":
case "cut":
case "past":
return {};
case "compositionend":
case "compositionstart":
case "compositionupdate":
return {
data: event.data
}
case "keydown":
case "keypress":
case "keyup":
return {
char_code: event.charCode,
key: event.key,
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
key_code: event.keyCode,
shift_key: event.shiftKey,
locale: "locale",
location: event.location,
repeat: event.repeat,
which: event.which,
// locale: event.locale,
}
case "focus":
case "blur":
return {};
case "change":
let target = event.target;
let value;
if (target.type === "checkbox" || target.type === "radio") {
value = target.checked ? "true" : "false";
} else {
value = target.value ?? target.textContent;
}
return {
value: value
};
case "input":
case "invalid":
case "reset":
case "submit": {
let target = event.target;
let value = target.value ?? target.textContent;
return {
value: value
};
}
case "click":
case "contextmenu":
case "doubleclick":
case "drag":
case "dragend":
case "dragenter":
case "dragexit":
case "dragleave":
case "dragover":
case "dragstart":
case "drop":
case "mousedown":
case "mouseenter":
case "mouseleave":
case "mousemove":
case "mouseout":
case "mouseover":
case "mouseup":
return {
alt_key: event.altKey,
button: event.button,
buttons: event.buttons,
client_x: event.clientX,
client_y: event.clientY,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
}
case "pointerdown":
case "pointermove":
case "pointerup":
case "pointercancel":
case "gotpointercapture":
case "lostpointercapture":
case "pointerenter":
case "pointerleave":
case "pointerover":
case "pointerout":
return {
alt_key: event.altKey,
button: event.button,
buttons: event.buttons,
client_x: event.clientX,
client_y: event.clientY,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
pointer_id: event.pointerId,
width: event.width,
height: event.height,
pressure: event.pressure,
tangential_pressure: event.tangentialPressure,
tilt_x: event.tiltX,
tilt_y: event.tiltY,
twist: event.twist,
pointer_type: event.pointerType,
is_primary: event.isPrimary,
}
case "select":
return {};
case "touchcancel":
case "touchend":
case "touchmove":
case "touchstart":
return {
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
shift_key: event.shiftKey,
// changed_touches: event.changedTouches,
// target_touches: event.targetTouches,
// touches: event.touches,
}
case "scroll":
return {};
case "wheel":
return {
delta_x: event.deltaX,
delta_y: event.deltaY,
delta_z: event.deltaZ,
delta_mode: event.deltaMode,
};
case "animationstart":
case "animationend":
case "animationiteration":
return {
animation_name: event.animationName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
};
case "transitionend":
return {
property_name: event.propertyName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
};
case "abort":
case "canplay":
case "canplaythrough":
case "durationchange":
case "emptied":
case "encrypted":
case "ended":
case "error":
case "loadeddata":
case "loadedmetadata":
case "loadstart":
case "pause":
case "play":
case "playing":
case "progress":
case "ratechange":
case "seeked":
case "seeking":
case "stalled":
case "suspend":
case "timeupdate":
case "volumechange":
case "waiting":
return {};
case "toggle":
return {};
default:
return {};
}
}
class Interpreter { class Interpreter {
constructor(root) { constructor(root) {
this.root = root; this.root = root;
@@ -39,14 +248,9 @@ class Interpreter {
} }
ReplaceWith(edit) { ReplaceWith(edit) {
// console.log(edit);
let root = this.nodes[edit.root]; let root = this.nodes[edit.root];
let els = this.stack.splice(this.stack.length - edit.m); let els = this.stack.splice(this.stack.length - edit.m);
// console.log(root);
// console.log(els);
root.replaceWith(...els); root.replaceWith(...els);
} }
@@ -79,6 +283,7 @@ class Interpreter {
const tagName = edit.tag; const tagName = edit.tag;
const el = document.createElement(tagName); const el = document.createElement(tagName);
this.nodes[edit.root] = el; this.nodes[edit.root] = el;
el.setAttribute("dioxus-id", edit.root);
this.stack.push(el); this.stack.push(el);
} }
@@ -110,14 +315,14 @@ class Interpreter {
this.root.addEventListener(event_name, (event) => { this.root.addEventListener(event_name, (event) => {
// console.log("CLICKED"); // console.log("CLICKED");
const target = event.target; const target = event.target;
const val = target.getAttribute(`dioxus-event-${event_name}`); const real_id = target.getAttribute(`dioxus-id`);
if (val == null) { if (real_id == null) {
return; return;
} }
const fields = val.split("."); // const fields = val.split(".");
const scope_id = parseInt(fields[0]); // const scope_id = parseInt(fields[0]);
const real_id = parseInt(fields[1]); // const real_id = parseInt(fields[1]);
// // console.log(`parsed event with scope_id ${scope_id} and real_id ${real_id}`); // // console.log(`parsed event with scope_id ${scope_id} and real_id ${real_id}`);
@@ -125,26 +330,12 @@ class Interpreter {
let contents = serialize_event(event); let contents = serialize_event(event);
let evt = { let evt = {
event: event_name, event: event_name,
scope: scope_id, // scope: scope_id,
mounted_dom_id: real_id, mounted_dom_id: parseInt(real_id),
contents: contents, contents: contents,
}; };
// console.log(evt);
rpc.call('user_event', evt).then((reply) => {
// console.log("reply received", reply);
this.stack.push(this.root); rpc.call('user_event', evt);
reply.map((reply) => {
let edits = reply.edits;
for (let x = 0; x < edits.length; x++) {
let edit = edits[x];
let f = this[edit.type];
f.call(this, edit);
}
});
})
}); });
} }
} }
@@ -201,286 +392,28 @@ class Interpreter {
} }
} }
handleEdits(edits) {
// console.log("handling raw edits", rawedits);
// let edits = JSON.parse(rawedits);
// console.log("handling edits", edits);
this.stack.push(this.root);
for (let x = 0; x < edits.length; x++) {
let edit = edits[x];
let f = this[edit.type];
f.call(this, edit);
}
}
} }
async function initialize() { function main() {
let root = window.document.getElementById("_dioxusroot"); let root = window.document.getElementById("_dioxusroot");
const interpreter = new Interpreter(root); window.interpreter = new Interpreter(root);
console.log(window.interpreter);
const reply = await rpc.call('initiate'); rpc.call('initialize');
let pre_rendered = reply.pre_rendered;
if (pre_rendered !== undefined) {
root.innerHTML = pre_rendered;
}
const edits = reply.edits;
apply_edits(edits, interpreter);
} }
function apply_edits(edits, interpreter) { main()
// console.log(edits);
for (let x = 0; x < edits.length; x++) {
let edit = edits[x];
let f = interpreter[edit.type];
f.call(interpreter, edit);
}
// // console.log("stack completed: ", interpreter.stack);
}
function serialize_event(event) {
let serializer = SerializeMap[event.type];
if (serializer === undefined) {
return {};
} else {
return serializer(event);
}
}
const SerializeMap = {
"copy": serialize_clipboard,
"cut": serialize_clipboard,
"paste": serialize_clipboard,
"compositionend": serialize_composition,
"compositionstart": serialize_composition,
"compositionupdate": serialize_composition,
"keydown": serialize_keyboard,
"keypress": serialize_keyboard,
"keyup": serialize_keyboard,
"focus": serialize_focus,
"blur": serialize_focus,
// "change": serialize_form,
"change": serialize_change,
"input": serialize_form,
"invalid": serialize_form,
"reset": serialize_form,
"submit": serialize_form,
"click": serialize_mouse,
"contextmenu": serialize_mouse,
"doubleclick": serialize_mouse,
"drag": serialize_mouse,
"dragend": serialize_mouse,
"dragenter": serialize_mouse,
"dragexit": serialize_mouse,
"dragleave": serialize_mouse,
"dragover": serialize_mouse,
"dragstart": serialize_mouse,
"drop": serialize_mouse,
"mousedown": serialize_mouse,
"mouseenter": serialize_mouse,
"mouseleave": serialize_mouse,
"mousemove": serialize_mouse,
"mouseout": serialize_mouse,
"mouseover": serialize_mouse,
"mouseup": serialize_mouse,
"pointerdown": serialize_pointer,
"pointermove": serialize_pointer,
"pointerup": serialize_pointer,
"pointercancel": serialize_pointer,
"gotpointercapture": serialize_pointer,
"lostpointercapture": serialize_pointer,
"pointerenter": serialize_pointer,
"pointerleave": serialize_pointer,
"pointerover": serialize_pointer,
"pointerout": serialize_pointer,
"select": serialize_selection,
"touchcancel": serialize_touch,
"touchend": serialize_touch,
"touchmove": serialize_touch,
"touchstart": serialize_touch,
"scroll": serialize_scroll,
"wheel": serialize_wheel,
"animationstart": serialize_animation,
"animationend": serialize_animation,
"animationiteration": serialize_animation,
"transitionend": serialize_transition,
"abort": serialize_media,
"canplay": serialize_media,
"canplaythrough": serialize_media,
"durationchange": serialize_media,
"emptied": serialize_media,
"encrypted": serialize_media,
"ended": serialize_media,
"error": serialize_media,
"loadeddata": serialize_media,
"loadedmetadata": serialize_media,
"loadstart": serialize_media,
"pause": serialize_media,
"play": serialize_media,
"playing": serialize_media,
"progress": serialize_media,
"ratechange": serialize_media,
"seeked": serialize_media,
"seeking": serialize_media,
"stalled": serialize_media,
"suspend": serialize_media,
"timeupdate": serialize_media,
"volumechange": serialize_media,
"waiting": serialize_media,
"toggle": serialize_toggle
}
function serialize_clipboard(_event) {
return {};
}
function serialize_composition(event) {
return {
data: event.data
}
}
function serialize_keyboard(event) {
return {
char_code: event.charCode,
key: event.key,
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
key_code: event.keyCode,
shift_key: event.shiftKey,
locale: "locale",
// locale: event.locale,
location: event.location,
repeat: event.repeat,
which: event.which,
}
}
function serialize_focus(_event) {
return {}
}
function serialize_change(event) {
let target = event.target;
let value;
if (target.type === "checkbox" || target.type === "radio") {
value = target.checked ? "true" : "false";
} else {
value = target.value ?? target.textContent;
}
return {
value: value
}
}
function serialize_form(event) {
let target = event.target;
let value = target.value ?? target.textContent;
return {
value: value
}
}
function serialize_mouse(event) {
return {
alt_key: event.altKey,
button: event.button,
buttons: event.buttons,
client_x: event.clientX,
client_y: event.clientY,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
}
}
function serialize_pointer(event) {
return {
alt_key: event.altKey,
button: event.button,
buttons: event.buttons,
client_x: event.clientX,
client_y: event.clientY,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
pointer_id: event.pointerId,
width: event.width,
height: event.height,
pressure: event.pressure,
tangential_pressure: event.tangentialPressure,
tilt_x: event.tiltX,
tilt_y: event.tiltY,
twist: event.twist,
pointer_type: event.pointerType,
is_primary: event.isPrimary,
}
}
function serialize_selection(event) {
return {}
}
function serialize_touch(event) {
return {
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
shift_key: event.shiftKey,
// changed_touches: event.changedTouches,
// target_touches: event.targetTouches,
// touches: event.touches,
}
}
function serialize_scroll(event) {
return {}
}
function serialize_wheel(event) {
return {
delta_x: event.deltaX,
delta_y: event.deltaY,
delta_z: event.deltaZ,
delta_mode: event.deltaMode,
}
}
function serialize_animation(event) {
return {
animation_name: event.animationName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
}
}
function serialize_transition(event) {
return {
property_name: event.propertyName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
}
}
function serialize_media(event) {
return {}
}
function serialize_toggle(event) {
return {}
}
initialize();
+124 -161
View File
@@ -5,7 +5,7 @@
use std::borrow::BorrowMut; use std::borrow::BorrowMut;
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::collections::HashMap; use std::collections::{HashMap, VecDeque};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::rc::Rc; use std::rc::Rc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
@@ -20,10 +20,10 @@ pub use wry;
use wry::application::accelerator::{Accelerator, SysMods}; use wry::application::accelerator::{Accelerator, SysMods};
use wry::application::event::{ElementState, Event, StartCause, WindowEvent}; use wry::application::event::{ElementState, Event, StartCause, WindowEvent};
use wry::application::event_loop::{self, ControlFlow, EventLoop}; use wry::application::event_loop::{self, ControlFlow, EventLoop, EventLoopWindowTarget};
use wry::application::keyboard::{Key, KeyCode, ModifiersState}; use wry::application::keyboard::{Key, KeyCode, ModifiersState};
use wry::application::menu::{MenuBar, MenuItem, MenuItemAttributes}; use wry::application::menu::{MenuBar, MenuItem, MenuItemAttributes};
use wry::application::window::Fullscreen; use wry::application::window::{Fullscreen, WindowId};
use wry::webview::{WebView, WebViewBuilder}; use wry::webview::{WebView, WebViewBuilder};
use wry::{ use wry::{
application::menu, application::menu,
@@ -59,12 +59,6 @@ enum RpcEvent<'a> {
Initialize { edits: Vec<DomEdit<'a>> }, Initialize { edits: Vec<DomEdit<'a>> },
} }
#[derive(Debug)]
enum BridgeEvent {
Initialize(serde_json::Value),
Update(serde_json::Value),
}
#[derive(Serialize)] #[derive(Serialize)]
struct Response<'a> { struct Response<'a> {
pre_rendered: Option<String>, pre_rendered: Option<String>,
@@ -88,111 +82,29 @@ pub fn run<T: 'static + Send + Sync>(
// All of our webview windows are stored in a way that we can look them up later // All of our webview windows are stored in a way that we can look them up later
// The "DesktopContext" will provide functionality for spawning these windows // The "DesktopContext" will provide functionality for spawning these windows
let mut webviews = HashMap::new(); let mut webviews = HashMap::<WindowId, WebView>::new();
let event_loop = EventLoop::new(); let event_loop = EventLoop::new();
let props_shared = Cell::new(Some(props)); let props_shared = Cell::new(Some(props));
// create local modifier state // create local modifier state
let mut modifiers = ModifiersState::default(); let modifiers = ModifiersState::default();
let quit_hotkey = Accelerator::new(SysMods::Cmd, KeyCode::KeyQ); let quit_hotkey = Accelerator::new(SysMods::Cmd, KeyCode::KeyQ);
event_loop.run(move |event, event_loop, control_flow| { let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
let is_ready: Arc<AtomicBool> = Default::default();
event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait; *control_flow = ControlFlow::Wait;
match event { match window_event {
Event::NewEvents(StartCause::Init) => { Event::NewEvents(StartCause::Init) => {
// create main menubar menu let window = create_window(event_loop);
let mut menu_bar_menu = MenuBar::new();
// create `first_menu`
let mut first_menu = MenuBar::new();
first_menu.add_native_item(MenuItem::About("Todos".to_string()));
first_menu.add_native_item(MenuItem::Services);
first_menu.add_native_item(MenuItem::Separator);
first_menu.add_native_item(MenuItem::Hide);
first_menu.add_native_item(MenuItem::HideOthers);
first_menu.add_native_item(MenuItem::ShowAll);
first_menu.add_native_item(MenuItem::Quit);
first_menu.add_native_item(MenuItem::CloseWindow);
// create second menu
let mut second_menu = MenuBar::new();
// second_menu.add_submenu("Sub menu", true, my_sub_menu);
second_menu.add_native_item(MenuItem::Copy);
second_menu.add_native_item(MenuItem::Paste);
second_menu.add_native_item(MenuItem::SelectAll);
menu_bar_menu.add_submenu("First menu", true, first_menu);
menu_bar_menu.add_submenu("Second menu", true, second_menu);
let window = WindowBuilder::new()
.with_maximized(true)
.with_menu(menu_bar_menu)
.with_title("Dioxus App")
.build(event_loop)
.unwrap();
let window_id = window.id(); let window_id = window.id();
let sender =
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel(); launch_vdom_with_tokio(root, props_shared.take().unwrap(), edit_queue.clone());
let my_props = props_shared.take().unwrap(); let webview = create_webview(window, is_ready.clone(), sender);
let sender = launch_vdom_with_tokio(root, my_props, event_tx);
let locked_receiver = Rc::new(RefCell::new(event_rx));
let webview = WebViewBuilder::new(window)
.unwrap()
.with_url("wry://index.html")
.unwrap()
.with_rpc_handler(move |_window: &Window, mut req: RpcRequest| {
let mut rx = (*locked_receiver).borrow_mut();
match req.method.as_str() {
"initiate" => {
if let Ok(BridgeEvent::Initialize(edits)) = rx.try_recv() {
Some(RpcResponse::new_result(req.id.take(), Some(edits)))
} else {
None
}
}
"user_event" => {
let event = events::trigger_from_serialized(req.params.unwrap());
log::debug!("User event: {:?}", event);
sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap();
if let Some(BridgeEvent::Update(edits)) = rx.blocking_recv() {
log::info!("bridge received message");
Some(RpcResponse::new_result(req.id.take(), Some(edits)))
} else {
log::info!("none received message");
None
}
}
_ => None,
}
})
// Any content that that uses the `wry://` scheme will be shuttled through this handler as a "special case"
// For now, we only serve two pieces of content which get included as bytes into the final binary.
.with_custom_protocol("wry".into(), move |request| {
let path = request.uri().replace("wry://", "");
let (data, meta) = match path.as_str() {
"index.html" => (include_bytes!("./index.html").to_vec(), "text/html"),
"index.html/index.js" => {
(include_bytes!("./index.js").to_vec(), "text/javascript")
}
_ => unimplemented!("path {}", path),
};
wry::http::ResponseBuilder::new().mimetype(meta).body(data)
})
.build()
.unwrap();
webviews.insert(window_id, webview); webviews.insert(window_id, webview);
} }
@@ -206,29 +118,17 @@ pub fn run<T: 'static + Send + Sync>(
*control_flow = ControlFlow::Exit; *control_flow = ControlFlow::Exit;
} }
} }
// catch only pressed event WindowEvent::Moved(pos) => {
WindowEvent::KeyboardInput { event, .. } => { //
log::debug!("keybowrd input"); }
if quit_hotkey.matches(&modifiers, &event.physical_key) {
log::debug!("quitting");
WindowEvent::KeyboardInput { event, .. } => {
if quit_hotkey.matches(&modifiers, &event.physical_key) {
webviews.remove(&window_id); webviews.remove(&window_id);
if webviews.is_empty() { if webviews.is_empty() {
*control_flow = ControlFlow::Exit; *control_flow = ControlFlow::Exit;
} }
} }
// println!(
// "KeyEvent: `Shift` + `1` | logical_key: {:?}",
// &event.logical_key
// );
// we can match manually without `Accelerator`
// else if event.key_without_modifiers() == Key::Character("1")
// && modifiers.is_empty()
// {
// println!("KeyEvent: `1`");
// }
} }
WindowEvent::Resized(_) | WindowEvent::Moved(_) => { WindowEvent::Resized(_) | WindowEvent::Moved(_) => {
@@ -240,7 +140,18 @@ pub fn run<T: 'static + Send + Sync>(
_ => {} _ => {}
}, },
Event::MainEventsCleared => {} Event::MainEventsCleared => {
// I hate this ready hack but it's needed to wait for the "onload" to occur
// We can't run any initializion scripts because the window isn't ready yet?
if is_ready.load(std::sync::atomic::Ordering::Relaxed) {
let mut queue = edit_queue.write().unwrap();
let (id, view) = webviews.iter_mut().next().unwrap();
while let Some(edit) = queue.pop_back() {
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
.unwrap();
}
}
}
Event::Resumed => {} Event::Resumed => {}
Event::Suspended => {} Event::Suspended => {}
Event::LoopDestroyed => {} Event::LoopDestroyed => {}
@@ -250,19 +161,11 @@ pub fn run<T: 'static + Send + Sync>(
}) })
} }
pub fn start<P: 'static + Send>(
root: FC<P>,
config_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
) -> ((), ()) {
//
((), ())
}
// Create a new tokio runtime on a dedicated thread and then launch the apps VirtualDom. // Create a new tokio runtime on a dedicated thread and then launch the apps VirtualDom.
pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>( pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>(
root: FC<P>, root: FC<P>,
props: P, props: P,
event_tx: tokio::sync::mpsc::UnboundedSender<BridgeEvent>, edit_queue: Arc<RwLock<VecDeque<String>>>,
) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> { ) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>(); let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
let return_sender = sender.clone(); let return_sender = sender.clone();
@@ -275,47 +178,107 @@ pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>(
.unwrap(); .unwrap();
runtime.block_on(async move { runtime.block_on(async move {
let mut vir = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver); let mut dom = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver);
let _ = vir.get_scheduler_channel();
let edits = vir.rebuild(); let edits = dom.rebuild();
// the receiving end expects something along these lines edit_queue
#[derive(Serialize)] .write()
struct Evt<'a> { .unwrap()
edits: Vec<DomEdit<'a>>, .push_front(serde_json::to_string(&edits.edits).unwrap());
}
let edit_string = serde_json::to_value(Evt { edits: edits.edits }).unwrap();
event_tx
.send(BridgeEvent::Initialize(edit_string))
.expect("Sending should not fail");
loop { loop {
vir.wait_for_work().await; dom.wait_for_work().await;
// we're running on our own thread, so we don't need to worry about blocking anything let mut muts = dom.work_with_deadline(|| false);
// todo: maybe we want to schedule ourselves in
// on average though, the virtualdom running natively is stupid fast
let mut muts = vir.work_with_deadline(|| false);
log::debug!("finished running with deadline");
let mut edits = vec![];
while let Some(edit) = muts.pop() { while let Some(edit) = muts.pop() {
let edit_string = serde_json::to_value(Evt { edits: edit.edits }) edit_queue
.expect("serializing edits should never fail"); .write()
edits.push(edit_string); .unwrap()
.push_front(serde_json::to_string(&edit.edits).unwrap());
} }
event_tx
.send(BridgeEvent::Update(serde_json::Value::Array(edits)))
.expect("Sending should not fail");
} }
}) })
}); });
return_sender return_sender
} }
fn build_menu() -> MenuBar {
// create main menubar menu
let mut menu_bar_menu = MenuBar::new();
// create `first_menu`
let mut first_menu = MenuBar::new();
first_menu.add_native_item(MenuItem::About("Todos".to_string()));
first_menu.add_native_item(MenuItem::Services);
first_menu.add_native_item(MenuItem::Separator);
first_menu.add_native_item(MenuItem::Hide);
first_menu.add_native_item(MenuItem::HideOthers);
first_menu.add_native_item(MenuItem::ShowAll);
first_menu.add_native_item(MenuItem::Quit);
first_menu.add_native_item(MenuItem::CloseWindow);
// create second menu
let mut second_menu = MenuBar::new();
// second_menu.add_submenu("Sub menu", true, my_sub_menu);
second_menu.add_native_item(MenuItem::Copy);
second_menu.add_native_item(MenuItem::Paste);
second_menu.add_native_item(MenuItem::SelectAll);
menu_bar_menu.add_submenu("First menu", true, first_menu);
menu_bar_menu.add_submenu("Second menu", true, second_menu);
menu_bar_menu
}
fn create_window(event_loop: &EventLoopWindowTarget<()>) -> Window {
WindowBuilder::new()
.with_maximized(true)
.with_menu(build_menu())
.with_title("Dioxus App")
.build(event_loop)
.unwrap()
}
fn create_webview(
window: Window,
is_ready: Arc<AtomicBool>,
sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
) -> WebView {
WebViewBuilder::new(window)
.unwrap()
.with_url("wry://index.html")
.unwrap()
.with_rpc_handler(move |_window: &Window, mut req: RpcRequest| {
match req.method.as_str() {
"user_event" => {
let event = events::trigger_from_serialized(req.params.unwrap());
log::debug!("User event: {:?}", event);
sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap();
}
"initialize" => {
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
}
_ => {}
}
None
})
// Any content that that uses the `wry://` scheme will be shuttled through this handler as a "special case"
// For now, we only serve two pieces of content which get included as bytes into the final binary.
.with_custom_protocol("wry".into(), move |request| {
let path = request.uri().replace("wry://", "");
let (data, meta) = match path.as_str() {
"index.html" => (include_bytes!("./index.html").to_vec(), "text/html"),
"index.html/index.js" => (include_bytes!("./index.js").to_vec(), "text/javascript"),
_ => unimplemented!("path {}", path),
};
wry::http::ResponseBuilder::new().mimetype(meta).body(data)
})
.build()
.unwrap()
}
View File
Whitespace-only changes.
-2
View File
@@ -17,8 +17,6 @@ pub(crate) struct ProvidedStateInner<T> {
impl<T> ProvidedStateInner<T> { impl<T> ProvidedStateInner<T> {
pub(crate) fn notify_consumers(&mut self) { pub(crate) fn notify_consumers(&mut self) {
for consumer in self.consumers.iter() { for consumer in self.consumers.iter() {
println!("notifiying {:?}", consumer);
// log::debug("notifiying {:?}", consumer);
(self.notify_any)(*consumer); (self.notify_any)(*consumer);
} }
} }
+4 -1
View File
@@ -126,7 +126,10 @@ impl<'a, T: 'static> UseState<'a, T> {
pub fn setter(&self) -> Rc<dyn Fn(T)> { pub fn setter(&self) -> Rc<dyn Fn(T)> {
let slot = self.inner.wip.clone(); let slot = self.inner.wip.clone();
Rc::new(move |new| *slot.borrow_mut() = Some(new)) Rc::new(move |new| {
//
*slot.borrow_mut() = Some(new);
})
} }
pub fn for_async(&self) -> AsyncUseState<T> { pub fn for_async(&self) -> AsyncUseState<T> {
+5 -4
View File
@@ -5,6 +5,7 @@ use std::any::Any;
pub mod on { pub mod on {
use super::*; use super::*;
use std::sync::Arc;
macro_rules! event_directory { macro_rules! event_directory {
( $( ( $(
$( #[$attr:meta] )* $( #[$attr:meta] )*
@@ -23,19 +24,19 @@ pub mod on {
c: NodeFactory<'a>, c: NodeFactory<'a>,
mut callback: F, mut callback: F,
) -> Listener<'a> ) -> Listener<'a>
where F: FnMut($wrapper) + 'a where F: FnMut(Arc<$wrapper>) + 'a
{ {
let bump = &c.bump(); let bump = &c.bump();
// we can't allocate unsized in bumpalo's box, so we need to craft the box manually // we can't allocate unsized in bumpalo's box, so we need to craft the box manually
// safety: this is essentially the same as calling Box::new() but manually // safety: this is essentially the same as calling Box::new() but manually
// The box is attached to the lifetime of the bumpalo allocator // The box is attached to the lifetime of the bumpalo allocator
let cb: &mut dyn FnMut(Box<dyn Any + Send>) = bump.alloc(move |evt: Box<dyn Any + Send>| { let cb: &mut dyn FnMut(Arc<dyn Any + Send + Sync>) = bump.alloc(move |evt: Arc<dyn Any + Send + Sync>| {
let event = evt.downcast::<$wrapper>().unwrap(); let event = evt.downcast::<$wrapper>().unwrap();
callback(*event) callback(event)
}); });
let callback: BumpBox<dyn FnMut(Box<dyn Any + Send>) + 'a> = unsafe { BumpBox::from_raw(cb) }; let callback: BumpBox<dyn FnMut(Arc<dyn Any + Send + Sync>) + 'a> = unsafe { BumpBox::from_raw(cb) };
// ie oncopy // ie oncopy
let event_name = stringify!($name); let event_name = stringify!($name);
+29
View File
@@ -0,0 +1,29 @@
//! Example: README.md showcase
//!
//! The example from the README.md.
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_hooks::use_state;
use dioxus_html as dioxus_elements;
use dioxus_web;
use gloo_timers::future::TimeoutFuture;
fn main() {
dioxus_web::launch(App, |c| c);
}
static App: FC<()> = |cx, props| {
let mut count = use_state(cx, || 0);
cx.push_task(|| async move {
TimeoutFuture::new(100).await;
count += 1;
});
rsx!(cx, div {
h3 { "High-Five counter: {count}" }
button { onclick: move |_| count.set(0), "Reset!" }
})
};
+40
View File
@@ -0,0 +1,40 @@
#![allow(non_upper_case_globals)]
//! Example: README.md showcase
//!
//! The example from the README.md.
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
use dioxus_web;
fn main() {
dioxus_web::launch(App, |c| c);
}
static App: FC<()> = |cx, _| {
let doggo = cx.suspend(|| async move {
#[derive(serde::Deserialize)]
struct Doggo {
message: String,
}
let src = reqwest::get("https://dog.ceo/api/breeds/image/random")
.await
.expect("Failed to fetch doggo")
.json::<Doggo>()
.await
.expect("Failed to parse doggo")
.message;
rsx!(cx, img { src: "{src}" })
});
rsx!(cx, div {
h1 {"One doggo coming right up"}
button { onclick: move |_| cx.needs_update(), "Get a new doggo" }
{doggo}
})
};
+26 -24
View File
@@ -9,7 +9,7 @@
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent};
use fxhash::FxHashMap; use fxhash::FxHashMap;
use std::{any::Any, fmt::Debug, rc::Rc}; use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc};
use wasm_bindgen::{closure::Closure, JsCast}; use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{ use web_sys::{
CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
@@ -92,7 +92,7 @@ impl WebsysDom {
// .as_ref() // .as_ref()
// .unwrap() // .unwrap()
// .clone(); // .clone();
// bla.set(Box::new(node)).unwrap(); // bla.set(Arc::new(node)).unwrap();
// } // }
// } // }
// } // }
@@ -286,14 +286,16 @@ impl WebsysDom {
} else { } else {
let trigger = self.sender_callback.clone(); let trigger = self.sender_callback.clone();
let handler = Closure::wrap(Box::new(move |event: &web_sys::Event| { let c: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
// "Result" cannot be received from JS // "Result" cannot be received from JS
// Instead, we just build and immediately execute a closure that returns result // Instead, we just build and immediately execute a closure that returns result
match decode_trigger(event) { match decode_trigger(event) {
Ok(synthetic_event) => trigger.as_ref()(SchedulerMsg::UiEvent(synthetic_event)), Ok(synthetic_event) => trigger.as_ref()(SchedulerMsg::UiEvent(synthetic_event)),
Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
}; };
}) as Box<dyn FnMut(&Event)>); });
let handler = Closure::wrap(c);
self.root self.root
.add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref())
@@ -483,21 +485,21 @@ unsafe impl Sync for DioxusWebsysEvent {}
// todo: some of these events are being casted to the wrong event type. // todo: some of these events are being casted to the wrong event type.
// We need tests that simulate clicks/etc and make sure every event type works. // We need tests that simulate clicks/etc and make sure every event type works.
fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send> { fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send + Sync> {
use dioxus_html::on::*; use dioxus_html::on::*;
use dioxus_html::KeyCode; use dioxus_html::KeyCode;
// use dioxus_core::events::on::*; // use dioxus_core::events::on::*;
match event.type_().as_str() { match event.type_().as_str() {
"copy" | "cut" | "paste" => Box::new(ClipboardEvent {}), "copy" | "cut" | "paste" => Arc::new(ClipboardEvent {}),
"compositionend" | "compositionstart" | "compositionupdate" => { "compositionend" | "compositionstart" | "compositionupdate" => {
let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap();
Box::new(CompositionEvent { Arc::new(CompositionEvent {
data: evt.data().unwrap_or_default(), data: evt.data().unwrap_or_default(),
}) })
} }
"keydown" | "keypress" | "keyup" => { "keydown" | "keypress" | "keyup" => {
let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap(); let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap();
Box::new(KeyboardEvent { Arc::new(KeyboardEvent {
alt_key: evt.alt_key(), alt_key: evt.alt_key(),
char_code: evt.char_code(), char_code: evt.char_code(),
key: evt.key(), key: evt.key(),
@@ -513,7 +515,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send>
} }
"focus" | "blur" => { "focus" | "blur" => {
// //
Box::new(FocusEvent {}) Arc::new(FocusEvent {})
} }
// "change" => SyntheticEvent::GenericEvent(DioxusEvent::new((), DioxusWebsysEvent(event))), // "change" => SyntheticEvent::GenericEvent(DioxusEvent::new((), DioxusWebsysEvent(event))),
@@ -558,13 +560,13 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send>
}) })
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
Box::new(FormEvent { value }) Arc::new(FormEvent { value })
} }
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap(); let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap();
Box::new(MouseEvent { Arc::new(MouseEvent {
alt_key: evt.alt_key(), alt_key: evt.alt_key(),
button: evt.button(), button: evt.button(),
buttons: evt.buttons(), buttons: evt.buttons(),
@@ -582,7 +584,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send>
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap(); let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap();
Box::new(PointerEvent { Arc::new(PointerEvent {
alt_key: evt.alt_key(), alt_key: evt.alt_key(),
button: evt.button(), button: evt.button(),
buttons: evt.buttons(), buttons: evt.buttons(),
@@ -608,11 +610,11 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send>
// get_modifier_state: evt.get_modifier_state(), // get_modifier_state: evt.get_modifier_state(),
}) })
} }
"select" => Box::new(SelectionEvent {}), "select" => Arc::new(SelectionEvent {}),
"touchcancel" | "touchend" | "touchmove" | "touchstart" => { "touchcancel" | "touchend" | "touchmove" | "touchstart" => {
let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap(); let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap();
Box::new(TouchEvent { Arc::new(TouchEvent {
alt_key: evt.alt_key(), alt_key: evt.alt_key(),
ctrl_key: evt.ctrl_key(), ctrl_key: evt.ctrl_key(),
meta_key: evt.meta_key(), meta_key: evt.meta_key(),
@@ -620,11 +622,11 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send>
}) })
} }
"scroll" => Box::new(()), "scroll" => Arc::new(()),
"wheel" => { "wheel" => {
let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap();
Box::new(WheelEvent { Arc::new(WheelEvent {
delta_x: evt.delta_x(), delta_x: evt.delta_x(),
delta_y: evt.delta_y(), delta_y: evt.delta_y(),
delta_z: evt.delta_z(), delta_z: evt.delta_z(),
@@ -634,7 +636,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send>
"animationstart" | "animationend" | "animationiteration" => { "animationstart" | "animationend" | "animationiteration" => {
let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap(); let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap();
Box::new(AnimationEvent { Arc::new(AnimationEvent {
elapsed_time: evt.elapsed_time(), elapsed_time: evt.elapsed_time(),
animation_name: evt.animation_name(), animation_name: evt.animation_name(),
pseudo_element: evt.pseudo_element(), pseudo_element: evt.pseudo_element(),
@@ -643,7 +645,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send>
"transitionend" => { "transitionend" => {
let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap(); let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap();
Box::new(TransitionEvent { Arc::new(TransitionEvent {
elapsed_time: evt.elapsed_time(), elapsed_time: evt.elapsed_time(),
property_name: evt.property_name(), property_name: evt.property_name(),
pseudo_element: evt.pseudo_element(), pseudo_element: evt.pseudo_element(),
@@ -655,15 +657,15 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send>
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => { | "timeupdate" | "volumechange" | "waiting" => {
// //
Box::new(MediaEvent {}) Arc::new(MediaEvent {})
} }
"toggle" => { "toggle" => {
// //
Box::new(ToggleEvent {}) Arc::new(ToggleEvent {})
} }
_ => Box::new(()), _ => Arc::new(()),
} }
} }
@@ -701,9 +703,9 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
Ok(UserEvent { Ok(UserEvent {
name: event_name_from_typ(&typ), name: event_name_from_typ(&typ),
event: virtual_event_from_websys_event(event.clone()), data: virtual_event_from_websys_event(event.clone()),
mounted_dom_id: Some(ElementId(real_id as usize)), element: Some(ElementId(real_id as usize)),
scope_id: ScopeId(triggered_scope as usize), scope_id: Some(ScopeId(triggered_scope as usize)),
priority: dioxus_core::EventPriority::Medium, priority: dioxus_core::EventPriority::Medium,
}) })
} }
+1 -1
View File
@@ -193,7 +193,7 @@ pub mod debug {}
pub mod prelude { pub mod prelude {
//! A glob import that includes helper types like FC, rsx!, html!, and required traits //! A glob import that includes helper types like FC, rsx!, html!, and required traits
pub use dioxus_core::prelude::*; pub use dioxus_core::prelude::*;
pub use dioxus_core_macro::{format_args_f, rsx, Props}; pub use dioxus_core_macro::{format_args_f, rsx, Props, Routable};
pub use dioxus_elements::{GlobalAttributes, SvgAttributes}; pub use dioxus_elements::{GlobalAttributes, SvgAttributes};
pub use dioxus_hooks::*; pub use dioxus_hooks::*;
pub use dioxus_html as dioxus_elements; pub use dioxus_html as dioxus_elements;