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.
## 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
This project is licensed under the [MIT license].
+14 -3
View File
@@ -2,6 +2,8 @@
//!
//! The example from the README.md.
use std::time::Duration;
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App, |c| c);
@@ -10,14 +12,23 @@ fn main() {
static App: FC<()> = |cx, props| {
let mut count = use_state(cx, || 0);
cx.push_task(async {
panic!("polled future");
//
cx.push_task(async move {
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! {
div {
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,
/// #[at("/secure")]
/// Secure,
/// #[at("/profile/{id}")]
/// Profile(u32),
/// #[at("/404")]
/// NotFound,
/// }
+25 -24
View File
@@ -200,11 +200,11 @@ pub fn routable_derive_impl(input: Routable) -> TokenStream {
);
quote! {
::std::thread_local! {
#[doc(hidden)]
#[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);
}
// ::std::thread_local! {
// #[doc(hidden)]
// #[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);
// }
#[automatically_derived]
impl ::dioxus::router::Routable for #ident {
@@ -219,29 +219,30 @@ pub fn routable_derive_impl(input: Routable) -> TokenStream {
#not_found_route
}
fn current_route() -> ::std::option::Option<Self> {
#cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow()))
}
// fn current_route() -> ::std::option::Option<Self> {
// #cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow()))
// }
fn recognize(pathname: &str) -> ::std::option::Option<Self> {
::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 = ::std::clone::Clone::clone(&route);
#cache_thread_local_ident.with(move |val| {
*val.borrow_mut() = route;
});
}
route
todo!()
// ::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 = ::std::clone::Clone::clone(&route);
// #cache_thread_local_ident.with(move |val| {
// *val.borrow_mut() = route;
// });
// }
// route
}
fn cleanup() {
#cache_thread_local_ident.with(move |val| {
*val.borrow_mut() = ::std::option::Option::None;
});
}
// fn cleanup() {
// #cache_thread_local_ident.with(move |val| {
// *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
serde = { version = "1", features = ["derive"], optional = true }
backtrace = "0.3.63"
[dev-dependencies]
anyhow = "1.0.42"
+5 -5
View File
@@ -36,11 +36,11 @@ impl<const A: bool> FragmentBuilder<A> {
///
/// ## Example
///
/// ```rust
/// ```rust, ignore
/// fn App(cx: Context, props: &()) -> Element {
/// cx.render(rsx!{
/// CustomCard {
/// h1 {}
/// h1 {}2
/// p {}
/// }
/// })
@@ -82,7 +82,7 @@ impl Properties for FragmentProps {
///
/// ## Example
///
/// ```rust
/// ```rust, ignore
/// rsx!{
/// Fragment { key: "abc" }
/// }
@@ -118,7 +118,7 @@ pub fn Fragment(cx: Context, props: &FragmentProps) -> Element {
/// ## Example
///
/// For props that are 'static:
/// ```rust ignore
/// ```rust, ignore ignore
/// #[derive(Props, PartialEq)]
/// struct MyProps {
/// data: String
@@ -127,7 +127,7 @@ pub fn Fragment(cx: Context, props: &FragmentProps) -> Element {
///
/// For props that borrow:
///
/// ```rust ignore
/// ```rust, ignore ignore
/// #[derive(Props)]
/// struct MyProps<'a >{
/// 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
//! 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:
//! ```rust
//! ```rust, ignore
//! 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
@@ -147,6 +147,7 @@ pub(crate) enum DiffInstruction<'a> {
},
PopScope,
PopElement,
}
#[derive(Debug, Clone, Copy)]
@@ -162,30 +163,32 @@ pub(crate) struct DiffStack<'bump> {
pub(crate) instructions: Vec<DiffInstruction<'bump>>,
nodes_created_stack: SmallVec<[usize; 10]>,
pub scope_stack: SmallVec<[ScopeId; 5]>,
pub element_stack: SmallVec<[ElementId; 10]>,
}
impl<'bump> DiffStack<'bump> {
pub fn new() -> Self {
fn new() -> Self {
Self {
instructions: Vec::with_capacity(1000),
nodes_created_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()
}
pub fn pop_off_scope(&mut self) {
fn pop_off_scope(&mut self) {
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)
}
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.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.instructions.push(DiffInstruction::Mount {
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);
}
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.instructions.push(DiffInstruction::Mount { and });
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;
}
pub fn pop_nodes_created(&mut self) -> usize {
fn pop_nodes_created(&mut self) -> usize {
self.nodes_created_stack.pop().unwrap()
}
pub fn current_scope(&self) -> Option<ScopeId> {
fn current_scope(&self) -> Option<ScopeId> {
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
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));
self.stack.push(DiffInstruction::Diff { old, new });
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);
}
@@ -263,10 +267,13 @@ impl<'bump> DiffState<'bump> {
self.stack.add_child_count(num_on_stack);
}
DiffInstruction::PopScope => self.stack.pop_off_scope(),
DiffInstruction::PopElement => {
self.stack.element_stack.pop();
}
};
if deadline_expired() {
log::debug!("Deadline expired before we could finished!");
log::debug!("Deadline expired before we could finish!");
return false;
}
}
@@ -358,6 +365,7 @@ impl<'bump> DiffState<'bump> {
fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) {
let real_id = self.scopes.reserve_node(node);
self.mutations.create_text_node(vtext.text, real_id);
vtext.dom_id.set(Some(real_id));
self.stack.add_child_count(1);
@@ -390,11 +398,19 @@ impl<'bump> DiffState<'bump> {
children,
namespace,
dom_id,
parent_id,
..
} = 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));
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 fc_ptr = vcomponent.user_fc;
let new_idx = self
.scopes
.new_with_key(fc_ptr, caller, parent_scope, height, subtree);
let container = *self.stack.element_stack.last().unwrap();
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
vcomponent.associated_scope.set(Some(new_idx));
@@ -495,7 +515,9 @@ impl<'bump> DiffState<'bump> {
use VNode::*;
match (old_node, new_node) {
// 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)) => {
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 old.text != new.text {
self.mutations.set_text(new.text, root.as_u64());
}
self.scopes.update_node(new_node, root);
new.dom_id.set(Some(root));
}
@@ -551,7 +580,10 @@ impl<'bump> DiffState<'bump> {
return;
}
self.scopes.update_node(new_node, 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
// element to modify its attributes.
@@ -617,8 +649,12 @@ impl<'bump> DiffState<'bump> {
self.mutations.edits.push(PushRoot {
root: root.as_u64(),
});
self.stack.element_stack.push(root);
self.stack.instructions.push(DiffInstruction::PopElement);
self.stack.create_children(new.children, MountType::Append);
} else {
self.stack.element_stack.push(root);
self.stack.instructions.push(DiffInstruction::PopElement);
self.diff_children(old.children, new.children);
}
}
@@ -650,20 +686,18 @@ impl<'bump> DiffState<'bump> {
let scope = unsafe {
self.scopes
.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) };
// React doesn't automatically memoize, but we do.
let props_are_the_same = old.comparator.unwrap();
if self.force_diff || !props_are_the_same(new) {
if self.scopes.run_scope(&scope_addr) {
self.diff_node(
self.scopes.wip_head(&scope_addr),
self.scopes.fin_head(&scope_addr),
);
}
if (self.force_diff || !props_are_the_same(new)) && self.scopes.run_scope(&scope_addr) {
self.diff_node(
self.scopes.wip_head(&scope_addr),
self.scopes.fin_head(&scope_addr),
);
}
self.stack.scope_stack.pop();
@@ -856,13 +890,7 @@ impl<'bump> DiffState<'bump> {
Some(count) => count,
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
// within which to re-order nodes with the same keys, remove old nodes with
// now-unused keys, and create new nodes with fresh keys.
@@ -911,8 +939,6 @@ impl<'bump> DiffState<'bump> {
} else {
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.
@@ -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
// create the new children afresh.
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) {
self.remove_nodes(&old[1..], true);
self.stack
@@ -1206,13 +1223,21 @@ impl<'bump> DiffState<'bump> {
fn replace_node(&mut self, old: &'bump VNode<'bump>, nodes_created: usize) {
match old {
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.remove_nodes(el.children, false);
}
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);
}
@@ -1226,7 +1251,6 @@ impl<'bump> DiffState<'bump> {
self.replace_node(node, nodes_created);
let scope_id = c.associated_scope.get().unwrap();
println!("replacing c {:?} ", scope_id);
log::debug!("Destroying scope {:?}", scope_id);
self.scopes.try_remove(&scope_id).unwrap();
}
@@ -1320,13 +1344,14 @@ impl<'bump> DiffState<'bump> {
.current_scope()
.and_then(|id| self.scopes.get_scope(&id))
{
// safety: this lifetime is managed by the logic on scope
let extended = unsafe { std::mem::transmute(suspended) };
scope
.items
.borrow_mut()
.suspended_nodes
.insert(suspended.task_id, extended);
todo!()
// // safety: this lifetime is managed by the logic on scope
// let extended = unsafe { std::mem::transmute(suspended) };
// scope
// .items
// .borrow_mut()
// .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.
///
///
/// ```rust
/// ```rust, ignore
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
/// ```
pub struct LazyNodes<'a, 'b> {
@@ -47,7 +47,14 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
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
@@ -212,8 +219,6 @@ fn round_to_words(len: usize) -> usize {
fn it_works() {
let bump = bumpalo::Bump::new();
simple_logger::init().unwrap();
let factory = NodeFactory { bump: &bump };
let caller = NodeFactory::annotate_lazy(|f| {
@@ -232,9 +237,7 @@ fn it_drops() {
use std::rc::Rc;
let bump = bumpalo::Bump::new();
simple_logger::init().unwrap();
// let factory = NodeFactory { scope: &bump };
let factory = NodeFactory { bump: &bump };
struct DropInner {
id: i32,
@@ -246,7 +249,7 @@ fn it_drops() {
}
let val = Rc::new(10);
{
let caller = {
let it = (0..10)
.map(|i| {
let val = val.clone();
@@ -259,16 +262,13 @@ fn it_drops() {
})
.collect::<Vec<_>>();
let caller = NodeFactory::annotate_lazy(|f| {
NodeFactory::annotate_lazy(|f| {
log::debug!("main closure");
f.fragment_from_iter(it)
})
.unwrap();
}
.unwrap()
};
// let nodes = caller.call(factory);
// dbg!(nodes);
dbg!(Rc::strong_count(&val));
let _ = caller.call(factory);
assert_eq!(Rc::strong_count(&val), 1);
}
+87 -102
View File
@@ -5,28 +5,111 @@
use crate::innerlude::*;
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 edits: Vec<DomEdit<'a>>,
pub noderefs: Vec<NodeRefMutation<'a>>,
pub refs: Vec<NodeRefMutation<'a>>,
pub effects: Vec<&'a dyn FnMut()>,
}
impl Debug for Mutations<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Mutations")
.field("edits", &self.edits)
.field("noderefs", &self.noderefs)
// .field("effects", &self.effects)
.field("noderefs", &self.refs)
.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::*;
impl<'a> Mutations<'a> {
pub(crate) fn new() -> Self {
Self {
edits: Vec::new(),
noderefs: Vec::new(),
refs: Vec::new(),
effects: Vec::new(),
}
}
@@ -156,101 +239,3 @@ impl<'a> NodeRefMutation<'a> {
.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,
cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter},
sync::Arc,
};
/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM.
@@ -25,7 +26,7 @@ pub enum VNode<'src> {
///
/// # Example
///
/// ```
/// ```rust, ignore
/// let mut vdom = VirtualDom::new();
/// let node = vdom.render_vnode(rsx!( "hello" ));
///
@@ -41,7 +42,7 @@ pub enum VNode<'src> {
///
/// # Example
///
/// ```rust
/// ```rust, ignore
/// let mut vdom = VirtualDom::new();
///
/// let node = vdom.render_vnode(rsx!{
@@ -66,7 +67,7 @@ pub enum VNode<'src> {
///
/// # Example
///
/// ```rust
/// ```rust, ignore
/// rsx!{
/// a {}
/// link {}
@@ -81,7 +82,7 @@ pub enum VNode<'src> {
///
/// # Example
///
/// ```rust
/// ```rust, ignore
/// fn Example(cx: Context, props: &()) -> Element {
/// todo!()
/// }
@@ -100,11 +101,11 @@ pub enum VNode<'src> {
///
/// # 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.
///
@@ -112,7 +113,7 @@ pub enum VNode<'src> {
///
/// # Example
///
/// ```rust
/// ```rust, ignore
/// let mut vdom = VirtualDom::new();
///
/// let node = vdom.render_vnode(rsx!( Fragment {} ));
@@ -133,7 +134,7 @@ pub enum VNode<'src> {
/// an `rsx!` call.
///
/// # Example
/// ```rust
/// ```rust, ignore
/// let mut vdom = VirtualDom::new();
///
/// let node: NodeLink = vdom.render_vnode(rsx!( "hello" ));
@@ -202,7 +203,7 @@ impl<'src> VNode<'src> {
VNode::Linked(c) => VNode::Linked(NodeLink {
scope_id: c.scope_id.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.
pub struct VText<'src> {
pub text: &'src str,
pub dom_id: Cell<Option<ElementId>>,
pub is_static: bool,
}
@@ -275,19 +274,12 @@ pub struct VFragment<'src> {
/// An element like a "div" with children, listeners, and attributes.
pub struct VElement<'a> {
pub tag_name: &'static str,
pub namespace: Option<&'static str>,
pub key: Option<&'a str>,
pub dom_id: Cell<Option<ElementId>>,
pub parent_id: Cell<Option<ElementId>>,
pub listeners: &'a [Listener<'a>],
pub attributes: &'a [Attribute<'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.
///
/// ```rust
/// ```rust, ignore
/// struct my_element;
///
/// impl DioxusElement for my_element {
@@ -365,10 +357,10 @@ pub struct Listener<'bump> {
pub event: &'static str,
/// 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
/// Only supports the functional syntax
pub struct VComponent<'src> {
@@ -381,7 +373,7 @@ pub struct VComponent<'src> {
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
pub(crate) bump_props: *const (),
@@ -395,12 +387,11 @@ pub struct VComponent<'src> {
pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>,
}
pub struct VSuspended<'a> {
pub task_id: u64,
pub struct VSuspended {
pub task_id: usize,
pub scope: Cell<Option<ScopeId>>,
pub dom_id: Cell<Option<ElementId>>,
#[allow(clippy::type_complexity)]
pub callback: RefCell<Option<BumpBox<'a, dyn FnMut() -> Element + 'a>>>,
pub parent: Cell<Option<ElementId>>,
}
/// 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
}
}
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.
///
@@ -453,7 +434,7 @@ impl<'a> NodeFactory<'a> {
#[inline]
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.
@@ -591,14 +572,13 @@ impl<'a> NodeFactory<'a> {
// - Because FC<P> is the same, then P must be the same (even with generics)
// - Non-static P are autoderived to memoize as false
// - 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
// 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 {
false
}
@@ -633,8 +613,7 @@ impl<'a> NodeFactory<'a> {
let caller: &'a mut dyn Fn(&'a Scope) -> Element =
bump.alloc(move |scope: &Scope| -> Element {
let props: &'_ P = unsafe { &*(bump_props as *const P) };
let res = component(scope, props);
res
component(scope, props)
});
let can_memoize = P::IS_STATIC;
@@ -648,14 +627,14 @@ impl<'a> NodeFactory<'a> {
can_memoize,
drop_props,
associated_scope: Cell::new(None),
hard_allocation: Cell::new(None),
_hard_allocation: Cell::new(None),
}))
}
pub fn listener(
self,
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 {
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,
node_iter: impl IntoIterator<Item = impl IntoVNode<'a>>,
node_iter: impl IntoIterator<Item = impl IntoVNode<'a> + 'c> + 'b,
) -> VNode<'a> {
let bump = self.bump;
let mut nodes = bumpalo::collections::Vec::new_in(bump);
@@ -683,24 +662,25 @@ impl<'a> NodeFactory<'a> {
let children = nodes.into_bump_slice();
// TODO
// We need a dedicated path in the rsx! macro that will trigger the "you need keys" warning
//
// if cfg!(debug_assertions) {
// if children.len() > 1 {
// if children.last().unwrap().key().is_none() {
// log::error!(
// r#"
// 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.
// See docs.rs/dioxus for more information.
// ---
// To help you identify where this error is coming from, we've generated a backtrace.
// "#,
// );
// }
// }
// }
// todo: add a backtrace
if cfg!(debug_assertions) && children.len() > 1 && children.last().unwrap().key().is_none()
{
// todo: rsx! calls get turned into fragments which means they always trip this error
//
//
// use backtrace::Backtrace;
// let bt = Backtrace::new();
log::error!(
r#"
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.
See docs.rs/dioxus for more information.
-------------
"#,
// bt
);
}
VNode::Fragment(VFragment {
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.
/// We pipe basically everything through `fragment_from_iter`, so we expect a very specific type:
/// ```
/// ```rust, ignore
/// 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> {
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
/// use case they might have.
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) container: ElementId,
pub(crate) our_arena_idx: ScopeId,
@@ -60,23 +56,14 @@ pub struct Scope {
pub(crate) generation: Cell<u32>,
// The double-buffering situation that we will use
pub(crate) frames: [BumpFrame; 2],
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>>,
// State
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) sender: UnboundedSender<SchedulerMsg>,
@@ -85,7 +72,7 @@ pub struct Scope {
pub struct SelfReferentialItems<'a> {
pub(crate) listeners: Vec<&'a Listener<'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) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>,
}
@@ -108,7 +95,7 @@ impl Scope {
///
/// # Example
///
/// ```rust
/// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild();
///
@@ -120,13 +107,39 @@ impl Scope {
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.
///
/// A Scope with a height of `0` is the root scope - there are no other scopes above it.
///
/// # Example
///
/// ```rust
/// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild();
///
@@ -146,7 +159,7 @@ impl Scope {
///
/// # Example
///
/// ```rust
/// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild();
///
@@ -155,10 +168,7 @@ impl Scope {
/// assert_eq!(base.parent(), None);
/// ```
pub fn parent(&self) -> Option<ScopeId> {
match self.parent_scope {
Some(p) => Some(unsafe { &*p }.our_arena_idx),
None => None,
}
self.parent_scope.map(|p| unsafe { &*p }.our_arena_idx)
}
/// Get the ID of this Scope within this Dioxus VirtualDOM.
@@ -167,7 +177,7 @@ impl Scope {
///
/// # Example
///
/// ```rust
/// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild();
/// let base = dom.base_scope();
@@ -186,6 +196,7 @@ impl Scope {
let chan = self.sender.clone();
let id = self.scope_id();
Rc::new(move || {
log::debug!("set on channel an update for scope {:?}", id);
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
})
}
@@ -223,6 +234,104 @@ impl Scope {
&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.
///
/// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned.
@@ -257,132 +366,29 @@ impl Scope {
Some(link)
}
/// 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()());
pub fn suspend<'src, F: Future<Output = Element> + 'src>(
&'src self,
mut fut: impl FnMut() -> F,
) -> Option<VNode> {
let channel = self.sender.clone();
let node_fut = fut();
// wrap it in a type that will actually drop the contents
let boxed_fut = unsafe { BumpBox::from_raw(fut) };
let scope = self.scope_id();
// erase the 'src lifetime for self-referential storage
let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
// self.push_task(move || {
//
// 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();
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()
todo!()
}
/// 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 {
match self.generation.get() & 1 == 1 {
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
let bump = self.wip_frame() as *const _ as *mut Bump;
let g = &mut *bump;
g.reset();
let bump = self.wip_frame_mut();
bump.bump.reset();
}
pub fn cycle_frame(&self) {
pub(crate) fn cycle_frame(&self) {
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.
// Suspended nodes cannot be called repeatedly.
pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) {
let mut nodes = &mut self.items.get_mut().suspended_nodes;
if let Some(suspended) = nodes.remove(&task_id) {
let sus: &'a VSuspended<'static> = unsafe { &*suspended };
let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) };
let mut boxed = sus.callback.borrow_mut().take().unwrap();
let new_node: Element = boxed();
let sus: &'a VSuspended = suspended;
// let mut boxed = sus.callback.borrow_mut().take().unwrap();
// 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();
unsafe { std::mem::transmute(&*node) }
}
+88 -35
View File
@@ -1,8 +1,11 @@
use bumpalo::Bump;
use futures_channel::mpsc::UnboundedSender;
use fxhash::FxHashMap;
use fxhash::{FxHashMap, FxHashSet};
use slab::Slab;
use std::cell::{Cell, RefCell};
use std::{
borrow::Borrow,
cell::{Cell, RefCell},
};
use crate::innerlude::*;
@@ -19,8 +22,9 @@ pub struct Heuristic {
// has an internal heuristics engine to pre-allocate arenas to the right size
pub(crate) struct ScopeArena {
bump: Bump,
pub pending_futures: RefCell<FxHashSet<ScopeId>>,
scope_counter: Cell<usize>,
scopes: RefCell<FxHashMap<ScopeId, *mut Scope>>,
pub scopes: RefCell<FxHashMap<ScopeId, *mut Scope>>,
pub heuristics: RefCell<FxHashMap<FcSlot, Heuristic>>,
free_scopes: RefCell<Vec<*mut Scope>>,
nodes: RefCell<Slab<*const VNode<'static>>>,
@@ -28,45 +32,68 @@ pub(crate) struct 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 {
scope_counter: Cell::new(0),
bump: Bump::new(),
bump,
pending_futures: RefCell::new(FxHashSet::default()),
scopes: RefCell::new(FxHashMap::default()),
heuristics: RefCell::new(FxHashMap::default()),
free_scopes: RefCell::new(Vec::new()),
nodes: RefCell::new(Slab::new()),
nodes: RefCell::new(nodes),
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) }
}
// this is unsafe
pub unsafe fn get_scope_raw(&self, id: &ScopeId) -> Option<*mut Scope> {
self.scopes.borrow().get(id).map(|f| *f)
pub(crate) unsafe fn get_scope_raw(&self, id: &ScopeId) -> Option<*mut Scope> {
self.scopes.borrow().get(id).copied()
}
// 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)
}
pub fn new_with_key(
pub(crate) fn new_with_key(
&self,
fc_ptr: *const (),
caller: *const dyn Fn(&Scope) -> Element,
parent_scope: Option<*mut Scope>,
container: ElementId,
height: u32,
subtree: u32,
) -> ScopeId {
let new_scope_id = ScopeId(self.scope_counter.get());
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() {
let scope = unsafe { &mut *old_scope };
log::debug!(
@@ -80,6 +107,7 @@ impl ScopeArena {
scope.height = height;
scope.subtree = Cell::new(subtree);
scope.our_arena_idx = new_scope_id;
scope.container = container;
scope.frames[0].nodes.get_mut().push({
let vnode = scope.frames[0]
@@ -103,11 +131,8 @@ impl ScopeArena {
unsafe { std::mem::transmute(vnode as *mut VNode) }
});
let r = self.scopes.borrow_mut().insert(new_scope_id, scope);
assert!(r.is_none());
new_scope_id
let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope);
debug_assert!(any_item.is_none());
} else {
let (node_capacity, hook_capacity) = {
let heuristics = self.heuristics.borrow();
@@ -144,6 +169,7 @@ impl ScopeArena {
let scope = self.bump.alloc(Scope {
sender: self.sender.clone(),
container,
our_arena_idx: new_scope_id,
parent_scope,
height,
@@ -166,24 +192,22 @@ impl ScopeArena {
}),
});
dbg!(self.scopes.borrow());
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
let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope);
debug_assert!(any_item.is_none());
}
new_scope_id
}
pub fn try_remove(&self, id: &ScopeId) -> Option<()> {
self.ensure_drop_safety(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
scope.hooks.clear();
@@ -229,6 +253,11 @@ impl ScopeArena {
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) {
self.nodes.borrow_mut().remove(id.0);
}
@@ -275,15 +304,15 @@ impl ScopeArena {
}
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
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
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:
// - 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
@@ -325,7 +354,7 @@ impl ScopeArena {
debug_assert_eq!(scope.wip_frame().nodes.borrow().len(), 1);
if !scope.items.borrow().tasks.is_empty() {
// self.
self.pending_futures.borrow_mut().insert(*id);
}
// 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
pub fn wip_head(&self, id: &ScopeId) -> &VNode {
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 fxhash::FxHashSet;
use indexmap::IndexSet;
use smallvec::SmallVec;
use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll;
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`].
///
/// ```rust
/// ```rust, ignore
/// #[derive(Props, PartialEq)]
/// struct AppProps {
/// title: String
@@ -33,7 +35,7 @@ use std::{any::Any, collections::VecDeque};
///
/// Components may be composed to make complex apps.
///
/// ```rust
/// ```rust, ignore
/// fn App(cx: Context, props: &AppProps) -> Element {
/// cx.render(rsx!(
/// 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
/// draw the UI.
///
/// ```rust
/// ```rust, ignore
/// let mut vdom = VirtualDom::new(App);
/// let edits = vdom.rebuild();
/// ```
///
/// 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();
/// 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.
///
/// ```rust
/// ```rust, ignore
/// 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
/// handled by the renderer.
///
/// ```rust
/// ```rust, ignore
/// let mutations = vdom.work_with_deadline(|| false);
/// for edit in mutations {
/// 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.
///
/// ```rust
/// ```rust, ignore
/// fn App(cx: Context, props: &()) -> Element {
/// cx.render(rsx!{
/// div { "Hello World" }
@@ -106,7 +108,7 @@ use std::{any::Any, collections::VecDeque};
pub struct VirtualDom {
base_scope: ScopeId,
_root_caller: *mut dyn Fn(&Scope) -> Element,
_root_props: Box<dyn Any>,
scopes: Box<ScopeArena>,
@@ -114,8 +116,6 @@ pub struct VirtualDom {
sender: UnboundedSender<SchedulerMsg>,
pending_futures: FxHashSet<ScopeId>,
pending_messages: VecDeque<SchedulerMsg>,
dirty_scopes: IndexSet<ScopeId>,
@@ -134,7 +134,7 @@ impl VirtualDom {
///
///
/// # Example
/// ```
/// ```rust, ignore
/// fn Example(cx: Context, props: &()) -> Element {
/// cx.render(rsx!( div { "hello world" } ))
/// }
@@ -158,7 +158,7 @@ impl VirtualDom {
///
///
/// # Example
/// ```
/// ```rust, ignore
/// #[derive(PartialEq, Props)]
/// struct SomeProps {
/// 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.
///
/// ```rust
/// ```rust, ignore
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
/// let mutations = dom.rebuild();
/// ```
@@ -194,9 +194,9 @@ impl VirtualDom {
) -> Self {
let scopes = ScopeArena::new(sender.clone());
let caller = Box::new(move |scp: &Scope| -> Element { root(scp, &root_props) });
let caller_ref: *mut dyn Fn(&Scope) -> Element = Box::into_raw(caller);
let base_scope = scopes.new_with_key(root as _, caller_ref, None, 0, 0);
let mut caller = Box::new(move |scp: &Scope| -> Element { root(scp, &root_props) });
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, ElementId(0), 0, 0);
let pending_messages = VecDeque::new();
let mut dirty_scopes = IndexSet::new();
@@ -206,10 +206,8 @@ impl VirtualDom {
scopes: Box::new(scopes),
base_scope,
receiver,
// todo: clean this up manually?
_root_caller: caller_ref,
_root_props: caller,
pending_messages,
pending_futures: Default::default(),
dirty_scopes,
sender,
}
@@ -239,7 +237,7 @@ impl VirtualDom {
///
/// # Example
///
/// ```rust
/// ```rust, ignore
///
///
/// ```
@@ -251,7 +249,7 @@ impl VirtualDom {
///
/// # Example
///
/// ```rust
/// ```rust, ignore
///
///
/// ```
@@ -262,104 +260,79 @@ impl VirtualDom {
/// Waits for the scheduler to have work
/// This lets us poll async tasks during idle periods without blocking the main thread.
pub async fn wait_for_work(&mut self) {
// todo: poll the events once even if there is work to do to prevent starvation
// if there's no futures in the virtualdom, just wait for a scheduler message and put it into the queue to be processed
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,
loop {
if !self.dirty_scopes.is_empty() && self.pending_messages.is_empty() {
break;
}
impl<'a> Future for PollTasks<'a> {
type Output = ();
if self.pending_messages.is_empty() {
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(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> 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(()),
match select(PollTasks(&mut self.scopes), self.receiver.next()).await {
Either::Left((_, _)) => {}
Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()),
}
}
}
// Poll both the futures and the scheduler message queue simulataneously
use futures_util::future::{select, Either};
while let Ok(Some(msg)) = self.receiver.try_next() {
self.pending_messages.push_front(msg);
}
let scheduler_fut = self.receiver.next();
let tasks_fut = PollTasks {
pending_futures: &self.pending_futures,
scopes: &self.scopes,
};
match select(tasks_fut, scheduler_fut).await {
// Futures don't generate work
Either::Left((_, _)) => {}
// Save these messages in FIFO to be processed later
Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()),
if let Some(msg) = self.pending_messages.pop_back() {
match msg {
// just keep looping, the task is now saved but we should actually poll it
SchedulerMsg::TaskPushed(id) => {
self.scopes.pending_futures.borrow_mut().insert(id);
}
SchedulerMsg::UiEvent(event) => {
if let Some(element) = event.element {
self.scopes.call_listener_with_bubbling(event, element);
}
}
SchedulerMsg::Immediate(s) => {
self.dirty_scopes.insert(s);
}
SchedulerMsg::Suspended { scope } => todo!(),
}
}
}
}
/// Run the virtualdom with a deadline.
///
/// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline,
/// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will
/// exhaust the deadline working on them.
/// This method will perform any outstanding diffing work and try to return as many mutations as possible before the
/// deadline is reached. This method accepts a closure that returns `true` if the deadline has been reached. To wrap
/// 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
/// "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
/// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the
/// 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.
/// If the work is not finished by the deadline, Dioxus will store it for later and return when work_with_deadline
/// is called again. This means you can ensure some level of free time on the VirtualDom's thread during the work phase.
///
/// 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
/// 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
///
/// ```no_run
/// ```rust, ignore
/// fn App(cx: Context, props: &()) -> Element {
/// cx.render(rsx!( div {"hello"} ))
/// }
@@ -375,58 +348,19 @@ impl VirtualDom {
/// 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> {
let mut committed_mutations = vec![];
while self.has_any_work() {
while let Ok(Some(msg)) = self.receiver.try_next() {
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);
}
}
}
}
while !self.dirty_scopes.is_empty() {
log::debug!("working with deadline");
let scopes = &self.scopes;
let mut diff_state = DiffState::new(scopes);
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
self.dirty_scopes
.retain(|id| scopes.get_scope(id).is_some());
self.dirty_scopes.sort_by(|a, b| {
let h1 = scopes.get_scope(a).unwrap().height;
let h2 = scopes.get_scope(b).unwrap().height;
@@ -434,31 +368,27 @@ impl VirtualDom {
});
if let Some(scopeid) = self.dirty_scopes.pop() {
log::info!("handling dirty scope {:?}", scopeid);
if !ran_scopes.contains(&scopeid) {
ran_scopes.insert(scopeid);
log::debug!("about to run scope {:?}", scopeid);
if self.scopes.run_scope(&scopeid) {
let (old, new) = (
self.scopes.wip_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.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 work_completed {
if diff_state.work(&mut deadline) {
let DiffState {
mutations,
seen_scopes,
stack,
..
} = diff_state;
@@ -466,13 +396,9 @@ impl VirtualDom {
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);
} else {
// leave the work in an incomplete state
log::debug!("don't have a mechanism to pause work (yet)");
return committed_mutations;
}
}
@@ -490,7 +416,7 @@ impl VirtualDom {
/// All state stored in components will be completely wiped away.
///
/// # Example
/// ```
/// ```rust, ignore
/// static App: FC<()> = |cx, props| cx.render(rsx!{ "hello world" });
/// let mut dom = VirtualDom::new();
/// let edits = dom.rebuild();
@@ -506,6 +432,7 @@ impl VirtualDom {
.stack
.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.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
/// to force an update of the DOM when the state of the app is changed outside of the app.
///
///
/// # Example
/// ```rust
/// ```rust, ignore
/// #[derive(PartialEq, Props)]
/// struct AppProps {
/// value: Shared<&'static str>,
@@ -535,12 +461,7 @@ impl VirtualDom {
/// };
///
/// let value = Rc::new(RefCell::new("Hello"));
/// let mut dom = VirtualDom::new_with_props(
/// App,
/// AppProps {
/// value: value.clone(),
/// },
/// );
/// let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone(), });
///
/// 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> {
let mut machine = DiffState::new(&self.scopes);
machine.stack.push(DiffInstruction::Diff { new, old });
machine.stack.element_stack.push(ElementId(0));
machine.stack.scope_stack.push(self.base_scope);
machine.work(|| false);
machine.mutations
@@ -585,6 +507,7 @@ impl VirtualDom {
pub fn create_vnodes<'a>(&'a self, left: Option<LazyNodes<'a, '_>>) -> Mutations<'a> {
let nodes = self.render_vnodes(left);
let mut machine = DiffState::new(&self.scopes);
machine.stack.element_stack.push(ElementId(0));
machine.stack.create_node(nodes, MountType::Append);
machine.work(|| false);
machine.mutations
@@ -602,11 +525,13 @@ impl VirtualDom {
let mut create = DiffState::new(&self.scopes);
create.stack.scope_stack.push(self.base_scope);
create.stack.element_stack.push(ElementId(0));
create.stack.create_node(old, MountType::Append);
create.work(|| false);
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.work(&mut || false);
@@ -620,17 +545,53 @@ pub enum SchedulerMsg {
// setstate
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)]
pub struct UserEvent {
/// The originator of the event trigger
pub scope_id: ScopeId,
pub scope_id: Option<ScopeId>,
pub priority: EventPriority,
/// 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"
///
@@ -638,7 +599,7 @@ pub struct UserEvent {
pub name: &'static str,
/// Event Data
pub event: Box<dyn Any + Send>,
pub data: Arc<dyn Any + Send + Sync>,
}
/// Priority of Event Triggers.
@@ -692,3 +653,55 @@ pub enum EventPriority {
/// This is considered "idle" work or "background" work.
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!(
mutations.edits,
[
CreateElement {
root: 0,
tag: "div"
},
CreateElement {
root: 1,
tag: "div"
},
CreateTextNode {
CreateElement {
root: 2,
tag: "div"
},
CreateTextNode {
root: 3,
text: "Hello, world!"
},
AppendChildren { many: 1 },
@@ -81,32 +81,32 @@ fn create() {
assert_eq!(
mutations.edits,
[
CreateElement {
root: 0,
tag: "div"
},
CreateElement {
root: 1,
tag: "div"
},
CreateTextNode {
root: 2,
text: "Hello, world!"
},
CreateElement {
root: 3,
root: 2,
tag: "div"
},
CreateTextNode {
root: 3,
text: "Hello, world!"
},
CreateElement {
root: 4,
tag: "div"
},
CreateTextNode {
CreateElement {
root: 5,
text: "hello"
tag: "div"
},
CreateTextNode {
root: 6,
text: "hello"
},
CreateTextNode {
root: 7,
text: "world"
},
AppendChildren { many: 2 },
@@ -136,29 +136,29 @@ fn create_list() {
mutations.edits,
[
CreateElement {
root: 0,
tag: "div"
},
CreateTextNode {
root: 1,
text: "hello"
tag: "div"
},
AppendChildren { many: 1 },
CreateElement {
CreateTextNode {
root: 2,
tag: "div"
},
CreateTextNode {
root: 3,
text: "hello"
},
AppendChildren { many: 1 },
CreateElement {
root: 4,
root: 3,
tag: "div"
},
CreateTextNode {
root: 4,
text: "hello"
},
AppendChildren { many: 1 },
CreateElement {
root: 5,
tag: "div"
},
CreateTextNode {
root: 6,
text: "hello"
},
AppendChildren { many: 1 },
@@ -185,10 +185,6 @@ fn create_simple() {
assert_eq!(
mutations.edits,
[
CreateElement {
root: 0,
tag: "div"
},
CreateElement {
root: 1,
tag: "div"
@@ -201,6 +197,10 @@ fn create_simple() {
root: 3,
tag: "div"
},
CreateElement {
root: 4,
tag: "div"
},
AppendChildren { many: 4 },
]
);
@@ -234,39 +234,39 @@ fn create_components() {
assert_eq!(
mutations.edits,
[
CreateElement { root: 0, tag: "h1" },
CreateElement { root: 1, tag: "h1" },
CreateElement {
root: 1,
root: 2,
tag: "div"
},
CreateTextNode {
root: 2,
root: 3,
text: "abc1"
},
AppendChildren { many: 1 },
CreateElement { root: 3, tag: "p" },
CreateElement { root: 4, tag: "h1" },
CreateElement { root: 4, tag: "p" },
CreateElement { root: 5, tag: "h1" },
CreateElement {
root: 5,
root: 6,
tag: "div"
},
CreateTextNode {
root: 6,
root: 7,
text: "abc2"
},
AppendChildren { many: 1 },
CreateElement { root: 7, tag: "p" },
CreateElement { root: 8, tag: "h1" },
CreateElement { root: 8, tag: "p" },
CreateElement { root: 9, tag: "h1" },
CreateElement {
root: 9,
root: 10,
tag: "div"
},
CreateTextNode {
root: 10,
root: 11,
text: "abc3"
},
AppendChildren { many: 1 },
CreateElement { root: 11, tag: "p" },
CreateElement { root: 12, tag: "p" },
AppendChildren { many: 9 },
]
);
@@ -286,15 +286,15 @@ fn anchors() {
mutations.edits,
[
CreateElement {
root: 0,
root: 1,
tag: "div"
},
CreateTextNode {
root: 1,
root: 2,
text: "hello"
},
AppendChildren { many: 1 },
CreatePlaceholder { root: 2 },
CreatePlaceholder { root: 3 },
AppendChildren { many: 2 },
]
);
+98 -98
View File
@@ -32,11 +32,11 @@ fn html_and_rsx_generate_the_same_output() {
create.edits,
[
CreateElement {
root: 0,
root: 1,
tag: "div"
},
CreateTextNode {
root: 1,
root: 2,
text: "Hello world"
},
AppendChildren { many: 1 },
@@ -48,7 +48,7 @@ fn html_and_rsx_generate_the_same_output() {
change.edits,
[SetText {
text: "Goodbye world",
root: 1
root: 2
},]
);
}
@@ -68,29 +68,29 @@ fn fragments_create_properly() {
create.edits,
[
CreateElement {
root: 0,
root: 1,
tag: "div"
},
CreateTextNode {
root: 1,
root: 2,
text: "Hello a"
},
AppendChildren { many: 1 },
CreateElement {
root: 2,
root: 3,
tag: "div"
},
CreateTextNode {
root: 3,
root: 4,
text: "Hello b"
},
AppendChildren { many: 1 },
CreateElement {
root: 4,
root: 5,
tag: "div"
},
CreateTextNode {
root: 5,
root: 6,
text: "Hello c"
},
AppendChildren { many: 1 },
@@ -111,16 +111,16 @@ fn empty_fragments_create_anchors() {
assert_eq!(
create.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }]
[CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }]
);
assert_eq!(
change.edits,
[
CreateElement {
root: 1,
root: 2,
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);
assert_eq!(
create.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }]
[CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }]
);
assert_eq!(
change.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateElement {
root: 2,
tag: "div"
@@ -161,7 +157,11 @@ fn empty_fragments_create_many_anchors() {
root: 5,
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);
assert_eq!(
create.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }]
[CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }]
);
assert_eq!(
change.edits,
[
CreateElement {
root: 1,
root: 2,
tag: "div"
},
CreateTextNode {
text: "hello: 0",
root: 2
root: 3
},
AppendChildren { many: 1 },
CreateElement {
root: 3,
root: 4,
tag: "div"
},
CreateTextNode {
text: "hello: 1",
root: 4
root: 5
},
AppendChildren { many: 1 },
CreateElement {
root: 5,
root: 6,
tag: "div"
},
CreateTextNode {
text: "hello: 2",
root: 6
root: 7
},
AppendChildren { many: 1 },
ReplaceWith { m: 3, root: 0 }
ReplaceWith { m: 3, root: 1 }
]
);
}
@@ -236,21 +236,21 @@ fn many_items_become_fragment() {
create.edits,
[
CreateElement {
root: 0,
root: 1,
tag: "div"
},
CreateTextNode {
text: "hello",
root: 1
root: 2
},
AppendChildren { many: 1 },
CreateElement {
root: 2,
root: 3,
tag: "div"
},
CreateTextNode {
text: "hello",
root: 3
root: 4
},
AppendChildren { many: 1 },
AppendChildren { many: 2 },
@@ -261,9 +261,9 @@ fn many_items_become_fragment() {
assert_eq!(
change.edits,
[
Remove { root: 2 },
CreatePlaceholder { root: 3 },
ReplaceWith { root: 0, m: 1 },
Remove { root: 3 },
CreatePlaceholder { root: 4 },
ReplaceWith { root: 1, m: 1 },
]
);
}
@@ -308,15 +308,15 @@ fn two_fragments_with_differrent_elements_are_differet() {
changes.edits,
[
// create the new h1s
CreateElement { tag: "h1", root: 3 },
CreateElement { tag: "h1", root: 4 },
CreateElement { tag: "h1", root: 5 },
InsertAfter { root: 1, n: 3 },
// replace the divs with new h1s
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 },
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!(
create.edits,
[
CreateElement {
root: 0,
tag: "div"
},
CreateElement {
root: 1,
tag: "div"
@@ -359,20 +355,24 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() {
root: 4,
tag: "div"
},
CreateElement { root: 5, tag: "p" },
CreateElement {
root: 5,
tag: "div"
},
CreateElement { root: 6, tag: "p" },
AppendChildren { many: 6 },
]
);
assert_eq!(
change.edits,
[
Remove { root: 2 },
Remove { root: 3 },
Remove { root: 4 },
CreateElement { root: 6, tag: "h1" },
ReplaceWith { root: 0, m: 1 },
Remove { root: 5 },
CreateElement { root: 7, tag: "h1" },
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!(
create.edits,
[
CreateElement {
root: 0,
tag: "div"
},
CreateElement {
root: 1,
tag: "div"
},
CreateElement { root: 2, tag: "p" },
CreateElement {
root: 2,
tag: "div"
},
CreateElement { root: 3, tag: "p" },
AppendChildren { many: 3 },
]
);
assert_eq!(
change.edits,
[
CreateElement {
root: 3,
tag: "div"
},
CreateElement {
root: 4,
tag: "div"
@@ -422,7 +418,11 @@ fn two_fragments_with_same_elements_are_differet() {
root: 5,
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);
assert_eq!(
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);
assert_eq!(
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!(
change.edits,
[
PushRoot { root: 5 },
PushRoot { root: 4 },
PushRoot { root: 3 },
InsertBefore { n: 2, root: 0 }
InsertBefore { n: 2, root: 1 }
]
);
}
@@ -521,9 +521,9 @@ fn keyed_diffing_out_of_order_adds_2() {
assert_eq!(
change.edits,
[
PushRoot { root: 3 },
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!(
change.edits,
[
PushRoot { root: 5 },
PushRoot { root: 4 },
PushRoot { root: 3 },
InsertBefore { n: 2, root: 1 }
InsertBefore { n: 2, root: 2 }
]
);
}
@@ -577,9 +577,9 @@ fn keyed_diffing_out_of_order_adds_4() {
assert_eq!(
change.edits,
[
PushRoot { root: 5 },
PushRoot { root: 4 },
PushRoot { root: 3 },
InsertBefore { n: 2, root: 2 }
InsertBefore { n: 2, root: 3 }
]
);
}
@@ -604,7 +604,7 @@ fn keyed_diffing_out_of_order_adds_5() {
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
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!(
change.edits,
[
CreateElement {
root: 5,
tag: "div"
},
CreateElement {
root: 6,
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
CreateElement {
tag: "div",
root: 4
root: 5
},
CreateElement {
tag: "div",
root: 5
root: 6
},
InsertAfter { root: 2, n: 2 },
InsertAfter { root: 3, n: 2 },
// move 7 to the front
PushRoot { root: 3 },
InsertBefore { root: 0, n: 1 }
PushRoot { root: 4 },
InsertBefore { root: 1, n: 1 }
]
);
}
@@ -702,28 +702,28 @@ fn keyed_diffing_additions_and_moves_in_middle() {
change.edits,
[
// create 13, 17
CreateElement {
tag: "div",
root: 4
},
CreateElement {
tag: "div",
root: 5
},
InsertBefore { root: 1, n: 2 },
// create 11, 12
CreateElement {
tag: "div",
root: 6
},
InsertBefore { root: 2, n: 2 },
// create 11, 12
CreateElement {
tag: "div",
root: 7
},
InsertBefore { root: 2, n: 2 },
CreateElement {
tag: "div",
root: 8
},
InsertBefore { root: 3, n: 2 },
// move 7
PushRoot { root: 3 },
InsertBefore { root: 0, n: 1 }
PushRoot { root: 4 },
InsertBefore { root: 1, n: 1 }
]
);
}
@@ -751,22 +751,22 @@ fn controlled_keyed_diffing_out_of_order() {
changes.edits,
[
// move 4 to after 6
PushRoot { root: 0 },
InsertAfter { n: 1, root: 2 },
PushRoot { root: 1 },
InsertAfter { n: 1, root: 3 },
// remove 7
// create 9 and insert before 6
CreateElement {
root: 4,
tag: "div"
},
InsertBefore { n: 1, root: 2 },
// create 0 and insert before 5
CreateElement {
root: 5,
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,
[
CreateElement {
root: 5,
root: 6,
tag: "div"
},
InsertBefore { n: 1, root: 2 },
PushRoot { root: 3 },
InsertBefore { n: 1, root: 0 },
InsertBefore { n: 1, root: 3 },
PushRoot { root: 4 },
InsertBefore { n: 1, root: 1 },
]
);
}
+37 -39
View File
@@ -18,8 +18,6 @@ type Shared<T> = Arc<Mutex<T>>;
#[test]
fn manual_diffing() {
test_logging::set_up_logging(IS_LOGGING_ENABLED);
struct AppProps {
value: Shared<&'static str>,
}
@@ -73,33 +71,34 @@ fn events_generate() {
let mut channel = dom.get_scheduler_channel();
assert!(dom.has_any_work());
let edits = dom.work_with_deadline(|| false);
let edits = dom.rebuild();
assert_eq!(
edits[0].edits,
edits.edits,
[
CreateElement {
tag: "div",
root: 0,
},
NewEventListener {
event_name: "click",
scope: ScopeId(0),
root: 0,
},
CreateElement {
tag: "div",
root: 1,
},
NewEventListener {
event_name: "click",
scope: ScopeId(0),
root: 1,
},
CreateElement {
tag: "div",
root: 2,
},
CreateTextNode {
text: "nested",
root: 2,
root: 3,
},
AppendChildren { many: 1 },
CreateTextNode {
text: "Click me!",
root: 3,
root: 4,
},
AppendChildren { many: 2 },
AppendChildren { many: 1 },
]
)
}
@@ -124,7 +123,6 @@ fn components_generate() {
};
static Child: FC<()> = |cx, _| {
println!("running child");
cx.render(rsx! {
h1 {}
})
@@ -137,7 +135,7 @@ fn components_generate() {
[
CreateTextNode {
text: "Text0",
root: 0,
root: 1,
},
AppendChildren { many: 1 },
]
@@ -149,18 +147,6 @@ fn components_generate() {
[
CreateElement {
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,
},
ReplaceWith { root: 1, m: 1 },
@@ -171,7 +157,10 @@ fn components_generate() {
assert_eq!(
edits.edits,
[
CreateElement { tag: "h1", root: 3 },
CreateTextNode {
text: "Text2",
root: 3,
},
ReplaceWith { root: 2, m: 1 },
]
);
@@ -179,7 +168,16 @@ fn components_generate() {
let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!(
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();
@@ -188,9 +186,9 @@ fn components_generate() {
[
CreateTextNode {
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 {
text: "text 0",
root: 6,
root: 7,
},
CreateTextNode {
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!(
edits.edits,
[
CreateElement { tag: "h1", root: 8 },
ReplaceWith { root: 6, m: 1 },
Remove { root: 7 },
CreateElement { tag: "h1", root: 9 },
ReplaceWith { root: 7, m: 1 },
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,
[
CreateTextNode {
root: 0,
root: 1,
text: "Hello, world!"
},
AppendChildren { many: 1 },
+23 -22
View File
@@ -11,19 +11,18 @@
//! Don't have a good way to validate, everything is done manually ATM
use dioxus::prelude::*;
use dioxus::DomEdit;
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
use DomEdit::*;
#[test]
fn app_runs() {
static App: FC<()> = |cx, props| {
//
cx.render(rsx!( div{"hello"} ))
};
static App: FC<()> = |cx, props| rsx!(cx, div{"hello"} );
let mut vdom = VirtualDom::new(App);
let edits = vdom.rebuild();
dbg!(edits);
}
#[test]
@@ -65,9 +64,25 @@ fn conditional_rendering() {
let mut vdom = VirtualDom::new(App);
let mutations = vdom.rebuild();
dbg!(&mutations);
// the "false" fragment should generate an empty placeholder to re-visit
assert!(mutations.edits[mutations.edits.len() - 2].is("CreatePlaceholder"));
assert_eq!(
mutations.edits,
[
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]
@@ -88,17 +103,3 @@ fn child_components() {
let edits = vdom.rebuild();
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",
"rt-multi-thread",
"rt",
"time",
], optional = true, default-features = false }
dioxus-core-macro = { path = "../core-macro" }
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 {
event: String,
mounted_dom_id: u64,
scope: u64,
// scope: u64,
contents: serde_json::Value,
}
@@ -20,11 +20,11 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
let ImEvent {
event,
mounted_dom_id,
scope,
// scope,
contents,
} = 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 name = event_name_from_typ(&event);
@@ -33,75 +33,75 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
UserEvent {
name,
priority: EventPriority::Low,
scope_id,
mounted_dom_id,
event,
scope_id: None,
element: mounted_dom_id,
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 {
"copy" | "cut" | "paste" => {
//
Box::new(ClipboardEvent {})
Arc::new(ClipboardEvent {})
}
"compositionend" | "compositionstart" | "compositionupdate" => {
Box::new(serde_json::from_value::<CompositionEvent>(val).unwrap())
Arc::new(serde_json::from_value::<CompositionEvent>(val).unwrap())
}
"keydown" | "keypress" | "keyup" => {
let evt = serde_json::from_value::<KeyboardEvent>(val).unwrap();
Box::new(evt)
Arc::new(evt)
}
"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
// don't have a good solution with the serialized event problem
"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"
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
| "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"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
Box::new(serde_json::from_value::<PointerEvent>(val).unwrap())
Arc::new(serde_json::from_value::<PointerEvent>(val).unwrap())
}
"select" => {
//
Box::new(serde_json::from_value::<SelectionEvent>(val).unwrap())
Arc::new(serde_json::from_value::<SelectionEvent>(val).unwrap())
}
"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" => {
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"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "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 {
constructor(root) {
this.root = root;
@@ -39,14 +248,9 @@ class Interpreter {
}
ReplaceWith(edit) {
// console.log(edit);
let root = this.nodes[edit.root];
let els = this.stack.splice(this.stack.length - edit.m);
// console.log(root);
// console.log(els);
root.replaceWith(...els);
}
@@ -79,6 +283,7 @@ class Interpreter {
const tagName = edit.tag;
const el = document.createElement(tagName);
this.nodes[edit.root] = el;
el.setAttribute("dioxus-id", edit.root);
this.stack.push(el);
}
@@ -110,14 +315,14 @@ class Interpreter {
this.root.addEventListener(event_name, (event) => {
// console.log("CLICKED");
const target = event.target;
const val = target.getAttribute(`dioxus-event-${event_name}`);
if (val == null) {
const real_id = target.getAttribute(`dioxus-id`);
if (real_id == null) {
return;
}
const fields = val.split(".");
const scope_id = parseInt(fields[0]);
const real_id = parseInt(fields[1]);
// const fields = val.split(".");
// const scope_id = parseInt(fields[0]);
// const real_id = parseInt(fields[1]);
// // 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 evt = {
event: event_name,
scope: scope_id,
mounted_dom_id: real_id,
// scope: scope_id,
mounted_dom_id: parseInt(real_id),
contents: contents,
};
// console.log(evt);
rpc.call('user_event', evt).then((reply) => {
// console.log("reply received", reply);
this.stack.push(this.root);
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);
}
});
})
rpc.call('user_event', evt);
});
}
}
@@ -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");
const interpreter = new Interpreter(root);
window.interpreter = new Interpreter(root);
console.log(window.interpreter);
const reply = await rpc.call('initiate');
let pre_rendered = reply.pre_rendered;
if (pre_rendered !== undefined) {
root.innerHTML = pre_rendered;
}
const edits = reply.edits;
apply_edits(edits, interpreter);
rpc.call('initialize');
}
function apply_edits(edits, interpreter) {
// 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();
main()
+124 -161
View File
@@ -5,7 +5,7 @@
use std::borrow::BorrowMut;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::collections::{HashMap, VecDeque};
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use std::sync::atomic::AtomicBool;
@@ -20,10 +20,10 @@ pub use wry;
use wry::application::accelerator::{Accelerator, SysMods};
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::menu::{MenuBar, MenuItem, MenuItemAttributes};
use wry::application::window::Fullscreen;
use wry::application::window::{Fullscreen, WindowId};
use wry::webview::{WebView, WebViewBuilder};
use wry::{
application::menu,
@@ -59,12 +59,6 @@ enum RpcEvent<'a> {
Initialize { edits: Vec<DomEdit<'a>> },
}
#[derive(Debug)]
enum BridgeEvent {
Initialize(serde_json::Value),
Update(serde_json::Value),
}
#[derive(Serialize)]
struct Response<'a> {
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
// 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 props_shared = Cell::new(Some(props));
// create local modifier state
let mut modifiers = ModifiersState::default();
let modifiers = ModifiersState::default();
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;
match event {
match window_event {
Event::NewEvents(StartCause::Init) => {
// 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);
let window = WindowBuilder::new()
.with_maximized(true)
.with_menu(menu_bar_menu)
.with_title("Dioxus App")
.build(event_loop)
.unwrap();
let window = create_window(event_loop);
let window_id = window.id();
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
let my_props = props_shared.take().unwrap();
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();
let sender =
launch_vdom_with_tokio(root, props_shared.take().unwrap(), edit_queue.clone());
let webview = create_webview(window, is_ready.clone(), sender);
webviews.insert(window_id, webview);
}
@@ -206,29 +118,17 @@ pub fn run<T: 'static + Send + Sync>(
*control_flow = ControlFlow::Exit;
}
}
// catch only pressed event
WindowEvent::KeyboardInput { event, .. } => {
log::debug!("keybowrd input");
if quit_hotkey.matches(&modifiers, &event.physical_key) {
log::debug!("quitting");
WindowEvent::Moved(pos) => {
//
}
WindowEvent::KeyboardInput { event, .. } => {
if quit_hotkey.matches(&modifiers, &event.physical_key) {
webviews.remove(&window_id);
if webviews.is_empty() {
*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(_) => {
@@ -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::Suspended => {}
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.
pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>(
root: FC<P>,
props: P,
event_tx: tokio::sync::mpsc::UnboundedSender<BridgeEvent>,
edit_queue: Arc<RwLock<VecDeque<String>>>,
) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
let return_sender = sender.clone();
@@ -275,47 +178,107 @@ pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>(
.unwrap();
runtime.block_on(async move {
let mut vir = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver);
let _ = vir.get_scheduler_channel();
let mut dom = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver);
let edits = vir.rebuild();
let edits = dom.rebuild();
// the receiving end expects something along these lines
#[derive(Serialize)]
struct Evt<'a> {
edits: Vec<DomEdit<'a>>,
}
let edit_string = serde_json::to_value(Evt { edits: edits.edits }).unwrap();
event_tx
.send(BridgeEvent::Initialize(edit_string))
.expect("Sending should not fail");
edit_queue
.write()
.unwrap()
.push_front(serde_json::to_string(&edits.edits).unwrap());
loop {
vir.wait_for_work().await;
// we're running on our own thread, so we don't need to worry about blocking anything
// 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![];
dom.wait_for_work().await;
let mut muts = dom.work_with_deadline(|| false);
while let Some(edit) = muts.pop() {
let edit_string = serde_json::to_value(Evt { edits: edit.edits })
.expect("serializing edits should never fail");
edits.push(edit_string);
edit_queue
.write()
.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
}
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> {
pub(crate) fn notify_consumers(&mut self) {
for consumer in self.consumers.iter() {
println!("notifiying {:?}", consumer);
// log::debug("notifiying {:?}", 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)> {
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> {
+5 -4
View File
@@ -5,6 +5,7 @@ use std::any::Any;
pub mod on {
use super::*;
use std::sync::Arc;
macro_rules! event_directory {
( $(
$( #[$attr:meta] )*
@@ -23,19 +24,19 @@ pub mod on {
c: NodeFactory<'a>,
mut callback: F,
) -> Listener<'a>
where F: FnMut($wrapper) + 'a
where F: FnMut(Arc<$wrapper>) + 'a
{
let bump = &c.bump();
// 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
// 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();
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
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 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 web_sys::{
CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
@@ -92,7 +92,7 @@ impl WebsysDom {
// .as_ref()
// .unwrap()
// .clone();
// bla.set(Box::new(node)).unwrap();
// bla.set(Arc::new(node)).unwrap();
// }
// }
// }
@@ -286,14 +286,16 @@ impl WebsysDom {
} else {
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
// Instead, we just build and immediately execute a closure that returns result
match decode_trigger(event) {
Ok(synthetic_event) => trigger.as_ref()(SchedulerMsg::UiEvent(synthetic_event)),
Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
};
}) as Box<dyn FnMut(&Event)>);
});
let handler = Closure::wrap(c);
self.root
.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.
// 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::KeyCode;
// use dioxus_core::events::on::*;
match event.type_().as_str() {
"copy" | "cut" | "paste" => Box::new(ClipboardEvent {}),
"copy" | "cut" | "paste" => Arc::new(ClipboardEvent {}),
"compositionend" | "compositionstart" | "compositionupdate" => {
let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap();
Box::new(CompositionEvent {
Arc::new(CompositionEvent {
data: evt.data().unwrap_or_default(),
})
}
"keydown" | "keypress" | "keyup" => {
let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap();
Box::new(KeyboardEvent {
Arc::new(KeyboardEvent {
alt_key: evt.alt_key(),
char_code: evt.char_code(),
key: evt.key(),
@@ -513,7 +515,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send>
}
"focus" | "blur" => {
//
Box::new(FocusEvent {})
Arc::new(FocusEvent {})
}
// "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");
Box::new(FormEvent { value })
Arc::new(FormEvent { value })
}
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap();
Box::new(MouseEvent {
Arc::new(MouseEvent {
alt_key: evt.alt_key(),
button: evt.button(),
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"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap();
Box::new(PointerEvent {
Arc::new(PointerEvent {
alt_key: evt.alt_key(),
button: evt.button(),
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(),
})
}
"select" => Box::new(SelectionEvent {}),
"select" => Arc::new(SelectionEvent {}),
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap();
Box::new(TouchEvent {
Arc::new(TouchEvent {
alt_key: evt.alt_key(),
ctrl_key: evt.ctrl_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" => {
let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap();
Box::new(WheelEvent {
Arc::new(WheelEvent {
delta_x: evt.delta_x(),
delta_y: evt.delta_y(),
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" => {
let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap();
Box::new(AnimationEvent {
Arc::new(AnimationEvent {
elapsed_time: evt.elapsed_time(),
animation_name: evt.animation_name(),
pseudo_element: evt.pseudo_element(),
@@ -643,7 +645,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Box<dyn Any + Send>
"transitionend" => {
let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap();
Box::new(TransitionEvent {
Arc::new(TransitionEvent {
elapsed_time: evt.elapsed_time(),
property_name: evt.property_name(),
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"
| "timeupdate" | "volumechange" | "waiting" => {
//
Box::new(MediaEvent {})
Arc::new(MediaEvent {})
}
"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 {
name: event_name_from_typ(&typ),
event: virtual_event_from_websys_event(event.clone()),
mounted_dom_id: Some(ElementId(real_id as usize)),
scope_id: ScopeId(triggered_scope as usize),
data: virtual_event_from_websys_event(event.clone()),
element: Some(ElementId(real_id as usize)),
scope_id: Some(ScopeId(triggered_scope as usize)),
priority: dioxus_core::EventPriority::Medium,
})
}
+1 -1
View File
@@ -193,7 +193,7 @@ pub mod debug {}
pub mod prelude {
//! A glob import that includes helper types like FC, rsx!, html!, and required traits
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_hooks::*;
pub use dioxus_html as dioxus_elements;