Merge branch 'jk/templates-v3' into implement-new-mutations-with-native-core
This commit is contained in:
commit
80674e4566
114 files changed
+4269
-5576
No files matched your search
+7
-5
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"packages/dioxus",
|
"packages/dioxus",
|
||||||
@@ -13,17 +12,20 @@ members = [
|
|||||||
"packages/mobile",
|
"packages/mobile",
|
||||||
"packages/interpreter",
|
"packages/interpreter",
|
||||||
"packages/fermi",
|
"packages/fermi",
|
||||||
"packages/tui",
|
|
||||||
"packages/liveview",
|
"packages/liveview",
|
||||||
"packages/autofmt",
|
"packages/autofmt",
|
||||||
"packages/rsx",
|
"packages/rsx",
|
||||||
|
"docs/guide",
|
||||||
|
# "packages/tui",
|
||||||
|
# "packages/native-core",
|
||||||
|
# "packages/native-core-macro",
|
||||||
|
]
|
||||||
|
exclude = [
|
||||||
|
"packages/tui",
|
||||||
"packages/native-core",
|
"packages/native-core",
|
||||||
"packages/native-core-macro",
|
"packages/native-core-macro",
|
||||||
# "packages/edit-stream",
|
|
||||||
"docs/guide",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# This is a "virtual package"
|
# This is a "virtual package"
|
||||||
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
|
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
|
||||||
[package]
|
[package]
|
||||||
|
|||||||
+2
-2
@@ -30,7 +30,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn BlogPost(cx: Scope) -> Element {
|
fn BlogPost(cx: Scope) -> Element {
|
||||||
let post = dioxus_router::use_route(&cx).last_segment()?;
|
let post = dioxus_router::use_route(&cx).last_segment().unwrap();
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
@@ -46,7 +46,7 @@ struct Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn User(cx: Scope) -> Element {
|
fn User(cx: Scope) -> Element {
|
||||||
let post = dioxus_router::use_route(&cx).last_segment()?;
|
let post = dioxus_router::use_route(&cx).last_segment().unwrap();
|
||||||
|
|
||||||
let query = dioxus_router::use_route(&cx)
|
let query = dioxus_router::use_route(&cx)
|
||||||
.query::<Query>()
|
.query::<Query>()
|
||||||
|
|||||||
@@ -165,13 +165,13 @@ fn app(cx: Scope) -> Element {
|
|||||||
|
|
||||||
// Can pass in props directly as an expression
|
// Can pass in props directly as an expression
|
||||||
{
|
{
|
||||||
let props = TallerProps {a: "hello", children: Default::default()};
|
let props = TallerProps {a: "hello", children: cx.render(rsx!(()))};
|
||||||
rsx!(Taller { ..props })
|
rsx!(Taller { ..props })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spreading can also be overridden manually
|
// Spreading can also be overridden manually
|
||||||
Taller {
|
Taller {
|
||||||
..TallerProps { a: "ballin!", children: Default::default() },
|
..TallerProps { a: "ballin!", children: cx.render(rsx!(()) )},
|
||||||
a: "not ballin!"
|
a: "not ballin!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let mut idx = use_state(cx, || 0);
|
||||||
|
let onhover = |h| println!("go!");
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
button { onclick: move |_| idx += 1, "+" }
|
||||||
|
button { onclick: move |_| idx -= 1, "-" }
|
||||||
|
ul {
|
||||||
|
(0..**idx).map(|i| rsx! {
|
||||||
|
Child { i: i, onhover: onhover }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline_props]
|
||||||
|
fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
li {
|
||||||
|
onmouseover: move |e| onhover.call(e),
|
||||||
|
"{i}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+6
-1
@@ -57,7 +57,10 @@ pub fn app(cx: Scope<()>) -> Element {
|
|||||||
placeholder: "What needs to be done?",
|
placeholder: "What needs to be done?",
|
||||||
value: "{draft}",
|
value: "{draft}",
|
||||||
autofocus: "true",
|
autofocus: "true",
|
||||||
oninput: move |evt| draft.set(evt.value.clone()),
|
oninput: move |evt| {
|
||||||
|
println!("calling oninput");
|
||||||
|
draft.set(evt.value.clone());
|
||||||
|
},
|
||||||
onkeydown: move |evt| {
|
onkeydown: move |evt| {
|
||||||
if evt.key() == Key::Enter && !draft.is_empty() {
|
if evt.key() == Key::Enter && !draft.is_empty() {
|
||||||
todos.make_mut().insert(
|
todos.make_mut().insert(
|
||||||
@@ -121,6 +124,8 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
|||||||
let completed = if todo.checked { "completed" } else { "" };
|
let completed = if todo.checked { "completed" } else { "" };
|
||||||
let editing = if **is_editing { "editing" } else { "" };
|
let editing = if **is_editing { "editing" } else { "" };
|
||||||
|
|
||||||
|
println!("rendering todo entry");
|
||||||
|
|
||||||
cx.render(rsx!{
|
cx.render(rsx!{
|
||||||
li {
|
li {
|
||||||
class: "{completed} {editing}",
|
class: "{completed} {editing}",
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ impl Buffer {
|
|||||||
match node {
|
match node {
|
||||||
BodyNode::Element(el) => self.write_element(el),
|
BodyNode::Element(el) => self.write_element(el),
|
||||||
BodyNode::Component(component) => self.write_component(component),
|
BodyNode::Component(component) => self.write_component(component),
|
||||||
BodyNode::DynamicText(text) => self.write_text(text),
|
BodyNode::Text(text) => self.write_text(text),
|
||||||
BodyNode::RawExpr(exp) => self.write_raw_expr(exp),
|
BodyNode::RawExpr(exp) => self.write_raw_expr(exp),
|
||||||
_ => Ok(()),
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ impl Buffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match children {
|
match children {
|
||||||
[BodyNode::DynamicText(ref text)] => Some(text.source.as_ref().unwrap().value().len()),
|
[BodyNode::Text(ref text)] => Some(text.source.as_ref().unwrap().value().len()),
|
||||||
[BodyNode::Component(ref comp)] => {
|
[BodyNode::Component(ref comp)] => {
|
||||||
let attr_len = self.field_len(&comp.fields, &comp.manual_props);
|
let attr_len = self.field_len(&comp.fields, &comp.manual_props);
|
||||||
|
|
||||||
|
|||||||
@@ -42,4 +42,3 @@ dioxus = { path = "../dioxus" }
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
serialize = ["serde"]
|
serialize = ["serde"]
|
||||||
debug_vdom = []
|
|
||||||
+64
-46
@@ -1,52 +1,84 @@
|
|||||||
# Dioxus-core
|
# dioxus-core
|
||||||
|
|
||||||
This is the core crate for the Dioxus Virtual DOM. This README will focus on the technical design and layout of this Virtual DOM implementation. If you want to read more about using Dioxus, then check out the Dioxus crate, documentation, and website.
|
dioxus-core is a fast and featureful VirtualDom implementation written in and for Rust.
|
||||||
|
|
||||||
To build new apps with Dioxus or to extend the ecosystem with new hooks or components, use the higher-level `dioxus` crate with the appropriate feature flags.
|
# Features
|
||||||
|
|
||||||
|
- Functions as components
|
||||||
|
- Hooks for local state
|
||||||
|
- Task pool for spawning futures
|
||||||
|
- Template-based architecture
|
||||||
|
- Asynchronous components
|
||||||
|
- Suspense boundaries
|
||||||
|
- Error boundaries through the `anyhow` crate
|
||||||
|
- Customizable memoization
|
||||||
|
|
||||||
|
If just starting out, check out the Guides first.
|
||||||
|
|
||||||
|
# General Theory
|
||||||
|
|
||||||
|
The dioxus-core `VirtualDom` object is built around the concept of a `Template`. Templates describe a layout tree known at compile time with dynamic parts filled at runtime.
|
||||||
|
|
||||||
|
Each component in the VirtualDom works as a dedicated render loop where re-renders are triggered by events external to the VirtualDom, or from the components themselves.
|
||||||
|
|
||||||
|
When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result<VNode>`. Between two renders, Dioxus compares the inner `VNode` object, and calculates the differences of the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and new layout, Dioxus will write modifications to the `Mutations` object.
|
||||||
|
|
||||||
|
Dioxus expects the target renderer to save its nodes in a list. Each element is given a numerical ID which can be used to directly index into that list for O(1) lookups.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
All Dioxus apps start as just a function that takes the [`Scope`] object and returns an [`Element`].
|
||||||
|
|
||||||
|
The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler syntax of Rust into the logic required to build Templates.
|
||||||
|
|
||||||
|
First, start with your app:
|
||||||
|
|
||||||
```rust, ignore
|
```rust, ignore
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
render!(div { "hello world" })
|
cx.render(rsx!( div { "hello world" } ))
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
fn main() {
|
Then, we'll want to create a new VirtualDom using this app as the root component.
|
||||||
let mut renderer = SomeRenderer::new();
|
|
||||||
|
|
||||||
// Creating a new virtualdom from a component
|
```rust, ingore
|
||||||
let mut dom = VirtualDom::new(app);
|
let mut dom = VirtualDom::new(app);
|
||||||
|
```
|
||||||
|
|
||||||
// Patching the renderer with the changes to draw the screen
|
To build the app into a stream of mutations, we'll use [`VirtualDom::rebuild`]:
|
||||||
let edits = dom.rebuild();
|
|
||||||
renderer.apply(edits);
|
|
||||||
|
|
||||||
// Injecting events
|
```rust, ignore
|
||||||
dom.handle_message(SchedulerMsg::Event(UserEvent {
|
let mutations = dom.rebuild();
|
||||||
scope_id: None,
|
|
||||||
priority: EventPriority::High,
|
|
||||||
element: ElementId(0),
|
|
||||||
name: "onclick",
|
|
||||||
data: Arc::new(()),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// polling asynchronously
|
apply_edits_to_real_dom(mutations);
|
||||||
dom.wait_for_work().await;
|
```
|
||||||
|
|
||||||
// working with a deadline
|
We can then wait for any asynchronous components or pending futures using the `wait_for_work()` method. If we have a deadline, then we can use render_with_deadline instead:
|
||||||
if let Some(edits) = dom.work_with_deadline(|| false) {
|
|
||||||
renderer.apply(edits);
|
```rust, ignore
|
||||||
|
// Wait for the dom to be marked dirty internally
|
||||||
|
dom.wait_for_work().await;
|
||||||
|
|
||||||
|
// Or wait for a deadline and then collect edits
|
||||||
|
dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)));
|
||||||
|
```
|
||||||
|
|
||||||
|
If an event occurs from outside the virtualdom while waiting for work, then we can cancel the wait using a `select!` block and inject the event.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
evt = real_dom.event() => dom.handle_event("click", evt.data, evt.element, evt.bubbles),
|
||||||
|
_ = dom.wait_for_work() => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getting state of scopes
|
// Render any work without blocking the main thread for too long
|
||||||
let scope = dom.get_scope(ScopeId(0)).unwrap();
|
let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)));
|
||||||
|
|
||||||
// iterating through the tree
|
// And then apply the edits
|
||||||
match scope.root_node() {
|
real_dom.apply(mutations);
|
||||||
VNodes::Text(vtext) => dbg!(vtext),
|
|
||||||
VNodes::Element(vel) => dbg!(vel),
|
|
||||||
_ => todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Internals
|
## Internals
|
||||||
@@ -82,17 +114,3 @@ The final implementation of Dioxus must:
|
|||||||
- Support server-side-rendering (SSR). VNodes should render to a string that can be served via a web server.
|
- Support server-side-rendering (SSR). VNodes should render to a string that can be served via a web server.
|
||||||
- Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs.
|
- Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs.
|
||||||
- Be modular. Components and hooks should be work anywhere without worrying about target platform.
|
- Be modular. Components and hooks should be work anywhere without worrying about target platform.
|
||||||
|
|
||||||
|
|
||||||
## Safety
|
|
||||||
|
|
||||||
Dioxus uses unsafe. The design of Dioxus *requires* unsafe (self-referential trees).
|
|
||||||
|
|
||||||
All of our test suite passes MIRI without errors.
|
|
||||||
|
|
||||||
Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinning, and a lot more foundational low-level work that is very difficult to implement with 0 unsafe.
|
|
||||||
|
|
||||||
If you don't want to use a crate that uses unsafe, then this crate is not for you.
|
|
||||||
|
|
||||||
However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous invariants required to execute certain features.
|
|
||||||
|
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
factory::{ComponentReturn, RenderReturn},
|
|
||||||
innerlude::Scoped,
|
innerlude::Scoped,
|
||||||
|
nodes::{ComponentReturn, RenderReturn},
|
||||||
scopes::{Scope, ScopeState},
|
scopes::{Scope, ScopeState},
|
||||||
Element,
|
Element,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A trait that essentially allows VComponentProps to be used generically
|
/// A trait that essentially allows VComponentProps to be used generically
|
||||||
pub unsafe trait AnyProps<'a> {
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This should not be implemented outside this module
|
||||||
|
pub(crate) unsafe trait AnyProps<'a> {
|
||||||
fn props_ptr(&self) -> *const ();
|
fn props_ptr(&self) -> *const ();
|
||||||
fn render(&'a self, bump: &'a ScopeState) -> RenderReturn<'a>;
|
fn render(&'a self, bump: &'a ScopeState) -> RenderReturn<'a>;
|
||||||
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool;
|
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool;
|
||||||
@@ -18,7 +22,6 @@ pub(crate) struct VProps<'a, P, A, F: ComponentReturn<'a, A> = Element<'a>> {
|
|||||||
pub render_fn: fn(Scope<'a, P>) -> F,
|
pub render_fn: fn(Scope<'a, P>) -> F,
|
||||||
pub memo: unsafe fn(&P, &P) -> bool,
|
pub memo: unsafe fn(&P, &P) -> bool,
|
||||||
pub props: P,
|
pub props: P,
|
||||||
// pub props: PropsAllocation<P>,
|
|
||||||
_marker: PhantomData<A>,
|
_marker: PhantomData<A>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +38,6 @@ where
|
|||||||
render_fn,
|
render_fn,
|
||||||
memo,
|
memo,
|
||||||
props,
|
props,
|
||||||
// props: PropsAllocation::Borrowed(props),
|
|
||||||
_marker: PhantomData,
|
_marker: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,12 +62,12 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> {
|
fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> {
|
||||||
let scope = cx.bump().alloc(Scoped {
|
let scope: &mut Scoped<P> = cx.bump().alloc(Scoped {
|
||||||
props: &self.props,
|
props: &self.props,
|
||||||
scope: cx,
|
scope: cx,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call the render function directly
|
// Call the render function directly
|
||||||
(self.render_fn)(scope).as_return(cx)
|
(self.render_fn)(scope).into_return(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+111
-17
@@ -1,47 +1,141 @@
|
|||||||
use crate::{nodes::VNode, virtual_dom::VirtualDom};
|
use crate::{
|
||||||
|
nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode,
|
||||||
|
ScopeId,
|
||||||
|
};
|
||||||
|
use bumpalo::boxed::Box as BumpBox;
|
||||||
|
|
||||||
|
/// An Element's unique identifier.
|
||||||
|
///
|
||||||
|
/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
|
||||||
|
/// unmounted, then the `ElementId` will be reused for a new component.
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
pub struct ElementId(pub usize);
|
pub struct ElementId(pub usize);
|
||||||
|
|
||||||
pub struct ElementRef {
|
pub(crate) struct ElementRef {
|
||||||
// the pathway of the real element inside the template
|
// the pathway of the real element inside the template
|
||||||
pub path: &'static [u8],
|
pub path: ElementPath,
|
||||||
|
|
||||||
// The actual template
|
// The actual template
|
||||||
pub template: *mut VNode<'static>,
|
pub template: *const VNode<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum ElementPath {
|
||||||
|
Deep(&'static [u8]),
|
||||||
|
Root(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ElementRef {
|
impl ElementRef {
|
||||||
pub fn null() -> Self {
|
pub(crate) fn null() -> Self {
|
||||||
Self {
|
Self {
|
||||||
template: std::ptr::null_mut(),
|
template: std::ptr::null_mut(),
|
||||||
path: &[],
|
path: ElementPath::Root(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
pub fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
|
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
|
||||||
let entry = self.elements.vacant_entry();
|
let entry = self.elements.vacant_entry();
|
||||||
let id = entry.key();
|
let id = entry.key();
|
||||||
|
|
||||||
entry.insert(ElementRef {
|
entry.insert(ElementRef {
|
||||||
template: template as *const _ as *mut _,
|
template: template as *const _ as *mut _,
|
||||||
path,
|
path: ElementPath::Deep(path),
|
||||||
});
|
});
|
||||||
|
|
||||||
ElementId(id)
|
ElementId(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cleanup_element(&mut self, id: ElementId) {
|
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
|
||||||
self.elements.remove(id.0);
|
let entry = self.elements.vacant_entry();
|
||||||
|
let id = entry.key();
|
||||||
|
entry.insert(ElementRef {
|
||||||
|
template: template as *const _ as *mut _,
|
||||||
|
path: ElementPath::Root(path),
|
||||||
|
});
|
||||||
|
ElementId(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reclaim(&mut self, el: ElementId) {
|
||||||
|
self.try_reclaim(el)
|
||||||
|
.unwrap_or_else(|| panic!("cannot reclaim {:?}", el));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> {
|
||||||
|
if el.0 == 0 {
|
||||||
|
panic!(
|
||||||
|
"Invalid element set to 0 - {:#?}",
|
||||||
|
std::backtrace::Backtrace::force_capture()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("reclaiming {:?}", el);
|
||||||
|
self.elements.try_remove(el.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) {
|
||||||
|
let node: *const VNode = node as *const _;
|
||||||
|
self.elements[el.0].template = unsafe { std::mem::transmute(node) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop a scope and all its children
|
||||||
|
pub(crate) fn drop_scope(&mut self, id: ScopeId) {
|
||||||
|
let scope = self.scopes.get(id.0).unwrap();
|
||||||
|
|
||||||
|
if let Some(root) = scope.as_ref().try_root_node() {
|
||||||
|
let root = unsafe { root.extend_lifetime_ref() };
|
||||||
|
if let RenderReturn::Sync(Ok(node)) = root {
|
||||||
|
self.drop_scope_inner(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let scope = self.scopes.get_mut(id.0).unwrap();
|
||||||
|
scope.props.take();
|
||||||
|
|
||||||
|
// Drop all the hooks once the children are dropped
|
||||||
|
// this means we'll drop hooks bottom-up
|
||||||
|
for hook in scope.hook_list.get_mut().drain(..) {
|
||||||
|
println!("dropping hook !");
|
||||||
|
drop(unsafe { BumpBox::from_raw(hook) });
|
||||||
|
println!("hook dropped !");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_scope_inner(&mut self, node: &VNode) {
|
||||||
|
for attr in node.dynamic_attrs {
|
||||||
|
if let AttributeValue::Listener(l) = &attr.value {
|
||||||
|
l.borrow_mut().take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (idx, _) in node.template.roots.iter().enumerate() {
|
||||||
|
match node.dynamic_root(idx) {
|
||||||
|
Some(DynamicNode::Component(c)) => self.drop_scope(c.scope.get().unwrap()),
|
||||||
|
Some(DynamicNode::Fragment(nodes)) => {
|
||||||
|
for node in *nodes {
|
||||||
|
self.drop_scope_inner(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
impl ElementPath {
|
||||||
now......
|
pub(crate) fn is_ascendant(&self, big: &&[u8]) -> bool {
|
||||||
|
match *self {
|
||||||
|
ElementPath::Deep(small) => small.len() <= big.len() && small == &big[..small.len()],
|
||||||
|
ElementPath::Root(r) => big.len() == 1 && big[0] == r as u8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
an ID is mostly a pointer to a node in the real dom.
|
impl PartialEq<&[u8]> for ElementPath {
|
||||||
We need to
|
fn eq(&self, other: &&[u8]) -> bool {
|
||||||
*/
|
match *self {
|
||||||
|
ElementPath::Deep(deep) => deep.eq(*other),
|
||||||
|
ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,29 @@
|
|||||||
use crate::factory::RenderReturn;
|
use crate::nodes::RenderReturn;
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
pub struct BumpFrame {
|
pub(crate) struct BumpFrame {
|
||||||
pub bump: Bump,
|
pub bump: Bump,
|
||||||
pub node: Cell<*mut RenderReturn<'static>>,
|
pub node: Cell<*const RenderReturn<'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BumpFrame {
|
impl BumpFrame {
|
||||||
pub fn new(capacity: usize) -> Self {
|
pub(crate) fn new(capacity: usize) -> Self {
|
||||||
let bump = Bump::with_capacity(capacity);
|
let bump = Bump::with_capacity(capacity);
|
||||||
Self {
|
Self {
|
||||||
bump,
|
bump,
|
||||||
node: Cell::new(std::ptr::null_mut()),
|
node: Cell::new(std::ptr::null()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
self.bump.reset();
|
|
||||||
self.node.set(std::ptr::null_mut());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new lifetime out of thin air
|
/// Creates a new lifetime out of thin air
|
||||||
pub unsafe fn load_node<'b>(&self) -> &'b RenderReturn<'b> {
|
pub(crate) unsafe fn try_load_node<'b>(&self) -> Option<&'b RenderReturn<'b>> {
|
||||||
unsafe { std::mem::transmute(&*self.node.get()) }
|
let node = self.node.get();
|
||||||
|
|
||||||
|
if node.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe { std::mem::transmute(&*node) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+80
-135
@@ -1,13 +1,14 @@
|
|||||||
use crate::factory::RenderReturn;
|
use std::cell::Cell;
|
||||||
use crate::innerlude::{VComponent, VFragment, VText};
|
|
||||||
|
use crate::innerlude::{VComponent, VText};
|
||||||
use crate::mutations::Mutation;
|
use crate::mutations::Mutation;
|
||||||
use crate::mutations::Mutation::*;
|
use crate::mutations::Mutation::*;
|
||||||
use crate::nodes::VNode;
|
use crate::nodes::VNode;
|
||||||
use crate::nodes::{DynamicNode, TemplateNode};
|
use crate::nodes::{DynamicNode, TemplateNode};
|
||||||
use crate::virtual_dom::VirtualDom;
|
use crate::virtual_dom::VirtualDom;
|
||||||
use crate::{AttributeValue, ElementId, ScopeId, SuspenseContext, TemplateAttribute};
|
use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext};
|
||||||
|
|
||||||
impl<'b: 'static> VirtualDom {
|
impl<'b> VirtualDom {
|
||||||
/// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
|
/// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
|
||||||
///
|
///
|
||||||
/// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
|
/// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
|
||||||
@@ -23,7 +24,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
pub(crate) fn create(&mut self, template: &'b VNode<'b>) -> usize {
|
pub(crate) fn create(&mut self, template: &'b VNode<'b>) -> usize {
|
||||||
// The best renderers will have templates prehydrated and registered
|
// The best renderers will have templates prehydrated and registered
|
||||||
// Just in case, let's create the template using instructions anyways
|
// Just in case, let's create the template using instructions anyways
|
||||||
if !self.templates.contains_key(&template.template.id) {
|
if !self.templates.contains_key(&template.template.name) {
|
||||||
self.register_template(template);
|
self.register_template(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,29 +45,36 @@ impl<'b: 'static> VirtualDom {
|
|||||||
DynamicNode::Text(VText { id: slot, value }) => {
|
DynamicNode::Text(VText { id: slot, value }) => {
|
||||||
let id = self.next_element(template, template.template.node_paths[*id]);
|
let id = self.next_element(template, template.template.node_paths[*id]);
|
||||||
slot.set(id);
|
slot.set(id);
|
||||||
self.mutations.push(CreateTextNode { value, id });
|
|
||||||
|
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||||
|
let unbounded_text = unsafe { std::mem::transmute(*value) };
|
||||||
|
self.mutations.push(CreateTextNode {
|
||||||
|
value: unbounded_text,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
DynamicNode::Fragment(VFragment::Empty(slot)) => {
|
DynamicNode::Placeholder(slot) => {
|
||||||
let id = self.next_element(template, template.template.node_paths[*id]);
|
let id = self.next_element(template, template.template.node_paths[*id]);
|
||||||
slot.set(id);
|
slot.set(id);
|
||||||
self.mutations.push(CreatePlaceholder { id });
|
self.mutations.push(CreatePlaceholder { id });
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
DynamicNode::Fragment(VFragment::NonEmpty(_))
|
DynamicNode::Fragment(_) | DynamicNode::Component { .. } => {
|
||||||
| DynamicNode::Component { .. } => {
|
|
||||||
self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id)
|
self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => {
|
TemplateNode::Element { .. } | TemplateNode::Text(_) => {
|
||||||
let this_id = self.next_element(template, &[]);
|
let this_id = self.next_root(template, root_idx);
|
||||||
|
|
||||||
template.root_ids[root_idx].set(this_id);
|
template.root_ids[root_idx].set(this_id);
|
||||||
self.mutations.push(LoadTemplate {
|
self.mutations.push(LoadTemplate {
|
||||||
name: template.template.id,
|
name: template.template.name,
|
||||||
index: root_idx,
|
index: root_idx,
|
||||||
id: this_id,
|
id: this_id,
|
||||||
});
|
});
|
||||||
@@ -80,23 +88,39 @@ impl<'b: 'static> VirtualDom {
|
|||||||
// Else, it's deep in the template and we should create a new id for it
|
// Else, it's deep in the template and we should create a new id for it
|
||||||
let id = match path.len() {
|
let id = match path.len() {
|
||||||
1 => this_id,
|
1 => this_id,
|
||||||
_ => self.next_element(template, template.template.attr_paths[attr_id]),
|
_ => {
|
||||||
|
let id = self
|
||||||
|
.next_element(template, template.template.attr_paths[attr_id]);
|
||||||
|
self.mutations.push(Mutation::AssignId {
|
||||||
|
path: &path[1..],
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
id
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let attribute = template.dynamic_attrs.get(attr_id).unwrap();
|
let attribute = template.dynamic_attrs.get(attr_id).unwrap();
|
||||||
attribute.mounted_element.set(id);
|
attribute.mounted_element.set(id);
|
||||||
|
|
||||||
|
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||||
|
let unbounded_name = unsafe { std::mem::transmute(attribute.name) };
|
||||||
|
|
||||||
match &attribute.value {
|
match &attribute.value {
|
||||||
AttributeValue::Text(value) => self.mutations.push(SetAttribute {
|
AttributeValue::Text(value) => {
|
||||||
name: attribute.name,
|
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||||
value: *value,
|
let unbounded_value = unsafe { std::mem::transmute(*value) };
|
||||||
ns: attribute.namespace,
|
|
||||||
id,
|
self.mutations.push(SetAttribute {
|
||||||
}),
|
name: unbounded_name,
|
||||||
|
value: unbounded_value,
|
||||||
|
ns: attribute.namespace,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
}
|
||||||
AttributeValue::Bool(value) => {
|
AttributeValue::Bool(value) => {
|
||||||
self.mutations.push(SetBoolAttribute {
|
self.mutations.push(SetBoolAttribute {
|
||||||
name: attribute.name,
|
name: unbounded_name,
|
||||||
value: *value,
|
value: *value,
|
||||||
id,
|
id,
|
||||||
})
|
})
|
||||||
@@ -104,7 +128,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
AttributeValue::Listener(_) => {
|
AttributeValue::Listener(_) => {
|
||||||
self.mutations.push(NewEventListener {
|
self.mutations.push(NewEventListener {
|
||||||
// all listeners start with "on"
|
// all listeners start with "on"
|
||||||
event_name: &attribute.name[2..],
|
name: &unbounded_name[2..],
|
||||||
scope: cur_scope,
|
scope: cur_scope,
|
||||||
id,
|
id,
|
||||||
})
|
})
|
||||||
@@ -179,7 +203,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
fn register_template(&mut self, template: &'b VNode<'b>) {
|
fn register_template(&mut self, template: &'b VNode<'b>) {
|
||||||
// First, make sure we mark the template as seen, regardless if we process it
|
// First, make sure we mark the template as seen, regardless if we process it
|
||||||
self.templates
|
self.templates
|
||||||
.insert(template.template.id, template.template);
|
.insert(template.template.name, template.template);
|
||||||
|
|
||||||
// If it's all dynamic nodes, then we don't need to register it
|
// If it's all dynamic nodes, then we don't need to register it
|
||||||
// Quickly run through and see if it's all just dynamic nodes
|
// Quickly run through and see if it's all just dynamic nodes
|
||||||
@@ -199,85 +223,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for node in template.template.roots {
|
self.mutations.templates.push(template.template);
|
||||||
self.create_static_node(template, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mutations.template_mutations.push(SaveTemplate {
|
|
||||||
name: template.template.id,
|
|
||||||
m: template.template.roots.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn create_static_node(
|
|
||||||
&mut self,
|
|
||||||
template: &'b VNode<'b>,
|
|
||||||
node: &'b TemplateNode<'static>,
|
|
||||||
) {
|
|
||||||
match *node {
|
|
||||||
// Todo: create the children's template
|
|
||||||
TemplateNode::Dynamic(_) => self
|
|
||||||
.mutations
|
|
||||||
.template_mutations
|
|
||||||
.push(CreateStaticPlaceholder {}),
|
|
||||||
TemplateNode::Text(value) => self
|
|
||||||
.mutations
|
|
||||||
.template_mutations
|
|
||||||
.push(CreateStaticText { value }),
|
|
||||||
TemplateNode::DynamicText { .. } => self
|
|
||||||
.mutations
|
|
||||||
.template_mutations
|
|
||||||
.push(CreateStaticText { value: "d" }),
|
|
||||||
|
|
||||||
TemplateNode::Element {
|
|
||||||
attrs,
|
|
||||||
children,
|
|
||||||
namespace,
|
|
||||||
tag,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if let Some(namespace) = namespace {
|
|
||||||
self.mutations
|
|
||||||
.template_mutations
|
|
||||||
.push(CreateElementNamespace {
|
|
||||||
name: tag,
|
|
||||||
namespace,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.mutations
|
|
||||||
.template_mutations
|
|
||||||
.push(CreateElement { name: tag });
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mutations
|
|
||||||
.template_mutations
|
|
||||||
.extend(attrs.into_iter().filter_map(|attr| match attr {
|
|
||||||
TemplateAttribute::Static {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
namespace,
|
|
||||||
..
|
|
||||||
} => Some(SetStaticAttribute {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
ns: *namespace,
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if children.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
children
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|child| self.create_static_node(template, child));
|
|
||||||
|
|
||||||
self.mutations
|
|
||||||
.template_mutations
|
|
||||||
.push(AppendChildren { m: children.len() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn create_dynamic_node(
|
pub(crate) fn create_dynamic_node(
|
||||||
@@ -289,7 +235,8 @@ impl<'b: 'static> VirtualDom {
|
|||||||
use DynamicNode::*;
|
use DynamicNode::*;
|
||||||
match node {
|
match node {
|
||||||
Text(text) => self.create_dynamic_text(template, text, idx),
|
Text(text) => self.create_dynamic_text(template, text, idx),
|
||||||
Fragment(frag) => self.create_fragment(frag, template, idx),
|
Fragment(frag) => self.create_fragment(frag),
|
||||||
|
Placeholder(frag) => self.create_placeholder(frag, template, idx),
|
||||||
Component(component) => self.create_component_node(template, component, idx),
|
Component(component) => self.create_component_node(template, component, idx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,59 +253,60 @@ impl<'b: 'static> VirtualDom {
|
|||||||
// Make sure the text node is assigned to the correct element
|
// Make sure the text node is assigned to the correct element
|
||||||
text.id.set(new_id);
|
text.id.set(new_id);
|
||||||
|
|
||||||
|
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||||
|
let value = unsafe { std::mem::transmute(text.value) };
|
||||||
|
|
||||||
// Add the mutation to the list
|
// Add the mutation to the list
|
||||||
self.mutations.push(HydrateText {
|
self.mutations.push(HydrateText {
|
||||||
id: new_id,
|
id: new_id,
|
||||||
path: &template.template.node_paths[idx][1..],
|
path: &template.template.node_paths[idx][1..],
|
||||||
value: text.value,
|
value,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Since we're hydrating an existing node, we don't create any new nodes
|
// Since we're hydrating an existing node, we don't create any new nodes
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn create_fragment(
|
pub(crate) fn create_placeholder(
|
||||||
&mut self,
|
&mut self,
|
||||||
frag: &'b VFragment<'b>,
|
slot: &Cell<ElementId>,
|
||||||
template: &'b VNode<'b>,
|
template: &'b VNode<'b>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
match frag {
|
// Allocate a dynamic element reference for this text node
|
||||||
VFragment::NonEmpty(nodes) => {
|
let id = self.next_element(template, template.template.node_paths[idx]);
|
||||||
nodes.iter().fold(0, |acc, child| acc + self.create(child))
|
|
||||||
}
|
|
||||||
|
|
||||||
VFragment::Empty(slot) => {
|
// Make sure the text node is assigned to the correct element
|
||||||
// Allocate a dynamic element reference for this text node
|
slot.set(id);
|
||||||
let id = self.next_element(template, template.template.node_paths[idx]);
|
|
||||||
|
|
||||||
// Make sure the text node is assigned to the correct element
|
// Assign the ID to the existing node in the template
|
||||||
slot.set(id);
|
self.mutations.push(AssignId {
|
||||||
|
path: &template.template.node_paths[idx][1..],
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
// Assign the ID to the existing node in the template
|
// Since the placeholder is already in the DOM, we don't create any new nodes
|
||||||
self.mutations.push(AssignId {
|
0
|
||||||
path: &template.template.node_paths[idx][1..],
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Since the placeholder is already in the DOM, we don't create any new nodes
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_component_node(
|
pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
|
||||||
|
nodes.iter().fold(0, |acc, child| acc + self.create(child))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn create_component_node(
|
||||||
&mut self,
|
&mut self,
|
||||||
template: &'b VNode<'b>,
|
template: &'b VNode<'b>,
|
||||||
component: &'b VComponent<'b>,
|
component: &'b VComponent<'b>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let props = component.props.replace(None).unwrap();
|
let props = component
|
||||||
|
.props
|
||||||
|
.replace(None)
|
||||||
|
.expect("Props to always exist when a component is being created");
|
||||||
|
|
||||||
let prop_ptr = unsafe { std::mem::transmute(props.as_ref()) };
|
let unbounded_props = unsafe { std::mem::transmute(props) };
|
||||||
let scope = self.new_scope(prop_ptr).id;
|
|
||||||
|
|
||||||
component.props.replace(Some(props));
|
let scope = self.new_scope(unbounded_props).id;
|
||||||
component.scope.set(Some(scope));
|
component.scope.set(Some(scope));
|
||||||
|
|
||||||
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
||||||
@@ -366,10 +314,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
use RenderReturn::*;
|
use RenderReturn::*;
|
||||||
|
|
||||||
match return_nodes {
|
match return_nodes {
|
||||||
Sync(Ok(t)) => {
|
Sync(Ok(t)) => self.mount_component(scope, template, t, idx),
|
||||||
self.mount_component(scope, template, t, idx)
|
|
||||||
// self.create(t)
|
|
||||||
}
|
|
||||||
Sync(Err(_e)) => todo!("Propogate error upwards"),
|
Sync(Err(_e)) => todo!("Propogate error upwards"),
|
||||||
Async(_) => self.mount_component_placeholder(template, idx, scope),
|
Async(_) => self.mount_component_placeholder(template, idx, scope),
|
||||||
}
|
}
|
||||||
@@ -384,7 +329,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
) -> usize {
|
) -> usize {
|
||||||
// Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary
|
// Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary
|
||||||
// is encountered
|
// is encountered
|
||||||
let mutations_to_this_point = self.mutations.len();
|
let mutations_to_this_point = self.mutations.edits.len();
|
||||||
|
|
||||||
// Create the component's root element
|
// Create the component's root element
|
||||||
let created = self.create_scope(scope, new);
|
let created = self.create_scope(scope, new);
|
||||||
@@ -396,7 +341,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
|
|
||||||
// If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense
|
// If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense
|
||||||
let boundary = match self.scopes[scope.0].has_context::<SuspenseContext>() {
|
let boundary = match self.scopes[scope.0].has_context::<SuspenseContext>() {
|
||||||
Some(boundary) => boundary,
|
Some(boundary) => unsafe { &*(boundary as *const SuspenseContext) },
|
||||||
_ => return created,
|
_ => return created,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -410,7 +355,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
// Note that we break off dynamic mutations only - since static mutations aren't rendered immediately
|
// Note that we break off dynamic mutations only - since static mutations aren't rendered immediately
|
||||||
let split_off = unsafe {
|
let split_off = unsafe {
|
||||||
std::mem::transmute::<Vec<Mutation>, Vec<Mutation>>(
|
std::mem::transmute::<Vec<Mutation>, Vec<Mutation>>(
|
||||||
self.mutations.split_off(mutations_to_this_point),
|
self.mutations.edits.split_off(mutations_to_this_point),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
boundary.mutations.borrow_mut().edits.extend(split_off);
|
boundary.mutations.borrow_mut().edits.extend(split_off);
|
||||||
|
|||||||
+335
-236
@@ -1,8 +1,10 @@
|
|||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arena::ElementId,
|
arena::ElementId,
|
||||||
factory::RenderReturn,
|
innerlude::{DirtyScope, VComponent, VText},
|
||||||
innerlude::{DirtyScope, VComponent, VFragment, VText},
|
|
||||||
mutations::Mutation,
|
mutations::Mutation,
|
||||||
|
nodes::RenderReturn,
|
||||||
nodes::{DynamicNode, VNode},
|
nodes::{DynamicNode, VNode},
|
||||||
scopes::ScopeId,
|
scopes::ScopeId,
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
@@ -10,71 +12,66 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use fxhash::{FxHashMap, FxHashSet};
|
use fxhash::{FxHashMap, FxHashSet};
|
||||||
use std::cell::Cell;
|
|
||||||
use DynamicNode::*;
|
use DynamicNode::*;
|
||||||
|
|
||||||
impl<'b: 'static> VirtualDom {
|
impl<'b> VirtualDom {
|
||||||
pub fn diff_scope(&mut self, scope: ScopeId) {
|
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
|
||||||
let scope_state = &mut self.scopes[scope.0];
|
let scope_state = &mut self.scopes[scope.0];
|
||||||
|
|
||||||
// Load the old and new bump arenas
|
|
||||||
let cur_arena = scope_state.current_frame();
|
|
||||||
let prev_arena = scope_state.previous_frame();
|
|
||||||
|
|
||||||
// Make sure the nodes arent null (they've been set properly)
|
|
||||||
// This is a rough check to make sure we're not entering any UB
|
|
||||||
assert_ne!(
|
|
||||||
cur_arena.node.get(),
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
"Call rebuild before diffing"
|
|
||||||
);
|
|
||||||
assert_ne!(
|
|
||||||
prev_arena.node.get(),
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
"Call rebuild before diffing"
|
|
||||||
);
|
|
||||||
|
|
||||||
self.scope_stack.push(scope);
|
self.scope_stack.push(scope);
|
||||||
unsafe {
|
unsafe {
|
||||||
let cur_arena = cur_arena.load_node();
|
// Load the old and new bump arenas
|
||||||
let prev_arena = prev_arena.load_node();
|
let old = scope_state
|
||||||
self.diff_maybe_node(prev_arena, cur_arena);
|
.previous_frame()
|
||||||
|
.try_load_node()
|
||||||
|
.expect("Call rebuild before diffing");
|
||||||
|
let new = scope_state
|
||||||
|
.current_frame()
|
||||||
|
.try_load_node()
|
||||||
|
.expect("Call rebuild before diffing");
|
||||||
|
self.diff_maybe_node(old, new);
|
||||||
}
|
}
|
||||||
self.scope_stack.pop();
|
self.scope_stack.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff_maybe_node(&mut self, left: &'b RenderReturn<'b>, right: &'b RenderReturn<'b>) {
|
fn diff_maybe_node(&mut self, old: &'b RenderReturn<'b>, new: &'b RenderReturn<'b>) {
|
||||||
use RenderReturn::{Async, Sync};
|
use RenderReturn::{Async, Sync};
|
||||||
match (left, right) {
|
match (old, new) {
|
||||||
// diff
|
|
||||||
(Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
|
(Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
|
||||||
|
|
||||||
_ => todo!("handle diffing nonstandard nodes"),
|
// Err cases
|
||||||
// // remove old with placeholder
|
(Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e),
|
||||||
// (Sync(Ok(l)), Sync(None)) | (Sync(Ok(l)), Async(_)) => {
|
(Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r),
|
||||||
// //
|
(Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ }
|
||||||
// let id = self.next_element(l, &[]); // todo!
|
|
||||||
// m.push(Mutation::CreatePlaceholder { id });
|
|
||||||
// self.drop_template(m, l, true);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // remove placeholder with nodes
|
// Async
|
||||||
// (Sync(None), Sync(Ok(_))) => {}
|
(Sync(Ok(_l)), Async(_)) => todo!(),
|
||||||
// (Async(_), Sync(Ok(v))) => {}
|
(Sync(Err(_e)), Async(_)) => todo!(),
|
||||||
|
(Async(_), Sync(Ok(_r))) => todo!(),
|
||||||
// // nothing... just transfer the placeholders over
|
(Async(_), Sync(Err(_e))) => { /* nothing */ }
|
||||||
// (Async(_), Async(_))
|
(Async(_), Async(_)) => { /* nothing */ }
|
||||||
// | (Sync(None), Sync(None))
|
|
||||||
// | (Sync(None), Async(_))
|
|
||||||
// | (Async(_), Sync(None)) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
|
fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {
|
||||||
if left_template.template.id != right_template.template.id {
|
todo!("Not yet handling error rollover")
|
||||||
|
}
|
||||||
|
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {
|
||||||
|
todo!("Not yet handling error rollover")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
|
||||||
|
println!(
|
||||||
|
"diffing node {:#?},\n\n{:#?}",
|
||||||
|
left_template.template.name, right_template.template.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if left_template.template.name != right_template.template.name {
|
||||||
return self.light_diff_templates(left_template, right_template);
|
return self.light_diff_templates(left_template, right_template);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("diffing attributs...");
|
||||||
|
|
||||||
for (left_attr, right_attr) in left_template
|
for (left_attr, right_attr) in left_template
|
||||||
.dynamic_attrs
|
.dynamic_attrs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -85,36 +82,59 @@ impl<'b: 'static> VirtualDom {
|
|||||||
.mounted_element
|
.mounted_element
|
||||||
.set(left_attr.mounted_element.get());
|
.set(left_attr.mounted_element.get());
|
||||||
|
|
||||||
|
// We want to make sure anything listener that gets pulled is valid
|
||||||
|
if let AttributeValue::Listener(_) = right_attr.value {
|
||||||
|
self.update_template(left_attr.mounted_element.get(), right_template);
|
||||||
|
}
|
||||||
|
|
||||||
if left_attr.value != right_attr.value || left_attr.volatile {
|
if left_attr.value != right_attr.value || left_attr.volatile {
|
||||||
// todo: add more types of attribute values
|
// todo: add more types of attribute values
|
||||||
match right_attr.value {
|
match right_attr.value {
|
||||||
AttributeValue::Text(text) => {
|
AttributeValue::Text(text) => {
|
||||||
|
let name = unsafe { std::mem::transmute(left_attr.name) };
|
||||||
|
let value = unsafe { std::mem::transmute(text) };
|
||||||
self.mutations.push(Mutation::SetAttribute {
|
self.mutations.push(Mutation::SetAttribute {
|
||||||
id: left_attr.mounted_element.get(),
|
id: left_attr.mounted_element.get(),
|
||||||
name: left_attr.name,
|
|
||||||
value: text,
|
|
||||||
ns: right_attr.namespace,
|
ns: right_attr.namespace,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// todo: more types of attribute values
|
// todo: more types of attribute values
|
||||||
_ => (),
|
_ => todo!("other attribute types"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (left_node, right_node) in left_template
|
println!("diffing dyn nodes...");
|
||||||
|
|
||||||
|
for (idx, (left_node, right_node)) in left_template
|
||||||
.dynamic_nodes
|
.dynamic_nodes
|
||||||
.iter()
|
.iter()
|
||||||
.zip(right_template.dynamic_nodes.iter())
|
.zip(right_template.dynamic_nodes.iter())
|
||||||
|
.enumerate()
|
||||||
{
|
{
|
||||||
match (left_node, right_node) {
|
match (left_node, right_node) {
|
||||||
(Text(left), Text(right)) => self.diff_vtext(left, right),
|
(Text(left), Text(right)) => self.diff_vtext(left, right),
|
||||||
(Fragment(left), Fragment(right)) => self.diff_vfragment(left, right),
|
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
|
||||||
(Component(left), Component(right)) => self.diff_vcomponent(left, right),
|
(Placeholder(left), Placeholder(right)) => {
|
||||||
_ => self.replace(left_template, right_template, left_node, right_node),
|
right.set(left.get());
|
||||||
|
}
|
||||||
|
(Component(left), Component(right)) => {
|
||||||
|
self.diff_vcomponent(left, right, right_template, idx)
|
||||||
|
}
|
||||||
|
(Placeholder(left), Fragment(right)) => {
|
||||||
|
self.replace_placeholder_with_nodes(left, right)
|
||||||
|
}
|
||||||
|
(Fragment(left), Placeholder(right)) => {
|
||||||
|
self.replace_nodes_with_placeholder(left, right)
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("applying roots...");
|
||||||
|
|
||||||
// Make sure the roots get transferred over
|
// Make sure the roots get transferred over
|
||||||
for (left, right) in left_template
|
for (left, right) in left_template
|
||||||
.root_ids
|
.root_ids
|
||||||
@@ -125,31 +145,73 @@ impl<'b: 'static> VirtualDom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff_vcomponent(&mut self, left: &'b VComponent<'b>, right: &'b VComponent<'b>) {
|
fn replace_placeholder_with_nodes(&mut self, l: &'b Cell<ElementId>, r: &'b [VNode<'b>]) {
|
||||||
// Due to how templates work, we should never get two different components. The only way we could enter
|
let m = self.create_children(r);
|
||||||
// this codepath is through "light_diff", but we check there that the pointers are the same
|
let id = l.get();
|
||||||
assert_eq!(left.render_fn, right.render_fn);
|
self.mutations.push(Mutation::ReplaceWith { id, m });
|
||||||
|
self.reclaim(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell<ElementId>) {
|
||||||
|
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder
|
||||||
|
let placeholder = self.next_element(&l[0], &[]);
|
||||||
|
r.set(placeholder);
|
||||||
|
self.mutations
|
||||||
|
.push(Mutation::CreatePlaceholder { id: placeholder });
|
||||||
|
|
||||||
|
// Remove the old nodes, except for onea
|
||||||
|
let first = self.replace_inner(&l[0]);
|
||||||
|
self.remove_nodes(&l[1..]);
|
||||||
|
|
||||||
|
self.mutations
|
||||||
|
.push(Mutation::ReplaceWith { id: first, m: 1 });
|
||||||
|
|
||||||
|
self.try_reclaim(first);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_vcomponent(
|
||||||
|
&mut self,
|
||||||
|
left: &'b VComponent<'b>,
|
||||||
|
right: &'b VComponent<'b>,
|
||||||
|
right_template: &'b VNode<'b>,
|
||||||
|
idx: usize,
|
||||||
|
) {
|
||||||
|
// Replace components that have different render fns
|
||||||
|
if left.render_fn != right.render_fn {
|
||||||
|
dbg!(&left, &right);
|
||||||
|
let created = self.create_component_node(right_template, right, idx);
|
||||||
|
let head = unsafe {
|
||||||
|
self.scopes[left.scope.get().unwrap().0]
|
||||||
|
.root_node()
|
||||||
|
.extend_lifetime_ref()
|
||||||
|
};
|
||||||
|
let id = match head {
|
||||||
|
RenderReturn::Sync(Ok(node)) => self.replace_inner(node),
|
||||||
|
_ => todo!(),
|
||||||
|
};
|
||||||
|
self.mutations
|
||||||
|
.push(Mutation::ReplaceWith { id, m: created });
|
||||||
|
self.drop_scope(left.scope.get().unwrap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure the new vcomponent has the right scopeid associated to it
|
// Make sure the new vcomponent has the right scopeid associated to it
|
||||||
let scope_id = left.scope.get().unwrap();
|
let scope_id = left.scope.get().unwrap();
|
||||||
right.scope.set(Some(scope_id));
|
right.scope.set(Some(scope_id));
|
||||||
|
|
||||||
// copy out the box for both
|
// copy out the box for both
|
||||||
let old = left.props.replace(None).unwrap();
|
let old = self.scopes[scope_id.0].props.as_ref();
|
||||||
let new = right.props.replace(None).unwrap();
|
let new = right.props.replace(None).unwrap();
|
||||||
|
|
||||||
// If the props are static, then we try to memoize by setting the new with the old
|
// If the props are static, then we try to memoize by setting the new with the old
|
||||||
// The target scopestate still has the reference to the old props, so there's no need to update anything
|
// The target scopestate still has the reference to the old props, so there's no need to update anything
|
||||||
// This also implicitly drops the new props since they're not used
|
// This also implicitly drops the new props since they're not used
|
||||||
if left.static_props && unsafe { old.memoize(new.as_ref()) } {
|
if left.static_props && unsafe { old.as_ref().unwrap().memoize(new.as_ref()) } {
|
||||||
return right.props.set(Some(old));
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the props are dynamic *or* the memoization failed, then we need to diff the props
|
|
||||||
|
|
||||||
// First, move over the props from the old to the new, dropping old props in the process
|
// First, move over the props from the old to the new, dropping old props in the process
|
||||||
self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new.as_ref()) };
|
self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new) };
|
||||||
right.props.set(Some(new));
|
|
||||||
|
|
||||||
// Now run the component and diff it
|
// Now run the component and diff it
|
||||||
self.run_scope(scope_id);
|
self.run_scope(scope_id);
|
||||||
@@ -171,7 +233,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
/// This is mostly implemented to help solve the issue where the same component is rendered under two different
|
/// This is mostly implemented to help solve the issue where the same component is rendered under two different
|
||||||
/// conditions:
|
/// conditions:
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust, ignore
|
||||||
/// if enabled {
|
/// if enabled {
|
||||||
/// rsx!{ Component { enabled_sign: "abc" } }
|
/// rsx!{ Component { enabled_sign: "abc" } }
|
||||||
/// } else {
|
/// } else {
|
||||||
@@ -182,8 +244,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
/// However, we should not that it's explicit in the docs that this is not a guarantee. If you need to preserve state,
|
/// However, we should not that it's explicit in the docs that this is not a guarantee. If you need to preserve state,
|
||||||
/// then you should be passing in separate props instead.
|
/// then you should be passing in separate props instead.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```rust, ignore
|
||||||
///
|
|
||||||
/// let props = if enabled {
|
/// let props = if enabled {
|
||||||
/// ComponentProps { enabled_sign: "abc" }
|
/// ComponentProps { enabled_sign: "abc" }
|
||||||
/// } else {
|
/// } else {
|
||||||
@@ -196,10 +257,11 @@ impl<'b: 'static> VirtualDom {
|
|||||||
/// ```
|
/// ```
|
||||||
fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
|
fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
|
||||||
match matching_components(left, right) {
|
match matching_components(left, right) {
|
||||||
None => self.replace_template(left, right),
|
None => self.replace(left, right),
|
||||||
Some(components) => components
|
Some(components) => components
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.for_each(|(l, r)| self.diff_vcomponent(l, r)),
|
.enumerate()
|
||||||
|
.for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,106 +270,121 @@ impl<'b: 'static> VirtualDom {
|
|||||||
/// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's
|
/// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's
|
||||||
/// different.
|
/// different.
|
||||||
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
|
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
|
||||||
right.id.set(left.id.get());
|
let id = left.id.get();
|
||||||
|
|
||||||
|
right.id.set(id);
|
||||||
if left.value != right.value {
|
if left.value != right.value {
|
||||||
self.mutations.push(Mutation::SetText {
|
let value = unsafe { std::mem::transmute(right.value) };
|
||||||
id: left.id.get(),
|
self.mutations.push(Mutation::SetText { id, value });
|
||||||
value: right.value,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff_vfragment(&mut self, left: &'b VFragment<'b>, right: &'b VFragment<'b>) {
|
/// Remove all the top-level nodes, returning the firstmost root ElementId
|
||||||
use VFragment::*;
|
///
|
||||||
match (left, right) {
|
/// All IDs will be garbage collected
|
||||||
(Empty(l), Empty(r)) => r.set(l.get()),
|
fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId {
|
||||||
(Empty(l), NonEmpty(r)) => self.replace_placeholder_with_nodes(l, r),
|
let id = match node.dynamic_root(0) {
|
||||||
(NonEmpty(l), Empty(r)) => self.replace_nodes_with_placeholder(l, r),
|
None => node.root_ids[0].get(),
|
||||||
(NonEmpty(old), NonEmpty(new)) => self.diff_non_empty_fragment(new, old),
|
Some(Text(t)) => t.id.get(),
|
||||||
}
|
Some(Placeholder(e)) => e.get(),
|
||||||
}
|
Some(Fragment(nodes)) => {
|
||||||
|
let id = self.replace_inner(&nodes[0]);
|
||||||
fn replace_placeholder_with_nodes(&mut self, l: &'b Cell<ElementId>, r: &'b [VNode<'b>]) {
|
self.remove_nodes(&nodes[1..]);
|
||||||
let created = self.create_children(r);
|
id
|
||||||
self.mutations.push(Mutation::ReplaceWith {
|
|
||||||
id: l.get(),
|
|
||||||
m: created,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell<ElementId>) {
|
|
||||||
//
|
|
||||||
// Remove the old nodes, except for one
|
|
||||||
self.remove_nodes(&l[1..]);
|
|
||||||
let first = self.find_first_element(&l[0]);
|
|
||||||
self.remove_all_but_first_node(&l[0]);
|
|
||||||
|
|
||||||
let placeholder = self.next_element(&l[0], &[]);
|
|
||||||
r.set(placeholder);
|
|
||||||
self.mutations
|
|
||||||
.push(Mutation::CreatePlaceholder { id: placeholder });
|
|
||||||
self.mutations
|
|
||||||
.push(Mutation::ReplaceWith { id: first, m: 1 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all the top-level nodes, returning the
|
|
||||||
fn remove_all_but_first_node(&mut self, node: &'b VNode<'b>) {
|
|
||||||
match &node.template.roots[0] {
|
|
||||||
TemplateNode::Text(_) | TemplateNode::Element { .. } => {}
|
|
||||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
|
||||||
match &node.dynamic_nodes[*id] {
|
|
||||||
Text(_) => {}
|
|
||||||
Fragment(VFragment::Empty(_)) => {}
|
|
||||||
Fragment(VFragment::NonEmpty(nodes)) => {
|
|
||||||
self.remove_all_but_first_node(&nodes[0]);
|
|
||||||
self.remove_nodes(&nodes[1..]);
|
|
||||||
}
|
|
||||||
Component(comp) => {
|
|
||||||
let scope = comp.scope.get().unwrap();
|
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
|
||||||
RenderReturn::Sync(Ok(t)) => self.remove_all_but_first_node(t),
|
|
||||||
_ => todo!("cannot handle nonstandard nodes"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
Some(Component(comp)) => {
|
||||||
|
let scope = comp.scope.get().unwrap();
|
||||||
|
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||||
|
RenderReturn::Sync(Ok(t)) => self.replace_inner(t),
|
||||||
|
_ => todo!("cannot handle nonstandard nodes"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Just remove the rest
|
// Just remove the rest from the dom
|
||||||
for (idx, _) in node.template.roots.iter().enumerate().skip(1) {
|
for (idx, _) in node.template.roots.iter().enumerate().skip(1) {
|
||||||
self.remove_root_node(node, idx);
|
self.remove_root_node(node, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Garabge collect all of the nodes since this gets used in replace
|
||||||
|
self.clean_up_node(node);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up the node, not generating mutations
|
||||||
|
///
|
||||||
|
/// Simply walks through the dynamic nodes
|
||||||
|
fn clean_up_node(&mut self, node: &'b VNode<'b>) {
|
||||||
|
for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() {
|
||||||
|
// Roots are cleaned up automatically?
|
||||||
|
if node.template.node_paths[idx].len() == 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match dyn_node {
|
||||||
|
Component(comp) => {
|
||||||
|
let scope = comp.scope.get().unwrap();
|
||||||
|
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||||
|
RenderReturn::Sync(Ok(t)) => self.clean_up_node(t),
|
||||||
|
_ => todo!("cannot handle nonstandard nodes"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Text(t) => self.reclaim(t.id.get()),
|
||||||
|
Placeholder(t) => self.reclaim(t.get()),
|
||||||
|
Fragment(nodes) => nodes.iter().for_each(|node| self.clean_up_node(node)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// we clean up nodes with dynamic attributes, provided the node is unique and not a root node
|
||||||
|
let mut id = None;
|
||||||
|
for (idx, attr) in node.dynamic_attrs.iter().enumerate() {
|
||||||
|
// We'll clean up the root nodes either way, so don't worry
|
||||||
|
if node.template.attr_paths[idx].len() == 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_id = attr.mounted_element.get();
|
||||||
|
|
||||||
|
if id == Some(next_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
id = Some(next_id);
|
||||||
|
|
||||||
|
self.reclaim(next_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) {
|
fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) {
|
||||||
let root = node.template.roots[idx];
|
match node.dynamic_root(idx) {
|
||||||
match root {
|
Some(Text(i)) => {
|
||||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => {
|
let id = i.id.get();
|
||||||
self.mutations.push(Mutation::Remove {
|
self.mutations.push(Mutation::Remove { id });
|
||||||
id: node.root_ids[idx].get(),
|
self.reclaim(id);
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
Some(Placeholder(e)) => {
|
||||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
let id = e.get();
|
||||||
match &node.dynamic_nodes[id] {
|
self.mutations.push(Mutation::Remove { id });
|
||||||
Text(i) => self.mutations.push(Mutation::Remove { id: i.id.get() }),
|
self.reclaim(id);
|
||||||
Fragment(VFragment::Empty(e)) => {
|
}
|
||||||
self.mutations.push(Mutation::Remove { id: e.get() })
|
Some(Fragment(nodes)) => self.remove_nodes(nodes),
|
||||||
}
|
Some(Component(comp)) => {
|
||||||
Fragment(VFragment::NonEmpty(nodes)) => self.remove_nodes(nodes),
|
let scope = comp.scope.get().unwrap();
|
||||||
Component(comp) => {
|
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||||
let scope = comp.scope.get().unwrap();
|
RenderReturn::Sync(Ok(t)) => self.remove_node(t),
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
_ => todo!("cannot handle nonstandard nodes"),
|
||||||
RenderReturn::Sync(Ok(t)) => self.remove_node(t),
|
|
||||||
_ => todo!("cannot handle nonstandard nodes"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
None => {
|
||||||
|
let id = node.root_ids[idx].get();
|
||||||
|
self.mutations.push(Mutation::Remove { id });
|
||||||
|
self.reclaim(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff_non_empty_fragment(&mut self, new: &'b [VNode<'b>], old: &'b [VNode<'b>]) {
|
fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
|
||||||
let new_is_keyed = new[0].key.is_some();
|
let new_is_keyed = new[0].key.is_some();
|
||||||
let old_is_keyed = old[0].key.is_some();
|
let old_is_keyed = old[0].key.is_some();
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
@@ -318,6 +395,9 @@ impl<'b: 'static> VirtualDom {
|
|||||||
old.iter().all(|o| o.key.is_some() == old_is_keyed),
|
old.iter().all(|o| o.key.is_some() == old_is_keyed),
|
||||||
"all siblings must be keyed or all siblings must be non-keyed"
|
"all siblings must be keyed or all siblings must be non-keyed"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
println!("Diffing fragment {:?}, {:?}", old.len(), new.len());
|
||||||
|
|
||||||
if new_is_keyed && old_is_keyed {
|
if new_is_keyed && old_is_keyed {
|
||||||
self.diff_keyed_children(old, new);
|
self.diff_keyed_children(old, new);
|
||||||
} else {
|
} else {
|
||||||
@@ -340,7 +420,9 @@ impl<'b: 'static> VirtualDom {
|
|||||||
debug_assert!(!new.is_empty());
|
debug_assert!(!new.is_empty());
|
||||||
debug_assert!(!old.is_empty());
|
debug_assert!(!old.is_empty());
|
||||||
|
|
||||||
match old.len().cmp(&new.len()) {
|
println!("Diffing non keyed children");
|
||||||
|
|
||||||
|
match dbg!(old.len().cmp(&new.len())) {
|
||||||
Ordering::Greater => self.remove_nodes(&old[new.len()..]),
|
Ordering::Greater => self.remove_nodes(&old[new.len()..]),
|
||||||
Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()),
|
Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()),
|
||||||
Ordering::Equal => {}
|
Ordering::Equal => {}
|
||||||
@@ -554,16 +636,15 @@ impl<'b: 'static> VirtualDom {
|
|||||||
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
|
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
|
||||||
// create the new children afresh.
|
// create the new children afresh.
|
||||||
if shared_keys.is_empty() {
|
if shared_keys.is_empty() {
|
||||||
if let Some(first_old) = old.get(0) {
|
if old.get(0).is_some() {
|
||||||
self.remove_nodes(&old[1..]);
|
self.remove_nodes(&old[1..]);
|
||||||
let nodes_created = self.create_children(new);
|
self.replace_many(&old[0], new);
|
||||||
self.replace_node_with_on_stack(first_old, nodes_created);
|
|
||||||
} else {
|
} else {
|
||||||
// I think this is wrong - why are we appending?
|
// I think this is wrong - why are we appending?
|
||||||
// only valid of the if there are no trailing elements
|
// only valid of the if there are no trailing elements
|
||||||
// self.create_and_append_children(new);
|
// self.create_and_append_children(new);
|
||||||
|
|
||||||
todo!()
|
todo!("we should never be appending - just creating N");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -573,8 +654,7 @@ impl<'b: 'static> VirtualDom {
|
|||||||
for child in old {
|
for child in old {
|
||||||
let key = child.key.unwrap();
|
let key = child.key.unwrap();
|
||||||
if !shared_keys.contains(&key) {
|
if !shared_keys.contains(&key) {
|
||||||
todo!("remove node");
|
self.remove_node(child);
|
||||||
// self.remove_nodes( [child]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,39 +757,33 @@ impl<'b: 'static> VirtualDom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_after(&mut self, node: &'b VNode<'b>, nodes_created: usize) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn insert_before(&mut self, node: &'b VNode<'b>, nodes_created: usize) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_node_with_on_stack(&mut self, old: &'b VNode<'b>, m: usize) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove these nodes from the dom
|
/// Remove these nodes from the dom
|
||||||
/// Wont generate mutations for the inner nodes
|
/// Wont generate mutations for the inner nodes
|
||||||
fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) {
|
fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) {
|
||||||
nodes.into_iter().for_each(|node| self.remove_node(node));
|
nodes.iter().for_each(|node| self.remove_node(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_node(&mut self, node: &'b VNode<'b>) {
|
fn remove_node(&mut self, node: &'b VNode<'b>) {
|
||||||
for (idx, root) in node.template.roots.iter().enumerate() {
|
for (idx, _) in node.template.roots.iter().enumerate() {
|
||||||
let id = match root {
|
let id = match node.dynamic_root(idx) {
|
||||||
TemplateNode::Text(_) | TemplateNode::Element { .. } => node.root_ids[idx].get(),
|
Some(Text(t)) => t.id.get(),
|
||||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
Some(Placeholder(t)) => t.get(),
|
||||||
match &node.dynamic_nodes[*id] {
|
Some(Fragment(t)) => return self.remove_nodes(t),
|
||||||
Text(t) => t.id.get(),
|
Some(Component(comp)) => return self.remove_component(comp.scope.get().unwrap()),
|
||||||
Fragment(VFragment::Empty(t)) => t.get(),
|
None => node.root_ids[idx].get(),
|
||||||
Fragment(VFragment::NonEmpty(t)) => return self.remove_nodes(t),
|
|
||||||
Component(comp) => return self.remove_component(comp.scope.get().unwrap()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.mutations.push(Mutation::Remove { id })
|
self.mutations.push(Mutation::Remove { id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.clean_up_node(node);
|
||||||
|
|
||||||
|
for root in node.root_ids {
|
||||||
|
let id = root.get();
|
||||||
|
if id.0 != 0 {
|
||||||
|
self.reclaim(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_component(&mut self, scope_id: ScopeId) {
|
fn remove_component(&mut self, scope_id: ScopeId) {
|
||||||
@@ -727,8 +801,42 @@ impl<'b: 'static> VirtualDom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Push all the real nodes on the stack
|
/// Push all the real nodes on the stack
|
||||||
fn push_all_real_nodes(&mut self, node: &VNode) -> usize {
|
fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
|
||||||
todo!()
|
let mut onstack = 0;
|
||||||
|
|
||||||
|
for (idx, _) in node.template.roots.iter().enumerate() {
|
||||||
|
match node.dynamic_root(idx) {
|
||||||
|
Some(Text(t)) => {
|
||||||
|
self.mutations.push(Mutation::PushRoot { id: t.id.get() });
|
||||||
|
onstack += 1;
|
||||||
|
}
|
||||||
|
Some(Placeholder(t)) => {
|
||||||
|
self.mutations.push(Mutation::PushRoot { id: t.get() });
|
||||||
|
onstack += 1;
|
||||||
|
}
|
||||||
|
Some(Fragment(nodes)) => {
|
||||||
|
for node in *nodes {
|
||||||
|
onstack += self.push_all_real_nodes(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Component(comp)) => {
|
||||||
|
let scope = comp.scope.get().unwrap();
|
||||||
|
onstack +=
|
||||||
|
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||||
|
RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.mutations.push(Mutation::PushRoot {
|
||||||
|
id: node.root_ids[idx].get(),
|
||||||
|
});
|
||||||
|
onstack += 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onstack
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize {
|
fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize {
|
||||||
@@ -747,68 +855,58 @@ impl<'b: 'static> VirtualDom {
|
|||||||
self.mutations.push(Mutation::InsertAfter { id, m })
|
self.mutations.push(Mutation::InsertAfter { id, m })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_first_element(&self, node: &VNode) -> ElementId {
|
fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
|
||||||
match &node.template.roots[0] {
|
match node.dynamic_root(0) {
|
||||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => node.root_ids[0].get(),
|
None => node.root_ids[0].get(),
|
||||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
Some(Text(t)) => t.id.get(),
|
||||||
match &node.dynamic_nodes[*id] {
|
Some(Fragment(t)) => self.find_first_element(&t[0]),
|
||||||
Text(t) => t.id.get(),
|
Some(Placeholder(t)) => t.get(),
|
||||||
Fragment(VFragment::NonEmpty(t)) => self.find_first_element(&t[0]),
|
Some(Component(comp)) => {
|
||||||
Fragment(VFragment::Empty(t)) => t.get(),
|
let scope = comp.scope.get().unwrap();
|
||||||
Component(comp) => {
|
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||||
let scope = comp.scope.get().unwrap();
|
RenderReturn::Sync(Ok(t)) => self.find_first_element(t),
|
||||||
match self.scopes[scope.0].root_node() {
|
_ => todo!("cannot handle nonstandard nodes"),
|
||||||
RenderReturn::Sync(Ok(t)) => self.find_first_element(t),
|
|
||||||
_ => todo!("cannot handle nonstandard nodes"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_last_element(&self, node: &VNode) -> ElementId {
|
fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
|
||||||
match node.template.roots.last().unwrap() {
|
match node.dynamic_root(node.template.roots.len() - 1) {
|
||||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => {
|
None => node.root_ids.last().unwrap().get(),
|
||||||
node.root_ids.last().unwrap().get()
|
Some(Text(t)) => t.id.get(),
|
||||||
}
|
Some(Fragment(t)) => self.find_last_element(t.last().unwrap()),
|
||||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
Some(Placeholder(t)) => t.get(),
|
||||||
match &node.dynamic_nodes[*id] {
|
Some(Component(comp)) => {
|
||||||
Text(t) => t.id.get(),
|
let scope = comp.scope.get().unwrap();
|
||||||
Fragment(VFragment::NonEmpty(t)) => self.find_last_element(t.last().unwrap()),
|
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||||
Fragment(VFragment::Empty(t)) => t.get(),
|
RenderReturn::Sync(Ok(t)) => self.find_last_element(t),
|
||||||
Component(comp) => {
|
_ => todo!("cannot handle nonstandard nodes"),
|
||||||
let scope = comp.scope.get().unwrap();
|
|
||||||
match self.scopes[scope.0].root_node() {
|
|
||||||
RenderReturn::Sync(Ok(t)) => self.find_last_element(t),
|
|
||||||
_ => todo!("cannot handle nonstandard nodes"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_template(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
|
fn replace(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
|
||||||
todo!("replacing should work!")
|
let first = self.find_first_element(left);
|
||||||
|
let id = self.replace_inner(left);
|
||||||
|
let created = self.create(right);
|
||||||
|
self.mutations.push(Mutation::ReplaceWith {
|
||||||
|
id: first,
|
||||||
|
m: created,
|
||||||
|
});
|
||||||
|
self.try_reclaim(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace(
|
fn replace_many(&mut self, left: &'b VNode<'b>, right: &'b [VNode<'b>]) {
|
||||||
&mut self,
|
let first = self.find_first_element(left);
|
||||||
left_template: &'b VNode<'b>,
|
let id = self.replace_inner(left);
|
||||||
right_template: &'b VNode<'b>,
|
let created = self.create_children(right);
|
||||||
left: &'b DynamicNode<'b>,
|
self.mutations.push(Mutation::ReplaceWith {
|
||||||
right: &'b DynamicNode<'b>,
|
id: first,
|
||||||
) {
|
m: created,
|
||||||
// Remove all but the first root
|
});
|
||||||
for root in &left_template.template.roots[1..] {
|
self.try_reclaim(id);
|
||||||
match root {
|
|
||||||
TemplateNode::Element { .. } => todo!(),
|
|
||||||
TemplateNode::Text(_) => todo!(),
|
|
||||||
TemplateNode::Dynamic(_) => todo!(),
|
|
||||||
TemplateNode::DynamicText(_) => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,7 +934,7 @@ fn matching_components<'a>(
|
|||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
(l.render_fn == r.render_fn).then(|| (l, r))
|
Some((l, r))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -847,6 +945,7 @@ fn matching_components<'a>(
|
|||||||
/// - for text - we can use SetTextContent
|
/// - for text - we can use SetTextContent
|
||||||
/// - for clearning children we can use RemoveChildren
|
/// - for clearning children we can use RemoveChildren
|
||||||
/// - for appending children we can use AppendChildren
|
/// - for appending children we can use AppendChildren
|
||||||
|
#[allow(dead_code)]
|
||||||
fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool {
|
fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool {
|
||||||
let path = node.template.node_paths[idx];
|
let path = node.template.node_paths[idx];
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
use std::{cell::RefCell, rc::Rc};
|
use std::cell::RefCell;
|
||||||
|
|
||||||
use crate::{ScopeId, ScopeState};
|
use crate::ScopeId;
|
||||||
|
|
||||||
pub struct ErrorContext {
|
/// A boundary that will capture any errors from child components
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct ErrorBoundary {
|
||||||
error: RefCell<Option<(anyhow::Error, ScopeId)>>,
|
error: RefCell<Option<(anyhow::Error, ScopeId)>>,
|
||||||
|
id: ScopeId,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Catch all errors from the children and bubble them up to this component
|
impl ErrorBoundary {
|
||||||
///
|
pub fn new(id: ScopeId) -> Self {
|
||||||
/// Returns the error and scope that caused the error
|
Self {
|
||||||
pub fn use_catch_error(cx: &ScopeState) -> Option<&(anyhow::Error, ScopeId)> {
|
error: RefCell::new(None),
|
||||||
let err_ctx = use_error_context(cx);
|
id,
|
||||||
|
}
|
||||||
let out = cx.use_hook(|| None);
|
|
||||||
|
|
||||||
if let Some(error) = err_ctx.error.take() {
|
|
||||||
*out = Some(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new error context at this component.
|
|
||||||
///
|
|
||||||
/// This component will start to catch any errors that occur in its children.
|
|
||||||
pub fn use_error_context(cx: &ScopeState) -> &ErrorContext {
|
|
||||||
cx.use_hook(|| cx.provide_context(Rc::new(ErrorContext { error: None.into() })))
|
|
||||||
}
|
}
|
||||||
+87
-83
@@ -1,109 +1,110 @@
|
|||||||
use bumpalo::boxed::Box as BumpBox;
|
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
fmt::Debug,
|
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct UiEvent<T: 'static + ?Sized> {
|
/// A wrapper around some generic data that handles the event's state
|
||||||
pub(crate) bubbles: Rc<Cell<bool>>,
|
///
|
||||||
|
///
|
||||||
|
/// Prevent this event from continuing to bubble up the tree to parent elements.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust, ignore
|
||||||
|
/// rsx! {
|
||||||
|
/// button {
|
||||||
|
/// onclick: move |evt: Event<MouseData>| {
|
||||||
|
/// evt.cancel_bubble();
|
||||||
|
///
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Event<T: 'static + ?Sized> {
|
||||||
pub(crate) data: Rc<T>,
|
pub(crate) data: Rc<T>,
|
||||||
|
pub(crate) propogates: Rc<Cell<bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiEvent<dyn Any> {
|
impl<T> Event<T> {
|
||||||
pub fn downcast<T: 'static + Sized>(self) -> Option<UiEvent<T>> {
|
/// Prevent this event from continuing to bubble up the tree to parent elements.
|
||||||
Some(UiEvent {
|
///
|
||||||
bubbles: self.bubbles,
|
/// # Example
|
||||||
data: self.data.downcast().ok()?,
|
///
|
||||||
})
|
/// ```rust, ignore
|
||||||
|
/// rsx! {
|
||||||
|
/// button {
|
||||||
|
/// onclick: move |evt: Event<MouseData>| {
|
||||||
|
/// evt.cancel_bubble();
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[deprecated = "use stop_propogation instead"]
|
||||||
|
pub fn cancel_bubble(&self) {
|
||||||
|
self.propogates.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prevent this event from continuing to bubble up the tree to parent elements.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust, ignore
|
||||||
|
/// rsx! {
|
||||||
|
/// button {
|
||||||
|
/// onclick: move |evt: Event<MouseData>| {
|
||||||
|
/// evt.cancel_bubble();
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn stop_propogation(&self) {
|
||||||
|
self.propogates.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the inner data from this event
|
||||||
|
///
|
||||||
|
/// ```rust, ignore
|
||||||
|
/// rsx! {
|
||||||
|
/// button {
|
||||||
|
/// onclick: move |evt: Event<MouseData>| {
|
||||||
|
/// let data = evt.inner.clone();
|
||||||
|
/// cx.spawn(async move {
|
||||||
|
/// println!("{:?}", data);
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn inner(&self) -> &Rc<T> {
|
||||||
|
&self.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<T: ?Sized> Clone for UiEvent<T> {
|
|
||||||
|
impl<T: ?Sized> Clone for Event<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
bubbles: self.bubbles.clone(),
|
propogates: self.propogates.clone(),
|
||||||
data: self.data.clone(),
|
data: self.data.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<T> UiEvent<T> {
|
|
||||||
pub fn cancel_bubble(&self) {
|
|
||||||
self.bubbles.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> std::ops::Deref for UiEvent<T> {
|
impl<T> std::ops::Deref for Event<T> {
|
||||||
type Target = Rc<T>;
|
type Target = Rc<T>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.data
|
&self.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Debug> std::fmt::Debug for UiEvent<T> {
|
impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("UiEvent")
|
f.debug_struct("UiEvent")
|
||||||
.field("bubble_state", &self.bubbles)
|
.field("bubble_state", &self.propogates)
|
||||||
.field("data", &self.data)
|
.field("data", &self.data)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Priority of Event Triggers.
|
|
||||||
///
|
|
||||||
/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus
|
|
||||||
/// won't be afraid to pause work or flush changes to the Real Dom. This is called "cooperative scheduling". Some Renderers
|
|
||||||
/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
|
|
||||||
///
|
|
||||||
/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
|
|
||||||
///
|
|
||||||
/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now,
|
|
||||||
/// we keep it simple, and just use a 3-tier priority system.
|
|
||||||
///
|
|
||||||
/// - `NoPriority` = 0
|
|
||||||
/// - `LowPriority` = 1
|
|
||||||
/// - `NormalPriority` = 2
|
|
||||||
/// - `UserBlocking` = 3
|
|
||||||
/// - `HighPriority` = 4
|
|
||||||
/// - `ImmediatePriority` = 5
|
|
||||||
///
|
|
||||||
/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will.
|
|
||||||
/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be
|
|
||||||
/// flushed before proceeding. Multiple discrete events is highly unlikely, though.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
|
|
||||||
pub enum EventPriority {
|
|
||||||
/// Work that must be completed during the EventHandler phase.
|
|
||||||
///
|
|
||||||
/// Currently this is reserved for controlled inputs.
|
|
||||||
Immediate = 3,
|
|
||||||
|
|
||||||
/// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work.
|
|
||||||
///
|
|
||||||
/// This is typically reserved for things like user interaction.
|
|
||||||
///
|
|
||||||
/// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate).
|
|
||||||
High = 2,
|
|
||||||
|
|
||||||
/// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
|
|
||||||
/// than "High Priority" events and will take precedence over low priority events.
|
|
||||||
///
|
|
||||||
/// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
|
|
||||||
///
|
|
||||||
/// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc).
|
|
||||||
Medium = 1,
|
|
||||||
|
|
||||||
/// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be
|
|
||||||
/// advanced to the front of the work queue until completed.
|
|
||||||
///
|
|
||||||
/// The primary user of Low Priority work is the asynchronous work system (Suspense).
|
|
||||||
///
|
|
||||||
/// This is considered "idle" work or "background" work.
|
|
||||||
Low = 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>;
|
|
||||||
|
|
||||||
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
|
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
|
||||||
///
|
///
|
||||||
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
|
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
|
||||||
@@ -112,9 +113,8 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>;
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust, ignore
|
/// ```rust, ignore
|
||||||
///
|
|
||||||
/// rsx!{
|
/// rsx!{
|
||||||
/// MyComponent { onclick: move |evt| log::info!("clicked"), }
|
/// MyComponent { onclick: move |evt| log::info!("clicked") }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[derive(Props)]
|
/// #[derive(Props)]
|
||||||
@@ -132,21 +132,23 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>;
|
|||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
pub struct EventHandler<'bump, T = ()> {
|
pub struct EventHandler<'bump, T = ()> {
|
||||||
/// The (optional) callback that the user specified
|
pub(super) callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
||||||
/// Uses a `RefCell` to allow for interior mutability, and FnMut closures.
|
|
||||||
pub callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T> Default for EventHandler<'a, T> {
|
impl<T> Default for EventHandler<'_, T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
callback: RefCell::new(None),
|
callback: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExternalListenerCallback<'bump, T> = bumpalo::boxed::Box<'bump, dyn FnMut(T) + 'bump>;
|
||||||
|
|
||||||
impl<T> EventHandler<'_, T> {
|
impl<T> EventHandler<'_, T> {
|
||||||
/// Call this event handler with the appropriate event type
|
/// Call this event handler with the appropriate event type
|
||||||
|
///
|
||||||
|
/// This borrows the event using a RefCell. Recursively calling a listener will cause a panic.
|
||||||
pub fn call(&self, event: T) {
|
pub fn call(&self, event: T) {
|
||||||
if let Some(callback) = self.callback.borrow_mut().as_mut() {
|
if let Some(callback) = self.callback.borrow_mut().as_mut() {
|
||||||
callback(event);
|
callback(event);
|
||||||
@@ -154,6 +156,8 @@ impl<T> EventHandler<'_, T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Forcibly drop the internal handler callback, releasing memory
|
/// Forcibly drop the internal handler callback, releasing memory
|
||||||
|
///
|
||||||
|
/// This will force any future calls to "call" to not doing anything
|
||||||
pub fn release(&self) {
|
pub fn release(&self) {
|
||||||
self.callback.replace(None);
|
self.callback.replace(None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,289 +0,0 @@
|
|||||||
use std::{
|
|
||||||
cell::{Cell, RefCell},
|
|
||||||
fmt::Arguments,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bumpalo::boxed::Box as BumpBox;
|
|
||||||
use bumpalo::Bump;
|
|
||||||
use std::future::Future;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
any_props::{AnyProps, VProps},
|
|
||||||
arena::ElementId,
|
|
||||||
innerlude::{DynamicNode, EventHandler, VComponent, VFragment, VText},
|
|
||||||
Attribute, AttributeValue, Element, LazyNodes, Properties, Scope, ScopeState, VNode,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl ScopeState {
|
|
||||||
/// Create some text that's allocated along with the other vnodes
|
|
||||||
pub fn text<'a>(&'a self, args: Arguments) -> DynamicNode<'a> {
|
|
||||||
let (text, _) = self.raw_text(args);
|
|
||||||
DynamicNode::Text(VText {
|
|
||||||
id: Cell::new(ElementId(0)),
|
|
||||||
value: text,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn raw_text_inline<'a>(&'a self, args: Arguments) -> &'a str {
|
|
||||||
self.raw_text(args).0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn raw_text<'a>(&'a self, args: Arguments) -> (&'a str, bool) {
|
|
||||||
match args.as_str() {
|
|
||||||
Some(static_str) => (static_str, true),
|
|
||||||
None => {
|
|
||||||
use bumpalo::core_alloc::fmt::Write;
|
|
||||||
let mut str_buf = bumpalo::collections::String::new_in(self.bump());
|
|
||||||
str_buf.write_fmt(args).unwrap();
|
|
||||||
(str_buf.into_bump_str(), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fragment_from_iter<'a, 'c, I>(
|
|
||||||
&'a self,
|
|
||||||
node_iter: impl IntoDynNode<'a, I> + 'c,
|
|
||||||
) -> DynamicNode {
|
|
||||||
node_iter.into_vnode(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new [`Attribute`]
|
|
||||||
pub fn attr<'a>(
|
|
||||||
&'a self,
|
|
||||||
name: &'static str,
|
|
||||||
value: impl IntoAttributeValue<'a>,
|
|
||||||
namespace: Option<&'static str>,
|
|
||||||
volatile: bool,
|
|
||||||
) -> Attribute<'a> {
|
|
||||||
Attribute {
|
|
||||||
name,
|
|
||||||
namespace,
|
|
||||||
volatile,
|
|
||||||
value: value.into_value(self.bump()),
|
|
||||||
mounted_element: Cell::new(ElementId(0)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new [`VNode::Component`]
|
|
||||||
pub fn component<'a, P, A, F: ComponentReturn<'a, A>>(
|
|
||||||
&'a self,
|
|
||||||
component: fn(Scope<'a, P>) -> F,
|
|
||||||
props: P,
|
|
||||||
fn_name: &'static str,
|
|
||||||
) -> DynamicNode<'a>
|
|
||||||
where
|
|
||||||
P: Properties + 'a,
|
|
||||||
{
|
|
||||||
let as_component = component;
|
|
||||||
let vcomp = VProps::new(as_component, P::memoize, props);
|
|
||||||
let as_dyn: Box<dyn AnyProps<'a>> = Box::new(vcomp);
|
|
||||||
let extended: Box<dyn AnyProps> = unsafe { std::mem::transmute(as_dyn) };
|
|
||||||
|
|
||||||
// let as_dyn: &dyn AnyProps = self.bump().alloc(vcomp);
|
|
||||||
// todo: clean up borrowed props
|
|
||||||
// if !P::IS_STATIC {
|
|
||||||
// let vcomp = &*vcomp;
|
|
||||||
// let vcomp = unsafe { std::mem::transmute(vcomp) };
|
|
||||||
// self.scope.items.borrow_mut().borrowed_props.push(vcomp);
|
|
||||||
// }
|
|
||||||
|
|
||||||
DynamicNode::Component(VComponent {
|
|
||||||
name: fn_name,
|
|
||||||
render_fn: component as *const (),
|
|
||||||
static_props: P::IS_STATIC,
|
|
||||||
props: Cell::new(Some(extended)),
|
|
||||||
scope: Cell::new(None),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new [`EventHandler`] from an [`FnMut`]
|
|
||||||
pub fn event_handler<'a, T>(&'a self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> {
|
|
||||||
let handler: &mut dyn FnMut(T) = self.bump().alloc(f);
|
|
||||||
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
|
|
||||||
let callback = RefCell::new(Some(caller));
|
|
||||||
EventHandler { callback }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ComponentReturn<'a, A = ()> {
|
|
||||||
fn as_return(self, cx: &'a ScopeState) -> RenderReturn<'a>;
|
|
||||||
}
|
|
||||||
impl<'a> ComponentReturn<'a> for Element<'a> {
|
|
||||||
fn as_return(self, _cx: &ScopeState) -> RenderReturn<'a> {
|
|
||||||
RenderReturn::Sync(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AsyncMarker;
|
|
||||||
impl<'a, F> ComponentReturn<'a, AsyncMarker> for F
|
|
||||||
where
|
|
||||||
F: Future<Output = Element<'a>> + 'a,
|
|
||||||
{
|
|
||||||
fn as_return(self, cx: &'a ScopeState) -> RenderReturn<'a> {
|
|
||||||
let f: &mut dyn Future<Output = Element<'a>> = cx.bump().alloc(self);
|
|
||||||
let boxed = unsafe { BumpBox::from_raw(f) };
|
|
||||||
let pined: BumpBox<_> = boxed.into();
|
|
||||||
RenderReturn::Async(pined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum RenderReturn<'a> {
|
|
||||||
/// A currently-available element
|
|
||||||
Sync(Element<'a>),
|
|
||||||
|
|
||||||
/// An ongoing future that will resolve to a [`Element`]
|
|
||||||
Async(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> RenderReturn<'a> {
|
|
||||||
pub(crate) unsafe fn extend_lifetime_ref<'c>(&self) -> &'c RenderReturn<'c> {
|
|
||||||
unsafe { std::mem::transmute(self) }
|
|
||||||
}
|
|
||||||
pub(crate) unsafe fn extend_lifetime<'c>(self) -> RenderReturn<'c> {
|
|
||||||
unsafe { std::mem::transmute(self) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoDynNode<'a, A = ()> {
|
|
||||||
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> IntoDynNode<'a> for () {
|
|
||||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
|
||||||
DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a, 'b> IntoDynNode<'a> for VNode<'a> {
|
|
||||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
|
||||||
DynamicNode::Fragment(VFragment::NonEmpty(_cx.bump().alloc([self])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b, T: IntoDynNode<'a>> IntoDynNode<'a> for Option<T> {
|
|
||||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
|
||||||
match self {
|
|
||||||
Some(val) => val.into_vnode(_cx),
|
|
||||||
None => DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoDynNode<'a> for &Element<'a> {
|
|
||||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
|
||||||
match self.as_ref() {
|
|
||||||
Ok(val) => val.clone().into_vnode(_cx),
|
|
||||||
_ => DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> {
|
|
||||||
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> {
|
|
||||||
DynamicNode::Fragment(VFragment::NonEmpty(cx.bump().alloc([self.call(cx)])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'b> IntoDynNode<'_> for &'b str {
|
|
||||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
|
||||||
cx.text(format_args!("{}", self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoDynNode<'_> for String {
|
|
||||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
|
||||||
cx.text(format_args!("{}", self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'b> IntoDynNode<'b> for Arguments<'_> {
|
|
||||||
fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> {
|
|
||||||
cx.text(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> IntoDynNode<'a> for &VNode<'a> {
|
|
||||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
|
||||||
todo!()
|
|
||||||
// VNode {
|
|
||||||
// node_id: self.node_id.clone(),
|
|
||||||
// parent: self.parent,
|
|
||||||
// template: self.template,
|
|
||||||
// root_ids: self.root_ids,
|
|
||||||
// key: self.key,
|
|
||||||
// dynamic_nodes: self.dynamic_nodes,
|
|
||||||
// dynamic_attrs: self.dynamic_attrs,
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoTemplate<'a> {
|
|
||||||
fn into_template(self, _cx: &'a ScopeState) -> VNode<'a>;
|
|
||||||
}
|
|
||||||
impl<'a, 'b> IntoTemplate<'a> for VNode<'a> {
|
|
||||||
fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> {
|
|
||||||
fn into_template(self, cx: &'a ScopeState) -> VNode<'a> {
|
|
||||||
self.call(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that we're using the E as a generic but this is never crafted anyways.
|
|
||||||
pub struct FromNodeIterator;
|
|
||||||
impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T
|
|
||||||
where
|
|
||||||
T: Iterator<Item = I>,
|
|
||||||
I: IntoTemplate<'a>,
|
|
||||||
{
|
|
||||||
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> {
|
|
||||||
let mut nodes = bumpalo::collections::Vec::new_in(cx.bump());
|
|
||||||
|
|
||||||
for node in self {
|
|
||||||
nodes.push(node.into_template(cx));
|
|
||||||
}
|
|
||||||
|
|
||||||
let children = nodes.into_bump_slice();
|
|
||||||
|
|
||||||
match children.len() {
|
|
||||||
0 => DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))),
|
|
||||||
_ => DynamicNode::Fragment(VFragment::NonEmpty(children)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A value that can be converted into an attribute value
|
|
||||||
pub trait IntoAttributeValue<'a> {
|
|
||||||
/// Convert into an attribute value
|
|
||||||
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoAttributeValue<'a> for &'a str {
|
|
||||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
|
||||||
AttributeValue::Text(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> IntoAttributeValue<'a> for f32 {
|
|
||||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
|
||||||
AttributeValue::Float(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> IntoAttributeValue<'a> for i32 {
|
|
||||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
|
||||||
AttributeValue::Int(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> IntoAttributeValue<'a> for bool {
|
|
||||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
|
||||||
AttributeValue::Bool(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> IntoAttributeValue<'a> for Arguments<'_> {
|
|
||||||
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> {
|
|
||||||
use bumpalo::core_alloc::fmt::Write;
|
|
||||||
let mut str_buf = bumpalo::collections::String::new_in(bump);
|
|
||||||
str_buf.write_fmt(self).unwrap();
|
|
||||||
AttributeValue::Text(str_buf.into_bump_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,9 +27,8 @@ use crate::innerlude::*;
|
|||||||
/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
|
/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
|
||||||
#[allow(non_upper_case_globals, non_snake_case)]
|
#[allow(non_upper_case_globals, non_snake_case)]
|
||||||
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
|
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
|
||||||
let children = cx.props.0.as_ref().unwrap();
|
let children = cx.props.0.as_ref().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
Ok(VNode {
|
Ok(VNode {
|
||||||
node_id: children.node_id.clone(),
|
|
||||||
key: children.key.clone(),
|
key: children.key.clone(),
|
||||||
parent: children.parent.clone(),
|
parent: children.parent.clone(),
|
||||||
template: children.template,
|
template: children.template,
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
use futures_channel::mpsc::UnboundedSender;
|
|
||||||
use slab::Slab;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
|
||||||
|
|
||||||
use crate::innerlude::ScopeId;
|
|
||||||
/// The type of message that can be sent to the scheduler.
|
|
||||||
///
|
|
||||||
/// These messages control how the scheduler will process updates to the UI.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum SchedulerMsg {
|
|
||||||
/// Events from athe Renderer
|
|
||||||
Event,
|
|
||||||
|
|
||||||
/// Immediate updates from Components that mark them as dirty
|
|
||||||
Immediate(ScopeId),
|
|
||||||
|
|
||||||
/// Mark all components as dirty and update them
|
|
||||||
DirtyAll,
|
|
||||||
|
|
||||||
/// New tasks from components that should be polled when the next poll is ready
|
|
||||||
NewTask(ScopeId),
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo extract this so spawning doesn't require refcell and rc doesnt need to be tracked
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct FutureQueue {
|
|
||||||
pub sender: UnboundedSender<SchedulerMsg>,
|
|
||||||
pub queue: RefCell<Slab<Arc<dyn Future<Output = ()>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FutureQueue {
|
|
||||||
pub fn new(sender: UnboundedSender<SchedulerMsg>) -> Self {
|
|
||||||
Self {
|
|
||||||
sender,
|
|
||||||
queue: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn(&self, scope: ScopeId, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
|
||||||
let id = self.queue.borrow_mut().insert(Arc::new(fut));
|
|
||||||
|
|
||||||
TaskId { id, scope }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&self, id: TaskId) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A task's unique identifier.
|
|
||||||
///
|
|
||||||
/// `TaskId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`TaskID`]s will never be reused
|
|
||||||
/// once a Task has been completed.
|
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
|
||||||
pub struct TaskId {
|
|
||||||
/// The global ID of the task
|
|
||||||
pub id: usize,
|
|
||||||
|
|
||||||
/// The original scope that this task was scheduled in
|
|
||||||
pub scope: ScopeId,
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
use crate::{nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode, Mutations};
|
|
||||||
|
|
||||||
impl<'b> VirtualDom {
|
|
||||||
pub fn drop_scope(&mut self, id: ScopeId) {
|
|
||||||
// let scope = self.scopes.get(id.0).unwrap();
|
|
||||||
|
|
||||||
// let root = scope.root_node();
|
|
||||||
// let root = unsafe { std::mem::transmute(root) };
|
|
||||||
|
|
||||||
// self.drop_template(root, false);
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn drop_template(
|
|
||||||
&mut self,
|
|
||||||
mutations: &mut Mutations,
|
|
||||||
template: &'b VNode<'b>,
|
|
||||||
gen_roots: bool,
|
|
||||||
) {
|
|
||||||
// for node in template.dynamic_nodes.iter() {
|
|
||||||
// match node {
|
|
||||||
// DynamicNode::Text { id, .. } => {}
|
|
||||||
|
|
||||||
// DynamicNode::Component { .. } => {
|
|
||||||
// todo!()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DynamicNode::Fragment { inner, nodes } => {}
|
|
||||||
// DynamicNode::Placeholder(_) => todo!(),
|
|
||||||
// _ => todo!(),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
//! This module provides support for a type called `LazyNodes` which is a micro-heap located on the stack to make calls
|
//! This module provides support for a type called `LazyNodes` which is a micro-heap located on the stack to make calls
|
||||||
//! to `rsx!` more efficient.
|
//! to `rsx!` more efficient.
|
||||||
//!
|
//!
|
||||||
//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`NodeFactory`] closures.
|
//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`ScopeState`] closures.
|
||||||
//!
|
//!
|
||||||
//! This can be done either through boxing directly, or by using dynamic-sized-types and a custom allocator. In our case,
|
//! This can be done either through boxing directly, or by using dynamic-sized-types and a custom allocator. In our case,
|
||||||
//! we build a tiny alloactor in the stack and allocate the closure into that.
|
//! we build a tiny alloactor in the stack and allocate the closure into that.
|
||||||
@@ -17,7 +17,7 @@ use std::mem;
|
|||||||
/// A concrete type provider for closures that build [`VNode`] structures.
|
/// A concrete type provider for closures that build [`VNode`] structures.
|
||||||
///
|
///
|
||||||
/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over
|
/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over
|
||||||
/// closures, but if we wrap the closure in a concrete type, we can maintain separate implementations of [`IntoVNode`].
|
/// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching.
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// ```rust, ignore
|
/// ```rust, ignore
|
||||||
@@ -27,13 +27,11 @@ pub struct LazyNodes<'a, 'b> {
|
|||||||
inner: StackNodeStorage<'a, 'b>,
|
inner: StackNodeStorage<'a, 'b>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type NodeFactory<'a> = &'a ScopeState;
|
|
||||||
|
|
||||||
type StackHeapSize = [usize; 16];
|
type StackHeapSize = [usize; 16];
|
||||||
|
|
||||||
enum StackNodeStorage<'a, 'b> {
|
enum StackNodeStorage<'a, 'b> {
|
||||||
Stack(LazyStack),
|
Stack(LazyStack),
|
||||||
Heap(Box<dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b>),
|
Heap(Box<dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> LazyNodes<'a, 'b> {
|
impl<'a, 'b> LazyNodes<'a, 'b> {
|
||||||
@@ -42,11 +40,11 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||||||
/// If the closure cannot fit into the stack allocation (16 bytes), then it
|
/// If the closure cannot fit into the stack allocation (16 bytes), then it
|
||||||
/// is placed on the heap. Most closures will fit into the stack, and is
|
/// is placed on the heap. Most closures will fit into the stack, and is
|
||||||
/// the most optimal way to use the creation function.
|
/// the most optimal way to use the creation function.
|
||||||
pub fn new(val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self {
|
pub fn new(val: impl FnOnce(&'a ScopeState) -> VNode<'a> + 'b) -> Self {
|
||||||
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
|
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
|
||||||
let mut slot = Some(val);
|
let mut slot = Some(val);
|
||||||
|
|
||||||
let val = move |fac: Option<NodeFactory<'a>>| {
|
let val = move |fac: Option<&'a ScopeState>| {
|
||||||
fac.map(
|
fac.map(
|
||||||
slot.take()
|
slot.take()
|
||||||
.expect("LazyNodes closure to be called only once"),
|
.expect("LazyNodes closure to be called only once"),
|
||||||
@@ -67,13 +65,13 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||||||
/// Create a new [`LazyNodes`] closure, but force it onto the heap.
|
/// Create a new [`LazyNodes`] closure, but force it onto the heap.
|
||||||
pub fn new_boxed<F>(inner: F) -> Self
|
pub fn new_boxed<F>(inner: F) -> Self
|
||||||
where
|
where
|
||||||
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
|
F: FnOnce(&'a ScopeState) -> VNode<'a> + 'b,
|
||||||
{
|
{
|
||||||
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
|
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
|
||||||
let mut slot = Some(inner);
|
let mut slot = Some(inner);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
inner: StackNodeStorage::Heap(Box::new(move |fac: Option<NodeFactory<'a>>| {
|
inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| {
|
||||||
fac.map(
|
fac.map(
|
||||||
slot.take()
|
slot.take()
|
||||||
.expect("LazyNodes closure to be called only once"),
|
.expect("LazyNodes closure to be called only once"),
|
||||||
@@ -84,9 +82,9 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||||||
|
|
||||||
unsafe fn new_inner<F>(val: F) -> Self
|
unsafe fn new_inner<F>(val: F) -> Self
|
||||||
where
|
where
|
||||||
F: FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b,
|
F: FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b,
|
||||||
{
|
{
|
||||||
let mut ptr: *const _ = &val as &dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>>;
|
let mut ptr: *const _ = &val as &dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>>;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ptr as *const u8, &val as *const _ as *const u8,
|
ptr as *const u8, &val as *const _ as *const u8,
|
||||||
@@ -162,12 +160,10 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||||||
/// ```rust, ignore
|
/// ```rust, ignore
|
||||||
/// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
|
/// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
|
||||||
///
|
///
|
||||||
/// let fac = NodeFactory::new(&cx);
|
|
||||||
///
|
|
||||||
/// let node = f.call(cac);
|
/// let node = f.call(cac);
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> {
|
pub fn call(self, f: &'a ScopeState) -> VNode<'a> {
|
||||||
match self.inner {
|
match self.inner {
|
||||||
StackNodeStorage::Heap(mut lazy) => {
|
StackNodeStorage::Heap(mut lazy) => {
|
||||||
lazy(Some(f)).expect("Closure should not be called twice")
|
lazy(Some(f)).expect("Closure should not be called twice")
|
||||||
@@ -184,18 +180,18 @@ struct LazyStack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LazyStack {
|
impl LazyStack {
|
||||||
fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> {
|
fn call<'a>(&mut self, f: &'a ScopeState) -> VNode<'a> {
|
||||||
let LazyStack { buf, .. } = self;
|
let LazyStack { buf, .. } = self;
|
||||||
let data = buf.as_ref();
|
let data = buf.as_ref();
|
||||||
|
|
||||||
let info_size =
|
let info_size =
|
||||||
mem::size_of::<*mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>>>()
|
mem::size_of::<*mut dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>>>()
|
||||||
/ mem::size_of::<usize>()
|
/ mem::size_of::<usize>()
|
||||||
- 1;
|
- 1;
|
||||||
|
|
||||||
let info_ofs = data.len() - info_size;
|
let info_ofs = data.len() - info_size;
|
||||||
|
|
||||||
let g: *mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> =
|
let g: *mut dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> =
|
||||||
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
|
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
|
||||||
|
|
||||||
self.dropped = true;
|
self.dropped = true;
|
||||||
@@ -210,14 +206,14 @@ impl Drop for LazyStack {
|
|||||||
let LazyStack { buf, .. } = self;
|
let LazyStack { buf, .. } = self;
|
||||||
let data = buf.as_ref();
|
let data = buf.as_ref();
|
||||||
|
|
||||||
let info_size = mem::size_of::<
|
let info_size =
|
||||||
*mut dyn FnMut(Option<NodeFactory<'_>>) -> Option<VNode<'_>>,
|
mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>>>()
|
||||||
>() / mem::size_of::<usize>()
|
/ mem::size_of::<usize>()
|
||||||
- 1;
|
- 1;
|
||||||
|
|
||||||
let info_ofs = data.len() - info_size;
|
let info_ofs = data.len() - info_size;
|
||||||
|
|
||||||
let g: *mut dyn FnMut(Option<NodeFactory<'_>>) -> Option<VNode<'_>> =
|
let g: *mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>> =
|
||||||
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
|
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
|
||||||
|
|
||||||
self.dropped = true;
|
self.dropped = true;
|
||||||
@@ -252,73 +248,3 @@ unsafe fn make_fat_ptr<T: ?Sized>(data_ptr: usize, meta_vals: &[usize]) -> *mut
|
|||||||
fn round_to_words(len: usize) -> usize {
|
fn round_to_words(len: usize) -> usize {
|
||||||
(len + mem::size_of::<usize>() - 1) / mem::size_of::<usize>()
|
(len + mem::size_of::<usize>() - 1) / mem::size_of::<usize>()
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[cfg(test)]
|
|
||||||
// mod tests {
|
|
||||||
// use super::*;
|
|
||||||
// use crate::innerlude::{Element, Scope, VirtualDom};
|
|
||||||
|
|
||||||
// #[test]
|
|
||||||
// fn it_works() {
|
|
||||||
// fn app(cx: Scope<()>) -> Element {
|
|
||||||
// cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let mut dom = VirtualDom::new(app);
|
|
||||||
// dom.rebuild();
|
|
||||||
|
|
||||||
// let g = dom.base_scope().root_node();
|
|
||||||
// dbg!(g);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[test]
|
|
||||||
// fn it_drops() {
|
|
||||||
// use std::rc::Rc;
|
|
||||||
|
|
||||||
// struct AppProps {
|
|
||||||
// inner: Rc<i32>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn app(cx: Scope<AppProps>) -> Element {
|
|
||||||
// struct DropInner {
|
|
||||||
// id: i32,
|
|
||||||
// }
|
|
||||||
// impl Drop for DropInner {
|
|
||||||
// fn drop(&mut self) {
|
|
||||||
// eprintln!("dropping inner");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let caller = {
|
|
||||||
// let it = (0..10).map(|i| {
|
|
||||||
// let val = cx.props.inner.clone();
|
|
||||||
// LazyNodes::new(move |f| {
|
|
||||||
// eprintln!("hell closure");
|
|
||||||
// let inner = DropInner { id: i };
|
|
||||||
// f.text(format_args!("hello world {:?}, {:?}", inner.id, val))
|
|
||||||
// })
|
|
||||||
// });
|
|
||||||
|
|
||||||
// LazyNodes::new(|f| {
|
|
||||||
// eprintln!("main closure");
|
|
||||||
// f.fragment_from_iter(it)
|
|
||||||
// })
|
|
||||||
// };
|
|
||||||
|
|
||||||
// cx.render(caller)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let inner = Rc::new(0);
|
|
||||||
// let mut dom = VirtualDom::new_with_props(
|
|
||||||
// app,
|
|
||||||
// AppProps {
|
|
||||||
// inner: inner.clone(),
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// dom.rebuild();
|
|
||||||
|
|
||||||
// drop(dom);
|
|
||||||
|
|
||||||
// assert_eq!(Rc::strong_count(&inner), 1);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
+12
-65
@@ -1,3 +1,6 @@
|
|||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
mod any_props;
|
mod any_props;
|
||||||
mod arena;
|
mod arena;
|
||||||
mod bump_frame;
|
mod bump_frame;
|
||||||
@@ -6,9 +9,7 @@ mod diff;
|
|||||||
mod dirty_scope;
|
mod dirty_scope;
|
||||||
mod error_boundary;
|
mod error_boundary;
|
||||||
mod events;
|
mod events;
|
||||||
mod factory;
|
|
||||||
mod fragment;
|
mod fragment;
|
||||||
mod garbage;
|
|
||||||
mod lazynodes;
|
mod lazynodes;
|
||||||
mod mutations;
|
mod mutations;
|
||||||
mod nodes;
|
mod nodes;
|
||||||
@@ -21,10 +22,12 @@ mod virtual_dom;
|
|||||||
pub(crate) mod innerlude {
|
pub(crate) mod innerlude {
|
||||||
pub use crate::arena::*;
|
pub use crate::arena::*;
|
||||||
pub use crate::dirty_scope::*;
|
pub use crate::dirty_scope::*;
|
||||||
|
pub use crate::error_boundary::*;
|
||||||
pub use crate::events::*;
|
pub use crate::events::*;
|
||||||
pub use crate::fragment::*;
|
pub use crate::fragment::*;
|
||||||
pub use crate::lazynodes::*;
|
pub use crate::lazynodes::*;
|
||||||
pub use crate::mutations::*;
|
pub use crate::mutations::*;
|
||||||
|
pub use crate::nodes::RenderReturn;
|
||||||
pub use crate::nodes::*;
|
pub use crate::nodes::*;
|
||||||
pub use crate::properties::*;
|
pub use crate::properties::*;
|
||||||
pub use crate::scheduler::*;
|
pub use crate::scheduler::*;
|
||||||
@@ -64,45 +67,13 @@ pub(crate) mod innerlude {
|
|||||||
/// )
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
pub type Component<P = ()> = fn(Scope<P>) -> Element;
|
pub type Component<P = ()> = fn(Scope<P>) -> Element;
|
||||||
|
|
||||||
/// A list of attributes
|
|
||||||
pub type Attributes<'a> = Option<&'a [Attribute<'a>]>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use crate::innerlude::{
|
pub use crate::innerlude::{
|
||||||
// AnyAttributeValue, AnyEvent,
|
fc_to_builder, Attribute, AttributeValue, Component, DynamicNode, Element, ElementId, Event,
|
||||||
fc_to_builder,
|
Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope,
|
||||||
Attribute,
|
ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, TemplateAttribute,
|
||||||
AttributeValue,
|
TemplateNode, VComponent, VNode, VText, VirtualDom,
|
||||||
Attributes,
|
|
||||||
Component,
|
|
||||||
DynamicNode,
|
|
||||||
Element,
|
|
||||||
ElementId,
|
|
||||||
ElementRef,
|
|
||||||
EventPriority,
|
|
||||||
Fragment,
|
|
||||||
LazyNodes,
|
|
||||||
Mutation,
|
|
||||||
Mutations,
|
|
||||||
NodeFactory,
|
|
||||||
Properties,
|
|
||||||
Scope,
|
|
||||||
ScopeId,
|
|
||||||
ScopeState,
|
|
||||||
Scoped,
|
|
||||||
SuspenseBoundary,
|
|
||||||
SuspenseContext,
|
|
||||||
TaskId,
|
|
||||||
Template,
|
|
||||||
TemplateAttribute,
|
|
||||||
TemplateNode,
|
|
||||||
UiEvent,
|
|
||||||
VComponent,
|
|
||||||
VFragment,
|
|
||||||
VNode,
|
|
||||||
VText,
|
|
||||||
VirtualDom,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The purpose of this module is to alleviate imports of many common types
|
/// The purpose of this module is to alleviate imports of many common types
|
||||||
@@ -110,9 +81,9 @@ pub use crate::innerlude::{
|
|||||||
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::innerlude::{
|
pub use crate::innerlude::{
|
||||||
fc_to_builder, Element, EventHandler, EventPriority, Fragment, LazyNodes, NodeFactory,
|
fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope,
|
||||||
Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute,
|
ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode,
|
||||||
TemplateNode, UiEvent, VNode, VirtualDom,
|
VirtualDom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,28 +91,4 @@ pub mod exports {
|
|||||||
//! Important dependencies that are used by the rest of the library
|
//! Important dependencies that are used by the rest of the library
|
||||||
//! Feel free to just add the dependencies in your own Crates.toml
|
//! Feel free to just add the dependencies in your own Crates.toml
|
||||||
pub use bumpalo;
|
pub use bumpalo;
|
||||||
pub use futures_channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
/// A helper macro for using hooks in async environements.
|
|
||||||
///
|
|
||||||
/// # Usage
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
/// let (data) = use_ref(&cx, || {});
|
|
||||||
///
|
|
||||||
/// let handle_thing = move |_| {
|
|
||||||
/// to_owned![data]
|
|
||||||
/// cx.spawn(async move {
|
|
||||||
/// // do stuff
|
|
||||||
/// });
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
macro_rules! to_owned {
|
|
||||||
($($es:ident),+) => {$(
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
let mut $es = $es.to_owned();
|
|
||||||
)*}
|
|
||||||
}
|
}
|
||||||
+153
-85
@@ -1,59 +1,60 @@
|
|||||||
use crate::{arena::ElementId, ScopeId};
|
use fxhash::FxHashSet;
|
||||||
|
|
||||||
#[derive(Debug)]
|
use crate::{arena::ElementId, ScopeId, Template};
|
||||||
|
|
||||||
|
/// A container for all the relevant steps to modify the Real DOM
|
||||||
|
///
|
||||||
|
/// This object provides a bunch of important information for a renderer to use patch the Real Dom with the state of the
|
||||||
|
/// VirtualDom. This includes the scopes that were modified, the templates that were discovered, and a list of changes
|
||||||
|
/// in the form of a [`Mutation`].
|
||||||
|
///
|
||||||
|
/// These changes are specific to one subtree, so to patch multiple subtrees, you'd need to handle each set separately.
|
||||||
|
///
|
||||||
|
/// Templates, however, apply to all subtrees, not just target subtree.
|
||||||
|
///
|
||||||
|
/// Mutations are the only link between the RealDOM and the VirtualDOM.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
#[must_use = "not handling edits can lead to visual inconsistencies in UI"]
|
||||||
pub struct Mutations<'a> {
|
pub struct Mutations<'a> {
|
||||||
|
/// The ID of the subtree that these edits are targetting
|
||||||
pub subtree: usize,
|
pub subtree: usize,
|
||||||
pub template_mutations: Vec<Mutation<'a>>,
|
|
||||||
|
/// The list of Scopes that were diffed, created, and removed during the Diff process.
|
||||||
|
pub dirty_scopes: FxHashSet<ScopeId>,
|
||||||
|
|
||||||
|
/// Any templates encountered while diffing the DOM.
|
||||||
|
///
|
||||||
|
/// These must be loaded into a cache before applying the edits
|
||||||
|
pub templates: Vec<Template<'a>>,
|
||||||
|
|
||||||
|
/// Any mutations required to patch the renderer to match the layout of the VirtualDom
|
||||||
pub edits: Vec<Mutation<'a>>,
|
pub edits: Vec<Mutation<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Mutations<'a> {
|
impl<'a> Mutations<'a> {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
subtree: 0,
|
|
||||||
edits: Vec::new(),
|
|
||||||
template_mutations: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A useful tool for testing mutations
|
|
||||||
///
|
|
||||||
/// Rewrites IDs to just be "template", so you can compare the mutations
|
/// Rewrites IDs to just be "template", so you can compare the mutations
|
||||||
|
///
|
||||||
|
/// Used really only for testing
|
||||||
pub fn santize(mut self) -> Self {
|
pub fn santize(mut self) -> Self {
|
||||||
for edit in self
|
for edit in self.edits.iter_mut() {
|
||||||
.template_mutations
|
if let Mutation::LoadTemplate { name, .. } = edit {
|
||||||
.iter_mut()
|
*name = "template"
|
||||||
.chain(self.edits.iter_mut())
|
|
||||||
{
|
|
||||||
match edit {
|
|
||||||
Mutation::LoadTemplate { name, .. } => *name = "template",
|
|
||||||
Mutation::SaveTemplate { name, .. } => *name = "template",
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> std::ops::Deref for Mutations<'a> {
|
/// Push a new mutation into the dom_edits list
|
||||||
type Target = Vec<Mutation<'a>>;
|
pub(crate) fn push(&mut self, mutation: Mutation<'static>) {
|
||||||
|
self.edits.push(mutation)
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.edits
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::DerefMut for Mutations<'_> {
|
/// A `Mutation` represents a single instruction for the renderer to use to modify the UI tree to match the state
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
/// of the Dioxus VirtualDom.
|
||||||
&mut self.edits
|
///
|
||||||
}
|
/// These edits can be serialized and sent over the network or through any interface
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
each subtree has its own numbering scheme
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "serialize",
|
feature = "serialize",
|
||||||
derive(serde::Serialize, serde::Deserialize),
|
derive(serde::Serialize, serde::Deserialize),
|
||||||
@@ -61,52 +62,111 @@ each subtree has its own numbering scheme
|
|||||||
)]
|
)]
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Mutation<'a> {
|
pub enum Mutation<'a> {
|
||||||
|
/// Add these m children to the target element
|
||||||
AppendChildren {
|
AppendChildren {
|
||||||
|
/// The ID of the element being mounted to
|
||||||
|
id: ElementId,
|
||||||
|
|
||||||
|
/// The number of nodes on the stack
|
||||||
m: usize,
|
m: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Assign the element at the given path the target ElementId.
|
||||||
|
///
|
||||||
|
/// The path is in the form of a list of indices based on children. Templates cannot have more than 255 children per
|
||||||
|
/// element, hence the use of a single byte.
|
||||||
|
///
|
||||||
|
///
|
||||||
AssignId {
|
AssignId {
|
||||||
|
/// The path of the child of the topmost node on the stack
|
||||||
|
///
|
||||||
|
/// A path of `[]` represents the topmost node. A path of `[0]` represents the first child.
|
||||||
|
/// `[0,1,2]` represents 1st child's 2nd child's 3rd child.
|
||||||
path: &'static [u8],
|
path: &'static [u8],
|
||||||
|
|
||||||
|
/// The ID we're assigning to this element/placeholder.
|
||||||
|
///
|
||||||
|
/// This will be used later to modify the element or replace it with another element.
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
},
|
},
|
||||||
|
|
||||||
CreateElement {
|
/// Create an placeholder int he DOM that we will use later.
|
||||||
name: &'a str,
|
///
|
||||||
},
|
/// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
|
||||||
CreateElementNamespace {
|
|
||||||
name: &'a str,
|
|
||||||
namespace: &'a str,
|
|
||||||
},
|
|
||||||
CreatePlaceholder {
|
CreatePlaceholder {
|
||||||
id: ElementId,
|
/// The ID we're assigning to this element/placeholder.
|
||||||
},
|
///
|
||||||
CreateStaticPlaceholder,
|
/// This will be used later to modify the element or replace it with another element.
|
||||||
CreateStaticText {
|
|
||||||
value: &'a str,
|
|
||||||
},
|
|
||||||
CreateTextNode {
|
|
||||||
value: &'a str,
|
|
||||||
id: ElementId,
|
|
||||||
},
|
|
||||||
HydrateText {
|
|
||||||
path: &'static [u8],
|
|
||||||
value: &'a str,
|
|
||||||
id: ElementId,
|
|
||||||
},
|
|
||||||
LoadTemplate {
|
|
||||||
name: &'static str,
|
|
||||||
index: usize,
|
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Take the current element and replace it with the element with the given id.
|
/// Create a node specifically for text with the given value
|
||||||
ReplaceWith {
|
CreateTextNode {
|
||||||
|
/// The text content of this text node
|
||||||
|
value: &'a str,
|
||||||
|
|
||||||
|
/// The ID we're assigning to this specific text nodes
|
||||||
|
///
|
||||||
|
/// This will be used later to modify the element or replace it with another element.
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Hydrate an existing text node at the given path with the given text.
|
||||||
|
///
|
||||||
|
/// Assign this text node the given ID since we will likely need to modify this text at a later point
|
||||||
|
HydrateText {
|
||||||
|
/// The path of the child of the topmost node on the stack
|
||||||
|
///
|
||||||
|
/// A path of `[]` represents the topmost node. A path of `[0]` represents the first child.
|
||||||
|
/// `[0,1,2]` represents 1st child's 2nd child's 3rd child.
|
||||||
|
path: &'static [u8],
|
||||||
|
|
||||||
|
/// The value of the textnode that we want to set the placeholder with
|
||||||
|
value: &'a str,
|
||||||
|
|
||||||
|
/// The ID we're assigning to this specific text nodes
|
||||||
|
///
|
||||||
|
/// This will be used later to modify the element or replace it with another element.
|
||||||
|
id: ElementId,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Load and clone an existing node from a template saved under that specific name
|
||||||
|
///
|
||||||
|
/// Dioxus guarantees that the renderer will have already been provided the template.
|
||||||
|
/// When the template is picked up in the template list, it should be saved under its "name" - here, the name
|
||||||
|
LoadTemplate {
|
||||||
|
/// The "name" of the template. When paired with `rsx!`, this is autogenerated
|
||||||
|
name: &'static str,
|
||||||
|
|
||||||
|
/// Which root are we loading from the template?
|
||||||
|
///
|
||||||
|
/// The template is stored as a list of nodes. This index represents the position of that root
|
||||||
|
index: usize,
|
||||||
|
|
||||||
|
/// The ID we're assigning to this element being loaded from the template
|
||||||
|
///
|
||||||
|
/// This will be used later to move the element around in lists
|
||||||
|
id: ElementId,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Replace the target element (given by its ID) with the topmost m nodes on the stack
|
||||||
|
ReplaceWith {
|
||||||
|
/// The ID of the node we're going to replace with
|
||||||
|
id: ElementId,
|
||||||
|
|
||||||
|
/// The number of nodes on the stack to use to replace
|
||||||
m: usize,
|
m: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Replace an existing element in the template at the given path with the m nodes on the stack
|
||||||
ReplacePlaceholder {
|
ReplacePlaceholder {
|
||||||
|
/// The path of the child of the topmost node on the stack
|
||||||
|
///
|
||||||
|
/// A path of `[]` represents the topmost node. A path of `[0]` represents the first child.
|
||||||
|
/// `[0,1,2]` represents 1st child's 2nd child's 3rd child.
|
||||||
path: &'static [u8],
|
path: &'static [u8],
|
||||||
|
|
||||||
|
/// The number of nodes on the stack to use to replace
|
||||||
m: usize,
|
m: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -128,47 +188,46 @@ pub enum Mutation<'a> {
|
|||||||
m: usize,
|
m: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
SaveTemplate {
|
/// Set the value of a node's attribute.
|
||||||
name: &'static str,
|
|
||||||
m: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
SetAttribute {
|
SetAttribute {
|
||||||
|
/// The name of the attribute to set.
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
|
/// The value of the attribute.
|
||||||
value: &'a str,
|
value: &'a str,
|
||||||
|
|
||||||
|
/// The ID of the node to set the attribute of.
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
|
||||||
// value: &'bump str,
|
|
||||||
/// The (optional) namespace of the attribute.
|
/// The (optional) namespace of the attribute.
|
||||||
/// For instance, "style" is in the "style" namespace.
|
/// For instance, "style" is in the "style" namespace.
|
||||||
ns: Option<&'a str>,
|
ns: Option<&'a str>,
|
||||||
},
|
},
|
||||||
|
|
||||||
SetStaticAttribute {
|
/// Set the value of a node's attribute.
|
||||||
name: &'a str,
|
|
||||||
value: &'a str,
|
|
||||||
ns: Option<&'a str>,
|
|
||||||
},
|
|
||||||
|
|
||||||
SetBoolAttribute {
|
SetBoolAttribute {
|
||||||
|
/// The name of the attribute to set.
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
|
|
||||||
|
/// The value of the attribute.
|
||||||
value: bool,
|
value: bool,
|
||||||
|
|
||||||
|
/// The ID of the node to set the attribute of.
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
},
|
},
|
||||||
|
|
||||||
SetInnerText {
|
/// Set the textcontent of a node.
|
||||||
value: &'a str,
|
|
||||||
},
|
|
||||||
|
|
||||||
SetText {
|
SetText {
|
||||||
|
/// The textcontent of the node
|
||||||
value: &'a str,
|
value: &'a str,
|
||||||
|
|
||||||
|
/// The ID of the node to set the textcontent of.
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Create a new Event Listener.
|
/// Create a new Event Listener.
|
||||||
NewEventListener {
|
NewEventListener {
|
||||||
/// The name of the event to listen for.
|
/// The name of the event to listen for.
|
||||||
event_name: &'a str,
|
name: &'a str,
|
||||||
|
|
||||||
/// The ID of the node to attach the listener to.
|
/// The ID of the node to attach the listener to.
|
||||||
scope: ScopeId,
|
scope: ScopeId,
|
||||||
@@ -179,13 +238,22 @@ pub enum Mutation<'a> {
|
|||||||
|
|
||||||
/// Remove an existing Event Listener.
|
/// Remove an existing Event Listener.
|
||||||
RemoveEventListener {
|
RemoveEventListener {
|
||||||
|
/// The name of the event to remove.
|
||||||
|
name: &'a str,
|
||||||
|
|
||||||
/// The ID of the node to remove.
|
/// The ID of the node to remove.
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
|
||||||
/// The name of the event to remove.
|
|
||||||
event: &'a str,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Remove a particular node from the DOM
|
||||||
Remove {
|
Remove {
|
||||||
|
/// The ID of the node to remove.
|
||||||
|
id: ElementId,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Push the given root node onto our stack.
|
||||||
|
PushRoot {
|
||||||
|
/// The ID of the root node to push.
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
+404
-93
@@ -1,20 +1,38 @@
|
|||||||
use crate::{any_props::AnyProps, arena::ElementId, Element, ScopeId, ScopeState, UiEvent};
|
use crate::{
|
||||||
|
any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState,
|
||||||
|
};
|
||||||
|
use bumpalo::boxed::Box as BumpBox;
|
||||||
|
use bumpalo::Bump;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
|
fmt::Arguments,
|
||||||
|
future::Future,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type TemplateId = &'static str;
|
pub type TemplateId = &'static str;
|
||||||
|
|
||||||
|
/// The actual state of the component's most recent computation
|
||||||
|
///
|
||||||
|
/// Because Dioxus accepts components in the form of `async fn(Scope) -> Result<VNode>`, we need to support both
|
||||||
|
/// sync and async versions.
|
||||||
|
///
|
||||||
|
/// Dioxus will do its best to immediately resolve any async components into a regular Element, but as an implementor
|
||||||
|
/// you might need to handle the case where there's no node immediately ready.
|
||||||
|
pub enum RenderReturn<'a> {
|
||||||
|
/// A currently-available element
|
||||||
|
Sync(Element<'a>),
|
||||||
|
|
||||||
|
/// An ongoing future that will resolve to a [`Element`]
|
||||||
|
Async(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
|
||||||
|
}
|
||||||
|
|
||||||
/// A reference to a template along with any context needed to hydrate it
|
/// A reference to a template along with any context needed to hydrate it
|
||||||
///
|
///
|
||||||
/// The dynamic parts of the template are stored separately from the static parts. This allows faster diffing by skipping
|
/// The dynamic parts of the template are stored separately from the static parts. This allows faster diffing by skipping
|
||||||
/// static parts of the template.
|
/// static parts of the template.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct VNode<'a> {
|
pub struct VNode<'a> {
|
||||||
// The ID assigned for the root of this template
|
|
||||||
pub node_id: Cell<ElementId>,
|
|
||||||
|
|
||||||
/// The key given to the root of this template.
|
/// The key given to the root of this template.
|
||||||
///
|
///
|
||||||
/// In fragments, this is the key of the first child. In other cases, it is the key of the root.
|
/// In fragments, this is the key of the first child. In other cases, it is the key of the root.
|
||||||
@@ -38,66 +56,170 @@ pub struct VNode<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> VNode<'a> {
|
impl<'a> VNode<'a> {
|
||||||
|
/// Create a template with no nodes that will be skipped over during diffing
|
||||||
pub fn empty() -> Element<'a> {
|
pub fn empty() -> Element<'a> {
|
||||||
Ok(VNode {
|
Ok(VNode {
|
||||||
node_id: Cell::new(ElementId(0)),
|
|
||||||
key: None,
|
key: None,
|
||||||
parent: None,
|
parent: None,
|
||||||
root_ids: &[],
|
root_ids: &[],
|
||||||
dynamic_nodes: &[],
|
dynamic_nodes: &[],
|
||||||
dynamic_attrs: &[],
|
dynamic_attrs: &[],
|
||||||
template: Template {
|
template: Template {
|
||||||
id: "dioxus-empty",
|
name: "dioxus-empty",
|
||||||
roots: &[],
|
roots: &[],
|
||||||
node_paths: &[],
|
node_paths: &[],
|
||||||
attr_paths: &[],
|
attr_paths: &[],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
/// Load a dynamic root at the given index
|
||||||
pub struct Template<'a> {
|
///
|
||||||
pub id: &'a str,
|
/// Returns [`None`] if the root is actually a static node (Element/Text)
|
||||||
pub roots: &'a [TemplateNode<'a>],
|
pub fn dynamic_root(&self, idx: usize) -> Option<&'a DynamicNode<'a>> {
|
||||||
pub node_paths: &'a [&'a [u8]],
|
match &self.template.roots[idx] {
|
||||||
pub attr_paths: &'a [&'a [u8]],
|
TemplateNode::Element { .. } | TemplateNode::Text(_) => None,
|
||||||
}
|
TemplateNode::Dynamic(id) | TemplateNode::DynamicText(id) => {
|
||||||
|
Some(&self.dynamic_nodes[*id])
|
||||||
/// A weird-ish variant of VNodes with way more limited types
|
}
|
||||||
#[derive(Debug, Clone, Copy)]
|
}
|
||||||
pub enum TemplateNode<'a> {
|
|
||||||
Element {
|
|
||||||
tag: &'a str,
|
|
||||||
namespace: Option<&'a str>,
|
|
||||||
attrs: &'a [TemplateAttribute<'a>],
|
|
||||||
children: &'a [TemplateNode<'a>],
|
|
||||||
inner_opt: bool,
|
|
||||||
},
|
|
||||||
Text(&'a str),
|
|
||||||
Dynamic(usize),
|
|
||||||
DynamicText(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum DynamicNode<'a> {
|
|
||||||
Component(VComponent<'a>),
|
|
||||||
Text(VText<'a>),
|
|
||||||
Fragment(VFragment<'a>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> DynamicNode<'a> {
|
|
||||||
pub fn is_component(&self) -> bool {
|
|
||||||
matches!(self, DynamicNode::Component(_))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A static layout of a UI tree that describes a set of dynamic and static nodes.
|
||||||
|
///
|
||||||
|
/// This is the core innovation in Dioxus. Most UIs are made of static nodes, yet participate in diffing like any
|
||||||
|
/// dynamic node. This struct can be created at compile time. It promises that its name is unique, allow Dioxus to use
|
||||||
|
/// its static description of the UI to skip immediately to the dynamic nodes during diffing.
|
||||||
|
///
|
||||||
|
/// For this to work properly, the [`Template::name`] *must* be unique across your entire project. This can be done via variety of
|
||||||
|
/// ways, with the suggested approach being the unique code location (file, line, col, etc).
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Template<'a> {
|
||||||
|
/// The name of the template. This must be unique across your entire program for template diffing to work properly
|
||||||
|
///
|
||||||
|
/// If two templates have the same name, it's likely that Dioxus will panic when diffing.
|
||||||
|
pub name: &'a str,
|
||||||
|
|
||||||
|
/// The list of template nodes that make up the template
|
||||||
|
///
|
||||||
|
/// Unlike react, calls to `rsx!` can have multiple roots. This list supports that paradigm.
|
||||||
|
pub roots: &'a [TemplateNode<'a>],
|
||||||
|
|
||||||
|
/// The paths of each node relative to the root of the template.
|
||||||
|
///
|
||||||
|
/// These will be one segment shorter than the path sent to the renderer since those paths are relative to the
|
||||||
|
/// topmost element, not the `roots` field.
|
||||||
|
pub node_paths: &'a [&'a [u8]],
|
||||||
|
|
||||||
|
/// The paths of each dynamic attribute relative to the root of the template
|
||||||
|
///
|
||||||
|
/// These will be one segment shorter than the path sent to the renderer since those paths are relative to the
|
||||||
|
/// topmost element, not the `roots` field.
|
||||||
|
pub attr_paths: &'a [&'a [u8]],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A statically known node in a layout.
|
||||||
|
///
|
||||||
|
/// This can be created at compile time, saving the VirtualDom time when diffing the tree
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
|
||||||
|
pub enum TemplateNode<'a> {
|
||||||
|
/// An statically known element in the dom.
|
||||||
|
///
|
||||||
|
/// In HTML this would be something like `<div id="123"> </div>`
|
||||||
|
Element {
|
||||||
|
/// The name of the element
|
||||||
|
///
|
||||||
|
/// IE for a div, it would be the string "div"
|
||||||
|
tag: &'a str,
|
||||||
|
|
||||||
|
/// The namespace of the element
|
||||||
|
///
|
||||||
|
/// In HTML, this would be a valid URI that defines a namespace for all elements below it
|
||||||
|
/// SVG is an example of this namespace
|
||||||
|
namespace: Option<&'a str>,
|
||||||
|
|
||||||
|
/// A list of possibly dynamic attribues for this element
|
||||||
|
///
|
||||||
|
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`.
|
||||||
|
attrs: &'a [TemplateAttribute<'a>],
|
||||||
|
|
||||||
|
/// A list of template nodes that define another set of template nodes
|
||||||
|
children: &'a [TemplateNode<'a>],
|
||||||
|
},
|
||||||
|
|
||||||
|
/// This template node is just a piece of static text
|
||||||
|
Text(&'a str),
|
||||||
|
|
||||||
|
/// This template node is unknown, and needs to be created at runtime.
|
||||||
|
Dynamic(usize),
|
||||||
|
|
||||||
|
/// This template node is known to be some text, but needs to be created at runtime
|
||||||
|
///
|
||||||
|
/// This is separate from the pure Dynamic variant for various optimizations
|
||||||
|
DynamicText(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A node created at runtime
|
||||||
|
///
|
||||||
|
/// This node's index in the DynamicNode list on VNode should match its repsective `Dynamic` index
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DynamicNode<'a> {
|
||||||
|
/// A component node
|
||||||
|
///
|
||||||
|
/// Most of the time, Dioxus will actually know which component this is as compile time, but the props and
|
||||||
|
/// assigned scope are dynamic.
|
||||||
|
///
|
||||||
|
/// The actual VComponent can be dynamic between two VNodes, though, allowing implementations to swap
|
||||||
|
/// the render function at runtime
|
||||||
|
Component(VComponent<'a>),
|
||||||
|
|
||||||
|
/// A text node
|
||||||
|
Text(VText<'a>),
|
||||||
|
|
||||||
|
/// A placeholder
|
||||||
|
///
|
||||||
|
/// Used by suspense when a node isn't ready and by fragments that don't render anything
|
||||||
|
///
|
||||||
|
/// In code, this is just an ElementId whose initial value is set to 0 upon creation
|
||||||
|
Placeholder(Cell<ElementId>),
|
||||||
|
|
||||||
|
/// A list of VNodes.
|
||||||
|
///
|
||||||
|
/// Note that this is not a list of dynamic nodes. These must be VNodes and created through conditional rendering
|
||||||
|
/// or iterators.
|
||||||
|
Fragment(&'a [VNode<'a>]),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DynamicNode<'_> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Placeholder(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An instance of a child component
|
||||||
pub struct VComponent<'a> {
|
pub struct VComponent<'a> {
|
||||||
|
/// The name of this component
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
|
|
||||||
|
/// Are the props valid for the 'static lifetime?
|
||||||
|
///
|
||||||
|
/// Internally, this is used as a guarantee. Externally, this might be incorrect, so don't count on it.
|
||||||
|
///
|
||||||
|
/// This flag is assumed by the [`crate::Properties`] trait which is unsafe to implement
|
||||||
pub static_props: bool,
|
pub static_props: bool,
|
||||||
|
|
||||||
|
/// The assigned Scope for this component
|
||||||
pub scope: Cell<Option<ScopeId>>,
|
pub scope: Cell<Option<ScopeId>>,
|
||||||
pub props: Cell<Option<Box<dyn AnyProps<'a> + 'a>>>,
|
|
||||||
|
/// The function pointer of the component, known at compile time
|
||||||
|
///
|
||||||
|
/// It is possible that components get folded at comppile time, so these shouldn't be really used as a key
|
||||||
pub render_fn: *const (),
|
pub render_fn: *const (),
|
||||||
|
|
||||||
|
pub(crate) props: Cell<Option<Box<dyn AnyProps<'a> + 'a>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> std::fmt::Debug for VComponent<'a> {
|
impl<'a> std::fmt::Debug for VComponent<'a> {
|
||||||
@@ -110,65 +232,91 @@ impl<'a> std::fmt::Debug for VComponent<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An instance of some text, mounted to the DOM
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct VText<'a> {
|
pub struct VText<'a> {
|
||||||
pub id: Cell<ElementId>,
|
/// The actual text itself
|
||||||
pub value: &'a str,
|
pub value: &'a str,
|
||||||
|
|
||||||
|
/// The ID of this node in the real DOM
|
||||||
|
pub id: Cell<ElementId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// An attribute of the TemplateNode, created at compile time
|
||||||
pub enum VFragment<'a> {
|
#[derive(Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||||
Empty(Cell<ElementId>),
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
NonEmpty(&'a [VNode<'a>]),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum TemplateAttribute<'a> {
|
pub enum TemplateAttribute<'a> {
|
||||||
|
/// This attribute is entirely known at compile time, enabling
|
||||||
Static {
|
Static {
|
||||||
name: &'static str,
|
/// The name of this attribute.
|
||||||
|
///
|
||||||
|
/// For example, the `href` attribute in `href="https://example.com"`, would have the name "href"
|
||||||
|
name: &'a str,
|
||||||
|
|
||||||
|
/// The value of this attribute, known at compile time
|
||||||
|
///
|
||||||
|
/// Currently this only accepts &str, so values, even if they're known at compile time, are not known
|
||||||
value: &'a str,
|
value: &'a str,
|
||||||
namespace: Option<&'static str>,
|
|
||||||
volatile: bool,
|
/// The namespace of this attribute. Does not exist in the HTML spec
|
||||||
|
namespace: Option<&'a str>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// The attribute in this position is actually determined dynamically at runtime
|
||||||
|
///
|
||||||
|
/// This is the index into the dynamic_attributes field on the container VNode
|
||||||
Dynamic(usize),
|
Dynamic(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Attribute<'a> {
|
pub struct Attribute<'a> {
|
||||||
|
/// The name of the attribute.
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
|
|
||||||
|
/// The value of the attribute
|
||||||
pub value: AttributeValue<'a>,
|
pub value: AttributeValue<'a>,
|
||||||
|
|
||||||
|
/// The namespace of the attribute.
|
||||||
|
///
|
||||||
|
/// Doesn’t exist in the html spec. Used in Dioxus to denote “style” tags and other attribute groups.
|
||||||
pub namespace: Option<&'static str>,
|
pub namespace: Option<&'static str>,
|
||||||
|
|
||||||
|
/// The element in the DOM that this attribute belongs to
|
||||||
pub mounted_element: Cell<ElementId>,
|
pub mounted_element: Cell<ElementId>,
|
||||||
|
|
||||||
|
/// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated
|
||||||
pub volatile: bool,
|
pub volatile: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Any of the built-in values that the Dioxus VirtualDom supports as dynamic attributes on elements
|
||||||
|
///
|
||||||
|
/// These are built-in to be faster during the diffing process. To use a custom value, use the [`AttributeValue::Any`]
|
||||||
|
/// variant.
|
||||||
pub enum AttributeValue<'a> {
|
pub enum AttributeValue<'a> {
|
||||||
|
/// Text attribute
|
||||||
Text(&'a str),
|
Text(&'a str),
|
||||||
Float(f32),
|
|
||||||
Int(i32),
|
/// A float
|
||||||
|
Float(f64),
|
||||||
|
|
||||||
|
/// Signed integer
|
||||||
|
Int(i64),
|
||||||
|
|
||||||
|
/// Boolean
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
Listener(RefCell<&'a mut dyn FnMut(UiEvent<dyn Any>)>),
|
|
||||||
Any(&'a dyn AnyValue),
|
/// A listener, like "onclick"
|
||||||
|
Listener(RefCell<Option<ListenerCb<'a>>>),
|
||||||
|
|
||||||
|
/// An arbitrary value that implements PartialEq and is static
|
||||||
|
Any(BumpBox<'a, dyn AnyValue>),
|
||||||
|
|
||||||
|
/// A "none" value, resulting in the removal of an attribute from the dom
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> AttributeValue<'a> {
|
type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event<dyn Any>) + 'a>;
|
||||||
pub fn new_listener<T: 'static>(
|
|
||||||
cx: &'a ScopeState,
|
|
||||||
mut f: impl FnMut(UiEvent<T>) + 'a,
|
|
||||||
) -> AttributeValue<'a> {
|
|
||||||
AttributeValue::Listener(RefCell::new(cx.bump().alloc(
|
|
||||||
move |event: UiEvent<dyn Any>| {
|
|
||||||
if let Ok(data) = event.data.downcast::<T>() {
|
|
||||||
f(UiEvent {
|
|
||||||
bubbles: event.bubbles,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> std::fmt::Debug for AttributeValue<'a> {
|
impl<'a> std::fmt::Debug for AttributeValue<'a> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
@@ -192,26 +340,13 @@ impl<'a> PartialEq for AttributeValue<'a> {
|
|||||||
(Self::Int(l0), Self::Int(r0)) => l0 == r0,
|
(Self::Int(l0), Self::Int(r0)) => l0 == r0,
|
||||||
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||||
(Self::Listener(_), Self::Listener(_)) => true,
|
(Self::Listener(_), Self::Listener(_)) => true,
|
||||||
(Self::Any(l0), Self::Any(r0)) => l0.any_cmp(*r0),
|
(Self::Any(l0), Self::Any(r0)) => l0.any_cmp(r0.as_ref()),
|
||||||
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
|
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> AttributeValue<'a> {
|
#[doc(hidden)]
|
||||||
pub fn matches_type(&self, other: &'a AttributeValue<'a>) -> bool {
|
|
||||||
match (self, other) {
|
|
||||||
(Self::Text(_), Self::Text(_)) => true,
|
|
||||||
(Self::Float(_), Self::Float(_)) => true,
|
|
||||||
(Self::Int(_), Self::Int(_)) => true,
|
|
||||||
(Self::Bool(_), Self::Bool(_)) => true,
|
|
||||||
(Self::Listener(_), Self::Listener(_)) => true,
|
|
||||||
(Self::Any(_), Self::Any(_)) => true,
|
|
||||||
_ => return false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait AnyValue {
|
pub trait AnyValue {
|
||||||
fn any_cmp(&self, other: &dyn AnyValue) -> bool;
|
fn any_cmp(&self, other: &dyn AnyValue) -> bool;
|
||||||
fn our_typeid(&self) -> TypeId;
|
fn our_typeid(&self) -> TypeId;
|
||||||
@@ -231,9 +366,185 @@ impl<T: PartialEq + Any> AnyValue for T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[doc(hidden)]
|
||||||
fn what_are_the_sizes() {
|
pub trait ComponentReturn<'a, A = ()> {
|
||||||
dbg!(std::mem::size_of::<VNode>());
|
fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a>;
|
||||||
dbg!(std::mem::size_of::<Template>());
|
}
|
||||||
dbg!(std::mem::size_of::<TemplateNode>());
|
|
||||||
|
impl<'a> ComponentReturn<'a> for Element<'a> {
|
||||||
|
fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> {
|
||||||
|
RenderReturn::Sync(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct AsyncMarker;
|
||||||
|
impl<'a, F> ComponentReturn<'a, AsyncMarker> for F
|
||||||
|
where
|
||||||
|
F: Future<Output = Element<'a>> + 'a,
|
||||||
|
{
|
||||||
|
fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> {
|
||||||
|
let f: &mut dyn Future<Output = Element<'a>> = cx.bump().alloc(self);
|
||||||
|
RenderReturn::Async(unsafe { BumpBox::from_raw(f) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> RenderReturn<'a> {
|
||||||
|
pub(crate) unsafe fn extend_lifetime_ref<'c>(&self) -> &'c RenderReturn<'c> {
|
||||||
|
unsafe { std::mem::transmute(self) }
|
||||||
|
}
|
||||||
|
pub(crate) unsafe fn extend_lifetime<'c>(self) -> RenderReturn<'c> {
|
||||||
|
unsafe { std::mem::transmute(self) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait that allows various items to be converted into a dynamic node for the rsx macro
|
||||||
|
pub trait IntoDynNode<'a, A = ()> {
|
||||||
|
/// Consume this item along with a scopestate and produce a DynamicNode
|
||||||
|
///
|
||||||
|
/// You can use the bump alloactor of the scopestate to creat the dynamic node
|
||||||
|
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoDynNode<'a> for () {
|
||||||
|
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
DynamicNode::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> IntoDynNode<'a> for VNode<'a> {
|
||||||
|
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
DynamicNode::Fragment(_cx.bump().alloc([self]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> IntoDynNode<'a> for Element<'a> {
|
||||||
|
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
match self {
|
||||||
|
Ok(val) => val.into_vnode(_cx),
|
||||||
|
_ => DynamicNode::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option<T> {
|
||||||
|
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
match self {
|
||||||
|
Some(val) => val.into_vnode(_cx),
|
||||||
|
None => DynamicNode::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoDynNode<'a> for &Element<'a> {
|
||||||
|
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
match self.as_ref() {
|
||||||
|
Ok(val) => val.clone().into_vnode(_cx),
|
||||||
|
_ => DynamicNode::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> {
|
||||||
|
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
DynamicNode::Fragment(cx.bump().alloc([self.call(cx)]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoDynNode<'_> for &'a str {
|
||||||
|
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
||||||
|
cx.text_node(format_args!("{}", self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoDynNode<'_> for String {
|
||||||
|
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
||||||
|
cx.text_node(format_args!("{}", self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'b> IntoDynNode<'b> for Arguments<'_> {
|
||||||
|
fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> {
|
||||||
|
cx.text_node(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoDynNode<'a> for &'a VNode<'a> {
|
||||||
|
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
DynamicNode::Fragment(_cx.bump().alloc([VNode {
|
||||||
|
parent: self.parent,
|
||||||
|
template: self.template,
|
||||||
|
root_ids: self.root_ids,
|
||||||
|
key: self.key,
|
||||||
|
dynamic_nodes: self.dynamic_nodes,
|
||||||
|
dynamic_attrs: self.dynamic_attrs,
|
||||||
|
}]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntoTemplate<'a> {
|
||||||
|
fn into_template(self, _cx: &'a ScopeState) -> VNode<'a>;
|
||||||
|
}
|
||||||
|
impl<'a> IntoTemplate<'a> for VNode<'a> {
|
||||||
|
fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> {
|
||||||
|
fn into_template(self, cx: &'a ScopeState) -> VNode<'a> {
|
||||||
|
self.call(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that we're using the E as a generic but this is never crafted anyways.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct FromNodeIterator;
|
||||||
|
impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T
|
||||||
|
where
|
||||||
|
T: Iterator<Item = I>,
|
||||||
|
I: IntoTemplate<'a>,
|
||||||
|
{
|
||||||
|
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
let mut nodes = bumpalo::collections::Vec::new_in(cx.bump());
|
||||||
|
|
||||||
|
nodes.extend(self.into_iter().map(|node| node.into_template(cx)));
|
||||||
|
|
||||||
|
match nodes.into_bump_slice() {
|
||||||
|
children if children.is_empty() => DynamicNode::default(),
|
||||||
|
children => DynamicNode::Fragment(children),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A value that can be converted into an attribute value
|
||||||
|
pub trait IntoAttributeValue<'a> {
|
||||||
|
/// Convert into an attribute value
|
||||||
|
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoAttributeValue<'a> for &'a str {
|
||||||
|
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||||
|
AttributeValue::Text(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> IntoAttributeValue<'a> for f64 {
|
||||||
|
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||||
|
AttributeValue::Float(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> IntoAttributeValue<'a> for i64 {
|
||||||
|
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||||
|
AttributeValue::Int(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> IntoAttributeValue<'a> for bool {
|
||||||
|
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||||
|
AttributeValue::Bool(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> IntoAttributeValue<'a> for Arguments<'_> {
|
||||||
|
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> {
|
||||||
|
use bumpalo::core_alloc::fmt::Write;
|
||||||
|
let mut str_buf = bumpalo::collections::String::new_in(bump);
|
||||||
|
str_buf.write_fmt(self).unwrap();
|
||||||
|
AttributeValue::Text(str_buf.into_bump_str())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,54 +8,38 @@ use std::{
|
|||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// An ID representing an ongoing suspended component
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
pub struct SuspenseId(pub usize);
|
pub(crate) struct SuspenseId(pub usize);
|
||||||
|
|
||||||
pub type SuspenseContext = Rc<SuspenseBoundary>;
|
/// A boundary in the VirtualDom that captures all suspended components below it
|
||||||
|
pub struct SuspenseContext {
|
||||||
/// Essentially a fiber in React
|
pub(crate) id: ScopeId,
|
||||||
pub struct SuspenseBoundary {
|
pub(crate) waiting_on: RefCell<HashSet<SuspenseId>>,
|
||||||
pub id: ScopeId,
|
pub(crate) mutations: RefCell<Mutations<'static>>,
|
||||||
pub waiting_on: RefCell<HashSet<SuspenseId>>,
|
pub(crate) placeholder: Cell<Option<ElementId>>,
|
||||||
pub mutations: RefCell<Mutations<'static>>,
|
pub(crate) created_on_stack: Cell<usize>,
|
||||||
pub placeholder: Cell<Option<ElementId>>,
|
|
||||||
|
|
||||||
pub created_on_stack: Cell<usize>,
|
|
||||||
|
|
||||||
// whenever the suspense resolves, we call this onresolve function
|
|
||||||
// this lets us do things like putting up a loading spinner
|
|
||||||
//
|
|
||||||
// todo: we need a way of controlling whether or not a component hides itself but still processes changes
|
|
||||||
// If we run into suspense, we perform a diff, so its important that the old elements are still around.
|
|
||||||
//
|
|
||||||
// When the timer expires, I imagine a container could hide the elements and show the spinner. This, however,
|
|
||||||
// can not be
|
|
||||||
pub onresolve: Option<Box<dyn FnOnce()>>,
|
|
||||||
|
|
||||||
/// Called when
|
|
||||||
pub onstart: Option<Box<dyn FnOnce()>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SuspenseBoundary {
|
impl SuspenseContext {
|
||||||
|
/// Create a new boundary for suspense
|
||||||
pub fn new(id: ScopeId) -> Self {
|
pub fn new(id: ScopeId) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
waiting_on: Default::default(),
|
waiting_on: Default::default(),
|
||||||
mutations: RefCell::new(Mutations::new()),
|
mutations: RefCell::new(Mutations::default()),
|
||||||
placeholder: Cell::new(None),
|
placeholder: Cell::new(None),
|
||||||
created_on_stack: Cell::new(0),
|
created_on_stack: Cell::new(0),
|
||||||
onresolve: None,
|
|
||||||
onstart: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct SuspenseLeaf {
|
pub(crate) struct SuspenseLeaf {
|
||||||
pub id: SuspenseId,
|
pub(crate) id: SuspenseId,
|
||||||
pub scope_id: ScopeId,
|
pub(crate) scope_id: ScopeId,
|
||||||
pub tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||||
pub notified: Cell<bool>,
|
pub(crate) notified: Cell<bool>,
|
||||||
pub task: *mut dyn Future<Output = Element<'static>>,
|
pub(crate) task: *mut dyn Future<Output = Element<'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RcWake for SuspenseLeaf {
|
impl RcWake for SuspenseLeaf {
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ use super::{waker::RcWake, Scheduler, SchedulerMsg};
|
|||||||
use crate::ScopeId;
|
use crate::ScopeId;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::task::Context;
|
use std::{pin::Pin, rc::Rc};
|
||||||
use std::{pin::Pin, rc::Rc, task::Poll};
|
|
||||||
|
|
||||||
|
/// A task's unique identifier.
|
||||||
|
///
|
||||||
|
/// `TaskId` is a `usize` that is unique across the entire VirtualDOM and across time. TaskIDs will never be reused
|
||||||
|
/// once a Task has been completed.
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
pub struct TaskId(pub usize);
|
pub struct TaskId(pub usize);
|
||||||
@@ -12,22 +15,9 @@ pub struct TaskId(pub usize);
|
|||||||
/// the task itself is the waker
|
/// the task itself is the waker
|
||||||
pub(crate) struct LocalTask {
|
pub(crate) struct LocalTask {
|
||||||
pub scope: ScopeId,
|
pub scope: ScopeId,
|
||||||
|
pub(super) task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||||
id: TaskId,
|
id: TaskId,
|
||||||
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||||
task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LocalTask {
|
|
||||||
/// Poll this task and return whether or not it is complete
|
|
||||||
pub(super) fn progress(self: &Rc<Self>) -> bool {
|
|
||||||
let waker = self.waker();
|
|
||||||
let mut cx = Context::from_waker(&waker);
|
|
||||||
|
|
||||||
match self.task.borrow_mut().as_mut().poll(&mut cx) {
|
|
||||||
Poll::Ready(_) => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scheduler {
|
impl Scheduler {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ use futures_util::FutureExt;
|
|||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
factory::RenderReturn,
|
|
||||||
innerlude::{Mutation, Mutations, SuspenseContext},
|
innerlude::{Mutation, Mutations, SuspenseContext},
|
||||||
TaskId, VNode, VirtualDom,
|
nodes::RenderReturn,
|
||||||
|
ScopeId, TaskId, VNode, VirtualDom,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{waker::RcWake, SuspenseId};
|
use super::{waker::RcWake, SuspenseId};
|
||||||
@@ -14,12 +14,15 @@ impl VirtualDom {
|
|||||||
///
|
///
|
||||||
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
|
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
|
||||||
/// queue
|
/// queue
|
||||||
pub fn handle_task_wakeup(&mut self, id: TaskId) {
|
pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) {
|
||||||
let mut tasks = self.scheduler.tasks.borrow_mut();
|
let mut tasks = self.scheduler.tasks.borrow_mut();
|
||||||
let task = &tasks[id.0];
|
let task = &tasks[id.0];
|
||||||
|
|
||||||
|
let waker = task.waker();
|
||||||
|
let mut cx = Context::from_waker(&waker);
|
||||||
|
|
||||||
// If the task completes...
|
// If the task completes...
|
||||||
if task.progress() {
|
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
||||||
// Remove it from the scope so we dont try to double drop it when the scope dropes
|
// Remove it from the scope so we dont try to double drop it when the scope dropes
|
||||||
self.scopes[task.scope.0].spawned_tasks.remove(&id);
|
self.scopes[task.scope.0].spawned_tasks.remove(&id);
|
||||||
|
|
||||||
@@ -28,7 +31,15 @@ impl VirtualDom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
|
pub(crate) fn acquire_suspense_boundary<'a>(&self, id: ScopeId) -> &'a SuspenseContext {
|
||||||
|
let ct = self.scopes[id.0]
|
||||||
|
.consume_context::<SuspenseContext>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
unsafe { &*(ct as *const SuspenseContext) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
|
||||||
println!("suspense notified");
|
println!("suspense notified");
|
||||||
|
|
||||||
let leaf = self
|
let leaf = self
|
||||||
@@ -53,9 +64,8 @@ impl VirtualDom {
|
|||||||
// we should attach them to that component and then render its children
|
// we should attach them to that component and then render its children
|
||||||
// continue rendering the tree until we hit yet another suspended component
|
// continue rendering the tree until we hit yet another suspended component
|
||||||
if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
|
if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
|
||||||
let fiber = &self.scopes[leaf.scope_id.0]
|
// safety: we're not going to modify the suspense context but we don't want to make a clone of it
|
||||||
.consume_context::<SuspenseContext>()
|
let fiber = self.acquire_suspense_boundary(leaf.scope_id);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
println!("ready pool");
|
println!("ready pool");
|
||||||
|
|
||||||
@@ -78,19 +88,22 @@ impl VirtualDom {
|
|||||||
let template: &VNode = unsafe { std::mem::transmute(template) };
|
let template: &VNode = unsafe { std::mem::transmute(template) };
|
||||||
let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) };
|
let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) };
|
||||||
|
|
||||||
todo!();
|
std::mem::swap(&mut self.mutations, mutations);
|
||||||
// let place_holder_id = scope.placeholder.get().unwrap();
|
|
||||||
// self.scope_stack.push(scope_id);
|
|
||||||
// let created = self.create(mutations, template);
|
|
||||||
// self.scope_stack.pop();
|
|
||||||
// mutations.push(Mutation::ReplaceWith {
|
|
||||||
// id: place_holder_id,
|
|
||||||
// m: created,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// for leaf in self.collected_leaves.drain(..) {
|
let place_holder_id = scope.placeholder.get().unwrap();
|
||||||
// fiber.waiting_on.borrow_mut().insert(leaf);
|
self.scope_stack.push(scope_id);
|
||||||
// }
|
let created = self.create(template);
|
||||||
|
self.scope_stack.pop();
|
||||||
|
mutations.push(Mutation::ReplaceWith {
|
||||||
|
id: place_holder_id,
|
||||||
|
m: created,
|
||||||
|
});
|
||||||
|
|
||||||
|
for leaf in self.collected_leaves.drain(..) {
|
||||||
|
fiber.waiting_on.borrow_mut().insert(leaf);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mem::swap(&mut self.mutations, mutations);
|
||||||
|
|
||||||
if fiber.waiting_on.borrow().is_empty() {
|
if fiber.waiting_on.borrow().is_empty() {
|
||||||
println!("fiber is finished!");
|
println!("fiber is finished!");
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
any_props::AnyProps,
|
any_props::AnyProps,
|
||||||
bump_frame::BumpFrame,
|
bump_frame::BumpFrame,
|
||||||
factory::RenderReturn,
|
|
||||||
innerlude::DirtyScope,
|
innerlude::DirtyScope,
|
||||||
innerlude::{SuspenseId, SuspenseLeaf},
|
innerlude::{SuspenseId, SuspenseLeaf},
|
||||||
|
nodes::RenderReturn,
|
||||||
scheduler::RcWake,
|
scheduler::RcWake,
|
||||||
scopes::{ScopeId, ScopeState},
|
scopes::{ScopeId, ScopeState},
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
|
AttributeValue, DynamicNode, VNode,
|
||||||
};
|
};
|
||||||
use futures_util::FutureExt;
|
use futures_util::FutureExt;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -17,17 +18,17 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
pub(super) fn new_scope(&mut self, props: *const dyn AnyProps<'static>) -> &mut ScopeState {
|
pub(super) fn new_scope(&mut self, props: Box<dyn AnyProps<'static>>) -> &ScopeState {
|
||||||
let parent = self.acquire_current_scope_raw();
|
let parent = self.acquire_current_scope_raw();
|
||||||
let entry = self.scopes.vacant_entry();
|
let entry = self.scopes.vacant_entry();
|
||||||
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
||||||
let id = ScopeId(entry.key());
|
let id = ScopeId(entry.key());
|
||||||
|
|
||||||
entry.insert(ScopeState {
|
entry.insert(Box::new(ScopeState {
|
||||||
parent,
|
parent,
|
||||||
id,
|
id,
|
||||||
height,
|
height,
|
||||||
props,
|
props: Some(props),
|
||||||
placeholder: Default::default(),
|
placeholder: Default::default(),
|
||||||
node_arena_1: BumpFrame::new(50),
|
node_arena_1: BumpFrame::new(50),
|
||||||
node_arena_2: BumpFrame::new(50),
|
node_arena_2: BumpFrame::new(50),
|
||||||
@@ -38,23 +39,69 @@ impl VirtualDom {
|
|||||||
hook_idx: Default::default(),
|
hook_idx: Default::default(),
|
||||||
shared_contexts: Default::default(),
|
shared_contexts: Default::default(),
|
||||||
tasks: self.scheduler.clone(),
|
tasks: self.scheduler.clone(),
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn acquire_current_scope_raw(&mut self) -> Option<*mut ScopeState> {
|
fn acquire_current_scope_raw(&mut self) -> Option<*mut ScopeState> {
|
||||||
self.scope_stack
|
self.scope_stack
|
||||||
.last()
|
.last()
|
||||||
.copied()
|
.copied()
|
||||||
.and_then(|id| self.scopes.get_mut(id.0).map(|f| f as *mut ScopeState))
|
.and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_drop_safety(&self, scope: ScopeId) {
|
||||||
|
let scope = &self.scopes[scope.0];
|
||||||
|
let node = unsafe { scope.previous_frame().try_load_node() };
|
||||||
|
|
||||||
|
// And now we want to make sure the previous frame has dropped anything that borrows self
|
||||||
|
if let Some(RenderReturn::Sync(Ok(node))) = node {
|
||||||
|
self.ensure_drop_safety_inner(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_drop_safety_inner(&self, node: &VNode) {
|
||||||
|
for attr in node.dynamic_attrs {
|
||||||
|
if let AttributeValue::Listener(l) = &attr.value {
|
||||||
|
l.borrow_mut().take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in node.dynamic_nodes {
|
||||||
|
match child {
|
||||||
|
DynamicNode::Component(c) => {
|
||||||
|
// Only descend if the props are borrowed
|
||||||
|
if !c.static_props {
|
||||||
|
self.ensure_drop_safety(c.scope.get().unwrap());
|
||||||
|
c.props.set(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicNode::Fragment(f) => {
|
||||||
|
for node in *f {
|
||||||
|
self.ensure_drop_safety_inner(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
||||||
|
// 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(scope_id);
|
||||||
|
|
||||||
let mut new_nodes = unsafe {
|
let mut new_nodes = unsafe {
|
||||||
let scope = &mut self.scopes[scope_id.0];
|
let scope = self.scopes[scope_id.0].as_mut();
|
||||||
|
|
||||||
|
scope.previous_frame_mut().bump.reset();
|
||||||
|
|
||||||
|
// Make sure to reset the hook counter so we give out hooks in the right order
|
||||||
scope.hook_idx.set(0);
|
scope.hook_idx.set(0);
|
||||||
|
|
||||||
// safety: due to how we traverse the tree, we know that the scope is not currently aliased
|
// safety: due to how we traverse the tree, we know that the scope is not currently aliased
|
||||||
let props: &dyn AnyProps = mem::transmute(&*scope.props);
|
let props = scope.props.as_ref().unwrap().as_ref();
|
||||||
|
let props: &dyn AnyProps = mem::transmute(props);
|
||||||
props.render(scope).extend_lifetime()
|
props.render(scope).extend_lifetime()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,7 +160,7 @@ impl VirtualDom {
|
|||||||
let frame = scope.previous_frame();
|
let frame = scope.previous_frame();
|
||||||
|
|
||||||
// set the new head of the bump frame
|
// set the new head of the bump frame
|
||||||
let alloced = frame.bump.alloc(new_nodes);
|
let alloced = &*frame.bump.alloc(new_nodes);
|
||||||
frame.node.set(alloced);
|
frame.node.set(alloced);
|
||||||
|
|
||||||
// And move the render generation forward by one
|
// And move the render generation forward by one
|
||||||
|
|||||||
+181
-49
@@ -1,18 +1,21 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
any_props::AnyProps,
|
any_props::AnyProps,
|
||||||
|
any_props::VProps,
|
||||||
arena::ElementId,
|
arena::ElementId,
|
||||||
bump_frame::BumpFrame,
|
bump_frame::BumpFrame,
|
||||||
factory::RenderReturn,
|
innerlude::{DynamicNode, EventHandler, VComponent, VText},
|
||||||
innerlude::{Scheduler, SchedulerMsg},
|
innerlude::{Scheduler, SchedulerMsg},
|
||||||
lazynodes::LazyNodes,
|
lazynodes::LazyNodes,
|
||||||
Element, TaskId,
|
nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn},
|
||||||
|
Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||||
};
|
};
|
||||||
use bumpalo::Bump;
|
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||||
use std::future::Future;
|
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
fmt::Arguments,
|
||||||
|
future::Future,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
@@ -20,7 +23,7 @@ use std::{
|
|||||||
/// A wrapper around the [`Scoped`] object that contains a reference to the [`ScopeState`] and properties for a given
|
/// A wrapper around the [`Scoped`] object that contains a reference to the [`ScopeState`] and properties for a given
|
||||||
/// component.
|
/// component.
|
||||||
///
|
///
|
||||||
/// The [`Scope`] is your handle to the [`VirtualDom`] and the component state. Every component is given its own
|
/// The [`Scope`] is your handle to the [`crate::VirtualDom`] and the component state. Every component is given its own
|
||||||
/// [`ScopeState`] and merged with its properties to create a [`Scoped`].
|
/// [`ScopeState`] and merged with its properties to create a [`Scoped`].
|
||||||
///
|
///
|
||||||
/// The [`Scope`] handle specifically exists to provide a stable reference to these items for the lifetime of the
|
/// The [`Scope`] handle specifically exists to provide a stable reference to these items for the lifetime of the
|
||||||
@@ -53,15 +56,16 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> {
|
|||||||
|
|
||||||
/// A component's unique identifier.
|
/// A component's unique identifier.
|
||||||
///
|
///
|
||||||
/// `ScopeId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`ScopeID`]s will never be reused
|
/// `ScopeId` is a `usize` that acts a key for the internal slab of Scopes. This means that the key is not unqiue across
|
||||||
/// once a component has been unmounted.
|
/// time. We do try and guarantee that between calls to `wait_for_work`, no ScopeIds will be recycled in order to give
|
||||||
|
/// time for any logic that relies on these IDs to properly update.
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
||||||
pub struct ScopeId(pub usize);
|
pub struct ScopeId(pub usize);
|
||||||
|
|
||||||
/// A component's state.
|
/// A component's state separate from its props.
|
||||||
///
|
///
|
||||||
/// This struct stores all the important information about a component's state without the props.
|
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||||
pub struct ScopeState {
|
pub struct ScopeState {
|
||||||
pub(crate) render_cnt: Cell<usize>,
|
pub(crate) render_cnt: Cell<usize>,
|
||||||
|
|
||||||
@@ -82,12 +86,12 @@ pub struct ScopeState {
|
|||||||
pub(crate) tasks: Rc<Scheduler>,
|
pub(crate) tasks: Rc<Scheduler>,
|
||||||
pub(crate) spawned_tasks: HashSet<TaskId>,
|
pub(crate) spawned_tasks: HashSet<TaskId>,
|
||||||
|
|
||||||
pub(crate) props: *const dyn AnyProps<'static>,
|
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
||||||
pub(crate) placeholder: Cell<Option<ElementId>>,
|
pub(crate) placeholder: Cell<Option<ElementId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScopeState {
|
impl<'src> ScopeState {
|
||||||
pub fn current_frame(&self) -> &BumpFrame {
|
pub(crate) fn current_frame(&self) -> &BumpFrame {
|
||||||
match self.render_cnt.get() % 2 {
|
match self.render_cnt.get() % 2 {
|
||||||
0 => &self.node_arena_1,
|
0 => &self.node_arena_1,
|
||||||
1 => &self.node_arena_2,
|
1 => &self.node_arena_2,
|
||||||
@@ -95,7 +99,7 @@ impl ScopeState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous_frame(&self) -> &BumpFrame {
|
pub(crate) fn previous_frame(&self) -> &BumpFrame {
|
||||||
match self.render_cnt.get() % 2 {
|
match self.render_cnt.get() % 2 {
|
||||||
1 => &self.node_arena_1,
|
1 => &self.node_arena_1,
|
||||||
0 => &self.node_arena_2,
|
0 => &self.node_arena_2,
|
||||||
@@ -103,6 +107,14 @@ impl ScopeState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn previous_frame_mut(&mut self) -> &mut BumpFrame {
|
||||||
|
match self.render_cnt.get() % 2 {
|
||||||
|
1 => &mut self.node_arena_1,
|
||||||
|
0 => &mut self.node_arena_2,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the current render since the inception of this component
|
/// Get the current render since the inception of this component
|
||||||
///
|
///
|
||||||
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
|
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
|
||||||
@@ -117,7 +129,8 @@ impl ScopeState {
|
|||||||
///
|
///
|
||||||
/// If you need to allocate items that need to be dropped, use bumpalo's box.
|
/// If you need to allocate items that need to be dropped, use bumpalo's box.
|
||||||
pub fn bump(&self) -> &Bump {
|
pub fn bump(&self) -> &Bump {
|
||||||
&self.current_frame().bump
|
// note that this is actually the previous frame since we use that as scratch space while the component is rendering
|
||||||
|
&self.previous_frame().bump
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle to the currently active head node arena for this Scope
|
/// Get a handle to the currently active head node arena for this Scope
|
||||||
@@ -125,7 +138,7 @@ impl ScopeState {
|
|||||||
/// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
|
/// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
|
||||||
///
|
///
|
||||||
/// Panics if the tree has not been built yet.
|
/// Panics if the tree has not been built yet.
|
||||||
pub fn root_node<'a>(&'a self) -> &'a RenderReturn<'a> {
|
pub fn root_node(&self) -> &RenderReturn {
|
||||||
self.try_root_node()
|
self.try_root_node()
|
||||||
.expect("The tree has not been built yet. Make sure to call rebuild on the tree before accessing its nodes.")
|
.expect("The tree has not been built yet. Make sure to call rebuild on the tree before accessing its nodes.")
|
||||||
}
|
}
|
||||||
@@ -135,7 +148,7 @@ impl ScopeState {
|
|||||||
/// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
|
/// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
|
||||||
///
|
///
|
||||||
/// Returns [`None`] if the tree has not been built yet.
|
/// Returns [`None`] if the tree has not been built yet.
|
||||||
pub fn try_root_node<'a>(&'a self) -> Option<&'a RenderReturn<'a>> {
|
pub fn try_root_node(&self) -> Option<&RenderReturn> {
|
||||||
let ptr = self.current_frame().node.get();
|
let ptr = self.current_frame().node.get();
|
||||||
|
|
||||||
if ptr.is_null() {
|
if ptr.is_null() {
|
||||||
@@ -165,9 +178,9 @@ impl ScopeState {
|
|||||||
self.height
|
self.height
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the Parent of this [`Scope`] within this Dioxus [`VirtualDom`].
|
/// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`].
|
||||||
///
|
///
|
||||||
/// This ID is not unique across Dioxus [`VirtualDom`]s or across time. IDs will be reused when components are unmounted.
|
/// This ID is not unique across Dioxus [`crate::VirtualDom`]s or across time. IDs will be reused when components are unmounted.
|
||||||
///
|
///
|
||||||
/// The base component will not have a parent, and will return `None`.
|
/// The base component will not have a parent, and will return `None`.
|
||||||
///
|
///
|
||||||
@@ -186,9 +199,9 @@ impl ScopeState {
|
|||||||
self.parent.map(|p| unsafe { &*p }.id)
|
self.parent.map(|p| unsafe { &*p }.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the ID of this Scope within this Dioxus [`VirtualDom`].
|
/// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`].
|
||||||
///
|
///
|
||||||
/// This ID is not unique across Dioxus [`VirtualDom`]s or across time. IDs will be reused when components are unmounted.
|
/// This ID is not unique across Dioxus [`crate::VirtualDom`]s or across time. IDs will be reused when components are unmounted.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
@@ -205,7 +218,7 @@ impl ScopeState {
|
|||||||
|
|
||||||
/// Create a subscription that schedules a future render for the reference component
|
/// Create a subscription that schedules a future render for the reference component
|
||||||
///
|
///
|
||||||
/// ## Notice: you should prefer using [`schedule_update_any`] and [`scope_id`]
|
/// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
|
||||||
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
||||||
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
||||||
Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
|
Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
|
||||||
@@ -228,7 +241,7 @@ impl ScopeState {
|
|||||||
|
|
||||||
/// Get the [`ScopeId`] of a mounted component.
|
/// Get the [`ScopeId`] of a mounted component.
|
||||||
///
|
///
|
||||||
/// `ScopeId` is not unique for the lifetime of the [`VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
|
/// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
|
||||||
pub fn needs_update_any(&self, id: ScopeId) {
|
pub fn needs_update_any(&self, id: ScopeId) {
|
||||||
self.tasks
|
self.tasks
|
||||||
.sender
|
.sender
|
||||||
@@ -237,20 +250,17 @@ impl ScopeState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return any context of type T if it exists on this scope
|
/// Return any context of type T if it exists on this scope
|
||||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
pub fn has_context<T: 'static>(&self) -> Option<&T> {
|
||||||
self.shared_contexts
|
let contextex = self.shared_contexts.borrow();
|
||||||
.borrow()
|
let val = contextex.get(&TypeId::of::<T>())?;
|
||||||
.get(&TypeId::of::<T>())
|
let as_concrete = val.downcast_ref::<T>()? as *const T;
|
||||||
.and_then(|shared| shared.downcast_ref::<T>())
|
Some(unsafe { &*as_concrete })
|
||||||
.cloned()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to retrieve a shared state with type `T` from any parent scope.
|
/// Try to retrieve a shared state with type `T` from any parent scope.
|
||||||
///
|
///
|
||||||
/// The state will be cloned and returned, if it exists.
|
/// To release the borrow, use `cloned` if the context is clone.
|
||||||
///
|
pub fn consume_context<T: 'static>(&self) -> Option<&T> {
|
||||||
/// We recommend wrapping the state in an `Rc` or `Arc` to avoid deep cloning.
|
|
||||||
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
|
||||||
if let Some(this_ctx) = self.has_context() {
|
if let Some(this_ctx) = self.has_context() {
|
||||||
return Some(this_ctx);
|
return Some(this_ctx);
|
||||||
}
|
}
|
||||||
@@ -260,26 +270,24 @@ impl ScopeState {
|
|||||||
// safety: all parent pointers are valid thanks to the bump arena
|
// safety: all parent pointers are valid thanks to the bump arena
|
||||||
let parent = unsafe { &*parent_ptr };
|
let parent = unsafe { &*parent_ptr };
|
||||||
if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
||||||
return Some(
|
let as_concrete = shared.downcast_ref::<T>()? as *const T;
|
||||||
shared
|
return Some(unsafe { &*as_concrete });
|
||||||
.downcast_ref::<T>()
|
|
||||||
.expect("Context of type T should exist")
|
|
||||||
.clone(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
search_parent = parent.parent;
|
search_parent = parent.parent;
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This method enables the ability to expose state to children further down the [`VirtualDom`] Tree.
|
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Does not require `clone` on the context,
|
||||||
|
/// though we do recommend it.
|
||||||
///
|
///
|
||||||
/// This is a "fundamental" operation and should only be called during initialization of a hook.
|
/// 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_context` and `use_consume_context` instead.
|
/// For a hook that provides the same functionality, use `use_provide_context` and `use_consume_context` instead.
|
||||||
///
|
///
|
||||||
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
|
/// If a state is provided that already exists, the new value will not be inserted. Instead, this method will
|
||||||
/// the context via Rc/Weak.
|
/// return the existing value. This behavior is chosen so shared values do not need to be `Clone`. This particular
|
||||||
|
/// behavior might change in the future.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
@@ -296,12 +304,20 @@ impl ScopeState {
|
|||||||
/// render!(div { "hello {state.0}" })
|
/// render!(div { "hello {state.0}" })
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
pub fn provide_context<T: 'static>(&self, value: T) -> &T {
|
||||||
self.shared_contexts
|
let mut contexts = self.shared_contexts.borrow_mut();
|
||||||
.borrow_mut()
|
|
||||||
.insert(TypeId::of::<T>(), Box::new(value.clone()))
|
let any = match contexts.get(&TypeId::of::<T>()) {
|
||||||
.and_then(|f| f.downcast::<T>().ok());
|
Some(item) => item.downcast_ref::<T>().unwrap() as *const T,
|
||||||
value
|
None => {
|
||||||
|
let boxed = Box::new(value);
|
||||||
|
let boxed_ptr = boxed.as_ref() as *const T;
|
||||||
|
contexts.insert(TypeId::of::<T>(), boxed);
|
||||||
|
boxed_ptr
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe { &*any }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||||
@@ -337,7 +353,7 @@ impl ScopeState {
|
|||||||
self.tasks.remove(id);
|
self.tasks.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take a lazy [`VNode`] structure and actually build it with the context of the efficient [`Bump`] allocator.
|
/// Take a lazy [`crate::VNode`] structure and actually build it with the context of the efficient [`bumpalo::Bump`] allocator.
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
@@ -350,10 +366,126 @@ impl ScopeState {
|
|||||||
/// cx.render(lazy_tree)
|
/// cx.render(lazy_tree)
|
||||||
/// }
|
/// }
|
||||||
///```
|
///```
|
||||||
pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> {
|
pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> {
|
||||||
Ok(rsx.call(self))
|
Ok(rsx.call(self))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a dynamic text node using [`Arguments`] and the [`ScopeState`]'s internal [`Bump`] allocator
|
||||||
|
pub fn text_node(&'src self, args: Arguments) -> DynamicNode<'src> {
|
||||||
|
DynamicNode::Text(VText {
|
||||||
|
value: self.raw_text(args),
|
||||||
|
id: Default::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate some text inside the [`ScopeState`] from [`Arguments`]
|
||||||
|
///
|
||||||
|
/// Uses the currently active [`Bump`] allocator
|
||||||
|
pub fn raw_text(&'src self, args: Arguments) -> &'src str {
|
||||||
|
args.as_str().unwrap_or_else(|| {
|
||||||
|
use bumpalo::core_alloc::fmt::Write;
|
||||||
|
let mut str_buf = bumpalo::collections::String::new_in(self.bump());
|
||||||
|
str_buf.write_fmt(args).unwrap();
|
||||||
|
str_buf.into_bump_str()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] using the internal [`Bump`] allocator
|
||||||
|
pub fn make_node<'c, I>(&'src self, into: impl IntoDynNode<'src, I> + 'c) -> DynamicNode {
|
||||||
|
into.into_vnode(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`Attribute`] from a name, value, namespace, and volatile bool
|
||||||
|
///
|
||||||
|
/// "Volatile" referes to whether or not Dioxus should always override the value. This helps prevent the UI in
|
||||||
|
/// some renderers stay in sync with the VirtualDom's understanding of the world
|
||||||
|
pub fn attr(
|
||||||
|
&'src self,
|
||||||
|
name: &'static str,
|
||||||
|
value: impl IntoAttributeValue<'src>,
|
||||||
|
namespace: Option<&'static str>,
|
||||||
|
volatile: bool,
|
||||||
|
) -> Attribute<'src> {
|
||||||
|
Attribute {
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
volatile,
|
||||||
|
mounted_element: Default::default(),
|
||||||
|
value: value.into_value(self.bump()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`DynamicNode::Component`] variant
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// The given component can be any of four signatures. Remember that an [`Element`] is really a [`Result<VNode>`].
|
||||||
|
///
|
||||||
|
/// ```rust, ignore
|
||||||
|
/// // Without explicit props
|
||||||
|
/// fn(Scope) -> Element;
|
||||||
|
/// async fn(Scope<'_>) -> Element;
|
||||||
|
///
|
||||||
|
/// // With explicit props
|
||||||
|
/// fn(Scope<Props>) -> Element;
|
||||||
|
/// async fn(Scope<Props<'_>>) -> Element;
|
||||||
|
/// ```
|
||||||
|
pub fn component<P, A, F: ComponentReturn<'src, A>>(
|
||||||
|
&'src self,
|
||||||
|
component: fn(Scope<'src, P>) -> F,
|
||||||
|
props: P,
|
||||||
|
fn_name: &'static str,
|
||||||
|
) -> DynamicNode<'src>
|
||||||
|
where
|
||||||
|
P: Properties + 'src,
|
||||||
|
{
|
||||||
|
let vcomp = VProps::new(component, P::memoize, props);
|
||||||
|
|
||||||
|
// cast off the lifetime of the render return
|
||||||
|
let as_dyn: Box<dyn AnyProps<'src> + '_> = Box::new(vcomp);
|
||||||
|
let extended: Box<dyn AnyProps<'src> + 'src> = unsafe { std::mem::transmute(as_dyn) };
|
||||||
|
|
||||||
|
DynamicNode::Component(VComponent {
|
||||||
|
name: fn_name,
|
||||||
|
render_fn: component as *const (),
|
||||||
|
static_props: P::IS_STATIC,
|
||||||
|
props: Cell::new(Some(extended)),
|
||||||
|
scope: Cell::new(None),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`EventHandler`] from an [`FnMut`]
|
||||||
|
pub fn event_handler<T>(&'src self, f: impl FnMut(T) + 'src) -> EventHandler<'src, T> {
|
||||||
|
let handler: &mut dyn FnMut(T) = self.bump().alloc(f);
|
||||||
|
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
|
||||||
|
let callback = RefCell::new(Some(caller));
|
||||||
|
EventHandler { callback }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`AttributeValue`] with the listener variant from a callback
|
||||||
|
///
|
||||||
|
/// The callback must be confined to the lifetime of the ScopeState
|
||||||
|
pub fn listener<T: 'static>(
|
||||||
|
&'src self,
|
||||||
|
mut callback: impl FnMut(Event<T>) + 'src,
|
||||||
|
) -> AttributeValue<'src> {
|
||||||
|
// safety: there's no other way to create a dynamicly-dispatched bump box other than alloc + from-raw
|
||||||
|
// This is the suggested way to build a bumpbox
|
||||||
|
//
|
||||||
|
// In theory, we could just use regular boxes
|
||||||
|
let boxed: BumpBox<'src, dyn FnMut(_) + 'src> = unsafe {
|
||||||
|
BumpBox::from_raw(self.bump().alloc(move |event: Event<dyn Any>| {
|
||||||
|
if let Ok(data) = event.data.downcast::<T>() {
|
||||||
|
callback(Event {
|
||||||
|
propogates: event.propogates,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
AttributeValue::Listener(RefCell::new(Some(boxed)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Store a value between renders. The foundational hook for all other hooks.
|
/// Store a value between renders. The foundational hook for all other hooks.
|
||||||
///
|
///
|
||||||
/// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`.
|
/// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`.
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
any_props::VProps,
|
any_props::VProps,
|
||||||
arena::{ElementId, ElementRef},
|
arena::{ElementId, ElementRef},
|
||||||
factory::RenderReturn,
|
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
|
||||||
innerlude::{DirtyScope, Mutations, Scheduler, SchedulerMsg},
|
|
||||||
mutations::Mutation,
|
mutations::Mutation,
|
||||||
|
nodes::RenderReturn,
|
||||||
nodes::{Template, TemplateId},
|
nodes::{Template, TemplateId},
|
||||||
scheduler::{SuspenseBoundary, SuspenseId},
|
scheduler::SuspenseId,
|
||||||
scopes::{ScopeId, ScopeState},
|
scopes::{ScopeId, ScopeState},
|
||||||
AttributeValue, Element, EventPriority, Scope, SuspenseContext, UiEvent,
|
AttributeValue, Element, Event, Scope, SuspenseContext,
|
||||||
};
|
};
|
||||||
use futures_util::{pin_mut, StreamExt};
|
use futures_util::{pin_mut, StreamExt};
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
@@ -94,9 +94,9 @@ use std::{
|
|||||||
/// ## Building an event loop around Dioxus:
|
/// ## Building an event loop around Dioxus:
|
||||||
///
|
///
|
||||||
/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
|
/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
|
||||||
/// ```rust
|
/// ```rust, ignore
|
||||||
/// fn app(cx: Scope) -> Element {
|
/// fn app(cx: Scope) -> Element {
|
||||||
/// cx.render(rsx!{
|
/// cx.render(rsx! {
|
||||||
/// div { "Hello World" }
|
/// div { "Hello World" }
|
||||||
/// })
|
/// })
|
||||||
/// }
|
/// }
|
||||||
@@ -121,7 +121,7 @@ use std::{
|
|||||||
/// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the
|
/// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the
|
||||||
/// [`VirtualDom::render_with_deadline`] method:
|
/// [`VirtualDom::render_with_deadline`] method:
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust, ignore
|
||||||
/// let dom = VirtualDom::new(app);
|
/// let dom = VirtualDom::new(app);
|
||||||
///
|
///
|
||||||
/// let deadline = tokio::time::sleep(Duration::from_millis(100));
|
/// let deadline = tokio::time::sleep(Duration::from_millis(100));
|
||||||
@@ -134,7 +134,7 @@ use std::{
|
|||||||
/// suggest rendering with a deadline, and then looping between [`VirtualDom::wait_for_work`] and render_immediate until
|
/// suggest rendering with a deadline, and then looping between [`VirtualDom::wait_for_work`] and render_immediate until
|
||||||
/// no suspended work is left.
|
/// no suspended work is left.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```rust, ignore
|
||||||
/// let dom = VirtualDom::new(app);
|
/// let dom = VirtualDom::new(app);
|
||||||
///
|
///
|
||||||
/// let deadline = tokio::time::sleep(Duration::from_millis(20));
|
/// let deadline = tokio::time::sleep(Duration::from_millis(20));
|
||||||
@@ -149,7 +149,7 @@ use std::{
|
|||||||
/// ```
|
/// ```
|
||||||
pub struct VirtualDom {
|
pub struct VirtualDom {
|
||||||
pub(crate) templates: HashMap<TemplateId, Template<'static>>,
|
pub(crate) templates: HashMap<TemplateId, Template<'static>>,
|
||||||
pub(crate) scopes: Slab<ScopeState>,
|
pub(crate) scopes: Slab<Box<ScopeState>>,
|
||||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||||
pub(crate) scheduler: Rc<Scheduler>,
|
pub(crate) scheduler: Rc<Scheduler>,
|
||||||
|
|
||||||
@@ -224,10 +224,7 @@ impl VirtualDom {
|
|||||||
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
|
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
|
||||||
/// let mutations = dom.rebuild();
|
/// let mutations = dom.rebuild();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new_with_props<P>(root: fn(Scope<P>) -> Element, root_props: P) -> Self
|
pub fn new_with_props<P: 'static>(root: fn(Scope<P>) -> Element, root_props: P) -> Self {
|
||||||
where
|
|
||||||
P: 'static,
|
|
||||||
{
|
|
||||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||||
let mut dom = Self {
|
let mut dom = Self {
|
||||||
rx,
|
rx,
|
||||||
@@ -239,20 +236,23 @@ impl VirtualDom {
|
|||||||
dirty_scopes: BTreeSet::new(),
|
dirty_scopes: BTreeSet::new(),
|
||||||
collected_leaves: Vec::new(),
|
collected_leaves: Vec::new(),
|
||||||
finished_fibers: Vec::new(),
|
finished_fibers: Vec::new(),
|
||||||
mutations: Mutations::new(),
|
mutations: Mutations::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let root = dom.new_scope(Box::leak(Box::new(VProps::new(
|
let root = dom.new_scope(Box::new(VProps::new(
|
||||||
root,
|
root,
|
||||||
|_, _| unreachable!(),
|
|_, _| unreachable!(),
|
||||||
root_props,
|
root_props,
|
||||||
))));
|
)));
|
||||||
|
|
||||||
// The root component is always a suspense boundary for any async children
|
// The root component is always a suspense boundary for any async children
|
||||||
// This could be unexpected, so we might rethink this behavior later
|
// This could be unexpected, so we might rethink this behavior later
|
||||||
//
|
//
|
||||||
// We *could* just panic if the suspense boundary is not found
|
// We *could* just panic if the suspense boundary is not found
|
||||||
root.provide_context(Rc::new(SuspenseBoundary::new(ScopeId(0))));
|
root.provide_context(SuspenseContext::new(ScopeId(0)));
|
||||||
|
|
||||||
|
// Unlike react, we provide a default error boundary that just renders the error as a string
|
||||||
|
root.provide_context(ErrorBoundary::new(ScopeId(0)));
|
||||||
|
|
||||||
// the root element is always given element ID 0 since it's the container for the entire tree
|
// the root element is always given element ID 0 since it's the container for the entire tree
|
||||||
dom.elements.insert(ElementRef::null());
|
dom.elements.insert(ElementRef::null());
|
||||||
@@ -264,7 +264,7 @@ impl VirtualDom {
|
|||||||
///
|
///
|
||||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||||
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||||
self.scopes.get(id.0)
|
self.scopes.get(id.0).map(|f| f.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the single scope at the top of the VirtualDom tree that will always be around
|
/// Get the single scope at the top of the VirtualDom tree that will always be around
|
||||||
@@ -285,7 +285,7 @@ impl VirtualDom {
|
|||||||
/// Manually mark a scope as requiring a re-render
|
/// Manually mark a scope as requiring a re-render
|
||||||
///
|
///
|
||||||
/// Whenever the VirtualDom "works", it will re-render this scope
|
/// Whenever the VirtualDom "works", it will re-render this scope
|
||||||
pub fn mark_dirty_scope(&mut self, id: ScopeId) {
|
pub fn mark_dirty(&mut self, id: ScopeId) {
|
||||||
let height = self.scopes[id.0].height;
|
let height = self.scopes[id.0].height;
|
||||||
self.dirty_scopes.insert(DirtyScope { height, id });
|
self.dirty_scopes.insert(DirtyScope { height, id });
|
||||||
}
|
}
|
||||||
@@ -324,7 +324,6 @@ impl VirtualDom {
|
|||||||
data: Rc<dyn Any>,
|
data: Rc<dyn Any>,
|
||||||
element: ElementId,
|
element: ElementId,
|
||||||
bubbles: bool,
|
bubbles: bool,
|
||||||
_priority: EventPriority,
|
|
||||||
) {
|
) {
|
||||||
/*
|
/*
|
||||||
------------------------
|
------------------------
|
||||||
@@ -351,8 +350,8 @@ impl VirtualDom {
|
|||||||
let mut listeners = vec![];
|
let mut listeners = vec![];
|
||||||
|
|
||||||
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
|
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
|
||||||
let uievent = UiEvent {
|
let uievent = Event {
|
||||||
bubbles: Rc::new(Cell::new(bubbles)),
|
propogates: Rc::new(Cell::new(bubbles)),
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -363,15 +362,13 @@ impl VirtualDom {
|
|||||||
let target_path = el_ref.path;
|
let target_path = el_ref.path;
|
||||||
|
|
||||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||||
fn is_path_ascendant(small: &[u8], big: &[u8]) -> bool {
|
println!("{:?} \n {:?} \n {:?}", attr, name, element);
|
||||||
small.len() >= big.len() && small == &big[..small.len()]
|
|
||||||
}
|
|
||||||
|
|
||||||
let this_path = template.template.attr_paths[idx];
|
let this_path = template.template.attr_paths[idx];
|
||||||
|
|
||||||
// listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing
|
// listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing
|
||||||
// we should fix this so that we look for "onclick" instead of "click"
|
// we should fix this so that we look for "onclick" instead of "click"
|
||||||
if &attr.name[2..] == name && is_path_ascendant(&target_path, &this_path) {
|
if &attr.name[2..] == name && target_path.is_ascendant(&this_path) {
|
||||||
listeners.push(&attr.value);
|
listeners.push(&attr.value);
|
||||||
|
|
||||||
// Break if the event doesn't bubble anyways
|
// Break if the event doesn't bubble anyways
|
||||||
@@ -382,18 +379,23 @@ impl VirtualDom {
|
|||||||
// Break if this is the exact target element.
|
// Break if this is the exact target element.
|
||||||
// This means we won't call two listeners with the same name on the same element. This should be
|
// This means we won't call two listeners with the same name on the same element. This should be
|
||||||
// documented, or be rejected from the rsx! macro outright
|
// documented, or be rejected from the rsx! macro outright
|
||||||
if this_path == target_path {
|
if target_path == this_path {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("calling listeners: {:?}", listeners);
|
||||||
|
|
||||||
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
|
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
|
||||||
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
||||||
for listener in listeners.drain(..).rev() {
|
for listener in listeners.drain(..).rev() {
|
||||||
if let AttributeValue::Listener(listener) = listener {
|
if let AttributeValue::Listener(listener) = listener {
|
||||||
listener.borrow_mut()(uievent.clone());
|
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||||
if !uievent.bubbles.get() {
|
cb(uievent.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !uievent.propogates.get() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,6 +403,8 @@ impl VirtualDom {
|
|||||||
|
|
||||||
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
|
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("all listeners exhausted");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wait for the scheduler to have any work.
|
/// Wait for the scheduler to have any work.
|
||||||
@@ -426,7 +430,7 @@ impl VirtualDom {
|
|||||||
match some_msg.take() {
|
match some_msg.take() {
|
||||||
// If a bunch of messages are ready in a sequence, try to pop them off synchronously
|
// If a bunch of messages are ready in a sequence, try to pop them off synchronously
|
||||||
Some(msg) => match msg {
|
Some(msg) => match msg {
|
||||||
SchedulerMsg::Immediate(id) => self.mark_dirty_scope(id),
|
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
||||||
SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
|
SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
|
||||||
SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
|
SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
|
||||||
},
|
},
|
||||||
@@ -450,12 +454,15 @@ impl VirtualDom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Swap the current mutations with a new
|
/// Process all events in the queue until there are no more left
|
||||||
fn finalize<'a>(&'a mut self) -> Mutations<'a> {
|
pub fn process_events(&mut self) {
|
||||||
// todo: make this a routine
|
while let Ok(Some(msg)) = self.rx.try_next() {
|
||||||
let mut out = Mutations::new();
|
match msg {
|
||||||
std::mem::swap(&mut self.mutations, &mut out);
|
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
||||||
out
|
SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
|
||||||
|
SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
|
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
|
||||||
@@ -478,12 +485,15 @@ impl VirtualDom {
|
|||||||
///
|
///
|
||||||
/// apply_edits(edits);
|
/// apply_edits(edits);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn rebuild<'a>(&'a mut self) -> Mutations<'a> {
|
pub fn rebuild(&mut self) -> Mutations {
|
||||||
match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
|
match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
|
||||||
// Rebuilding implies we append the created elements to the root
|
// Rebuilding implies we append the created elements to the root
|
||||||
RenderReturn::Sync(Ok(node)) => {
|
RenderReturn::Sync(Ok(node)) => {
|
||||||
let m = self.create_scope(ScopeId(0), node);
|
let m = self.create_scope(ScopeId(0), node);
|
||||||
self.mutations.push(Mutation::AppendChildren { m });
|
self.mutations.edits.push(Mutation::AppendChildren {
|
||||||
|
id: ElementId(0),
|
||||||
|
m,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// If an error occurs, we should try to render the default error component and context where the error occured
|
// If an error occurs, we should try to render the default error component and context where the error occured
|
||||||
RenderReturn::Sync(Err(e)) => panic!("Cannot catch errors during rebuild {:?}", e),
|
RenderReturn::Sync(Err(e)) => panic!("Cannot catch errors during rebuild {:?}", e),
|
||||||
@@ -516,10 +526,8 @@ impl VirtualDom {
|
|||||||
/// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues.
|
/// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues.
|
||||||
///
|
///
|
||||||
/// If no suspense trees are present
|
/// If no suspense trees are present
|
||||||
pub async fn render_with_deadline<'a>(
|
pub async fn render_with_deadline(&mut self, deadline: impl Future<Output = ()>) -> Mutations {
|
||||||
&'a mut self,
|
println!("render with deadline");
|
||||||
deadline: impl Future<Output = ()>,
|
|
||||||
) -> Mutations<'a> {
|
|
||||||
pin_mut!(deadline);
|
pin_mut!(deadline);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -529,9 +537,12 @@ impl VirtualDom {
|
|||||||
let context = scope.has_context::<SuspenseContext>().unwrap();
|
let context = scope.has_context::<SuspenseContext>().unwrap();
|
||||||
|
|
||||||
self.mutations
|
self.mutations
|
||||||
.extend(context.mutations.borrow_mut().template_mutations.drain(..));
|
.templates
|
||||||
|
.extend(context.mutations.borrow_mut().templates.drain(..));
|
||||||
|
|
||||||
self.mutations
|
self.mutations
|
||||||
.extend(context.mutations.borrow_mut().drain(..));
|
.edits
|
||||||
|
.extend(context.mutations.borrow_mut().edits.drain(..));
|
||||||
|
|
||||||
// TODO: count how many nodes are on the stack?
|
// TODO: count how many nodes are on the stack?
|
||||||
self.mutations.push(Mutation::ReplaceWith {
|
self.mutations.push(Mutation::ReplaceWith {
|
||||||
@@ -551,7 +562,7 @@ impl VirtualDom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save the current mutations length so we can split them into boundary
|
// Save the current mutations length so we can split them into boundary
|
||||||
let mutations_to_this_point = self.mutations.len();
|
let mutations_to_this_point = self.mutations.edits.len();
|
||||||
|
|
||||||
// Run the scope and get the mutations
|
// Run the scope and get the mutations
|
||||||
self.run_scope(dirty.id);
|
self.run_scope(dirty.id);
|
||||||
@@ -570,7 +581,8 @@ impl VirtualDom {
|
|||||||
boundary_mut
|
boundary_mut
|
||||||
.mutations
|
.mutations
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.extend(self.mutations.split_off(mutations_to_this_point));
|
.edits
|
||||||
|
.extend(self.mutations.edits.split_off(mutations_to_this_point));
|
||||||
|
|
||||||
// Attach suspended leaves
|
// Attach suspended leaves
|
||||||
boundary
|
boundary
|
||||||
@@ -605,10 +617,19 @@ impl VirtualDom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Swap the current mutations with a new
|
||||||
|
fn finalize(&mut self) -> Mutations {
|
||||||
|
// todo: make this a routine
|
||||||
|
let mut out = Mutations::default();
|
||||||
|
std::mem::swap(&mut self.mutations, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for VirtualDom {
|
impl Drop for VirtualDom {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// self.drop_scope(ScopeId(0));
|
// Simply drop this scope which drops all of its children
|
||||||
|
self.drop_scope(ScopeId(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
# Testing of Dioxus core
|
# Testing of Dioxus core
|
||||||
|
|
||||||
|
|
||||||
NodeFactory
|
|
||||||
- [] rsx, html, NodeFactory generate the same structures
|
|
||||||
|
|
||||||
Diffing
|
Diffing
|
||||||
- [x] create elements
|
- [x] create elements
|
||||||
- [x] create text
|
- [x] create text
|
||||||
@@ -19,15 +16,13 @@ Diffing
|
|||||||
- [x] keyed diffing
|
- [x] keyed diffing
|
||||||
- [x] keyed diffing out of order
|
- [x] keyed diffing out of order
|
||||||
- [x] keyed diffing with prefix/suffix
|
- [x] keyed diffing with prefix/suffix
|
||||||
- [x] suspended nodes work
|
- [x] suspended nodes work
|
||||||
|
|
||||||
Lifecycle
|
Lifecycle
|
||||||
- [] Components mount properly
|
- [] Components mount properly
|
||||||
- [] Components create new child components
|
- [] Components create new child components
|
||||||
- [] Replaced components unmount old components and mount new
|
- [] Replaced components unmount old components and mount new
|
||||||
- [] Post-render effects are called
|
- [] Post-render effects are called
|
||||||
- []
|
|
||||||
|
|
||||||
|
|
||||||
Shared Context
|
Shared Context
|
||||||
- [] Shared context propagates downwards
|
- [] Shared context propagates downwards
|
||||||
@@ -37,7 +32,7 @@ Suspense
|
|||||||
- [] use_suspense generates suspended nodes
|
- [] use_suspense generates suspended nodes
|
||||||
|
|
||||||
|
|
||||||
Hooks
|
Hooks
|
||||||
- [] Drop order is maintained
|
- [] Drop order is maintained
|
||||||
- [] Shared hook state is okay
|
- [] Shared hook state is okay
|
||||||
- [] use_hook works
|
- [] use_hook works
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! dynamic attributes in dioxus necessitate an allocated node ID.
|
||||||
|
//!
|
||||||
|
//! This tests to ensure we clean it up
|
||||||
|
|
||||||
|
use dioxus::core::{ElementId, Mutation::*};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attrs_cycle() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let id = cx.generation();
|
||||||
|
match cx.generation() % 2 {
|
||||||
|
0 => cx.render(rsx! {
|
||||||
|
div {}
|
||||||
|
}),
|
||||||
|
1 => cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
h1 { class: "{id}", id: "{id}" }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dom.rebuild().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||||
|
AppendChildren { m: 1, id: ElementId(0) },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||||
|
AssignId { path: &[0,], id: ElementId(3,) },
|
||||||
|
SetAttribute { name: "class", value: "1", id: ElementId(3,), ns: None },
|
||||||
|
SetAttribute { name: "id", value: "1", id: ElementId(3,), ns: None },
|
||||||
|
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||||
|
ReplaceWith { id: ElementId(2), m: 1 }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||||
|
AssignId { path: &[0], id: ElementId(1) },
|
||||||
|
SetAttribute { name: "class", value: "3", id: ElementId(1), ns: None },
|
||||||
|
SetAttribute { name: "id", value: "3", id: ElementId(1), ns: None },
|
||||||
|
ReplaceWith { id: ElementId(3), m: 1 }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// we take the node taken by attributes since we reused it
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
|
ReplaceWith { id: ElementId(2), m: 1 }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ fn bool_test() {
|
|||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
SetBoolAttribute { name: "hidden", value: false, id: ElementId(1,) },
|
SetBoolAttribute { name: "hidden", value: false, id: ElementId(1,) },
|
||||||
AppendChildren { m: 1 },
|
AppendChildren { m: 1, id: ElementId(0) },
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ fn test_borrowed_state() {
|
|||||||
HydrateText { path: &[0,], value: "Hello w1!", id: ElementId(4,) },
|
HydrateText { path: &[0,], value: "Hello w1!", id: ElementId(4,) },
|
||||||
ReplacePlaceholder { path: &[1,], m: 1 },
|
ReplacePlaceholder { path: &[1,], m: 1 },
|
||||||
ReplacePlaceholder { path: &[0,], m: 1 },
|
ReplacePlaceholder { path: &[0,], m: 1 },
|
||||||
AppendChildren { m: 1 },
|
AppendChildren { m: 1, id: ElementId(0) },
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,28 @@
|
|||||||
//! we should properly bubble up errors from components
|
//! we should properly bubble up errors from components
|
||||||
|
|
||||||
use std::{error::Error as StdError, marker::PhantomData, string::ParseError};
|
|
||||||
|
|
||||||
use anyhow::{anyhow, bail};
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
// todo: add these to dioxus
|
|
||||||
pub trait Reject<E: Clone>: Sized {
|
|
||||||
fn reject_err(self, t: impl FnOnce(E) -> anyhow::Error) -> Result<Self, anyhow::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn reject_because(self, t: impl Into<String>) -> Result<Self, anyhow::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reject(self) -> Result<Self, anyhow::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E: Clone> Reject<E> for &Result<T, E> {
|
|
||||||
fn reject_err(self, t: impl FnOnce(E) -> anyhow::Error) -> Result<Self, anyhow::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call "clone" on the underlying error so it can be propogated out
|
|
||||||
pub trait CloneErr<T, E: ToOwned> {
|
|
||||||
fn clone_err(&self) -> Result<&T, E::Owned>
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: ToOwned, T> CloneErr<T, E> for Result<T, E> {
|
|
||||||
fn clone_err(&self) -> Result<&T, E::Owned>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Ok(s) => Ok(s),
|
|
||||||
Err(e) => Err(e.to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
// propgates error upwards, does not give a reason, lets Dioxus figure it out
|
let raw = match cx.generation() % 2 {
|
||||||
let value = cx.use_hook(|| "123123123.123".parse::<f32>()).reject()?;
|
0 => "123.123",
|
||||||
|
1 => "123.123.123",
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
// propgates error upwards, gives a reason
|
let value = raw.parse::<f32>()?;
|
||||||
let value = cx
|
|
||||||
.use_hook(|| "123123123.123".parse::<f32>())
|
|
||||||
.reject_because("Parsing float failed")?;
|
|
||||||
|
|
||||||
let value = cx.use_hook(|| "123123123.123".parse::<f32>()).clone_err()?;
|
cx.render(rsx! {
|
||||||
|
div { "hello {value}" }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let value = cx
|
#[test]
|
||||||
.use_hook(|| "123123123.123".parse::<f32>())
|
fn it_goes() {
|
||||||
.as_ref()
|
let mut dom = VirtualDom::new(app);
|
||||||
.map_err(Clone::clone)?;
|
|
||||||
|
|
||||||
let value = cx
|
let edits = dom.rebuild().santize();
|
||||||
.use_hook(|| "123123123.123".parse::<f32>())
|
|
||||||
.as_ref()
|
|
||||||
.map_err(|_| anyhow!("Parsing float failed"))?;
|
|
||||||
|
|
||||||
todo!()
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
|
||||||
|
dom.render_immediate();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use dioxus::core::{ElementId, Mutation::*};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_shares() {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.provide_context(cx.generation() as i32);
|
||||||
|
|
||||||
|
cx.render(rsx!(child_1 {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child_1(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx!(child_2 {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child_2(cx: Scope) -> Element {
|
||||||
|
let value = cx.consume_context::<i32>().unwrap();
|
||||||
|
cx.render(rsx!("Value is {value}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
assert_eq!(
|
||||||
|
dom.rebuild().santize().edits,
|
||||||
|
[
|
||||||
|
CreateTextNode { value: "Value is 0", id: ElementId(1,) },
|
||||||
|
AppendChildren { m: 1, id: ElementId(0) },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 1);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 2);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(2));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[SetText { value: "Value is 2", id: ElementId(1,) },]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
dom.mark_dirty(ScopeId(2));
|
||||||
|
let edits = dom.render_immediate();
|
||||||
|
assert_eq!(
|
||||||
|
edits.santize().edits,
|
||||||
|
[SetText { value: "Value is 3", id: ElementId(1,) },]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,25 +22,13 @@ fn test_original_diff() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let edits = dom.rebuild().santize();
|
let edits = dom.rebuild().santize();
|
||||||
assert_eq!(
|
|
||||||
edits.template_mutations,
|
|
||||||
[
|
|
||||||
// create template
|
|
||||||
CreateElement { name: "div" },
|
|
||||||
CreateElement { name: "div" },
|
|
||||||
CreateStaticText { value: "Hello, world!" },
|
|
||||||
AppendChildren { m: 1 },
|
|
||||||
AppendChildren { m: 1 },
|
|
||||||
SaveTemplate { name: "template", m: 1 },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edits.edits,
|
edits.edits,
|
||||||
[
|
[
|
||||||
// add to root
|
// add to root
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
AppendChildren { m: 1 }
|
AppendChildren { m: 1, id: ElementId(0) }
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -65,28 +53,31 @@ fn create() {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let edits = dom.rebuild().santize();
|
let _edits = dom.rebuild().santize();
|
||||||
assert_eq!(
|
|
||||||
edits.template_mutations,
|
// todo: we don't test template mutations anymore since the templates are passed along
|
||||||
[
|
|
||||||
// create template
|
// assert_eq!(
|
||||||
CreateElement { name: "div" },
|
// edits.templates,
|
||||||
CreateElement { name: "div" },
|
// [
|
||||||
CreateStaticText { value: "Hello, world!" },
|
// // create template
|
||||||
CreateElement { name: "div" },
|
// CreateElement { name: "div" },
|
||||||
CreateElement { name: "div" },
|
// CreateElement { name: "div" },
|
||||||
CreateStaticPlaceholder {},
|
// CreateStaticText { value: "Hello, world!" },
|
||||||
AppendChildren { m: 1 },
|
// CreateElement { name: "div" },
|
||||||
AppendChildren { m: 1 },
|
// CreateElement { name: "div" },
|
||||||
AppendChildren { m: 2 },
|
// CreateStaticPlaceholder {},
|
||||||
AppendChildren { m: 1 },
|
// AppendChildren { m: 1 },
|
||||||
SaveTemplate { name: "template", m: 1 },
|
// AppendChildren { m: 1 },
|
||||||
// The fragment child template
|
// AppendChildren { m: 2 },
|
||||||
CreateStaticText { value: "hello" },
|
// AppendChildren { m: 1 },
|
||||||
CreateStaticText { value: "world" },
|
// SaveTemplate { name: "template", m: 1 },
|
||||||
SaveTemplate { name: "template", m: 2 },
|
// // The fragment child template
|
||||||
]
|
// CreateStaticText { value: "hello" },
|
||||||
);
|
// CreateStaticText { value: "world" },
|
||||||
|
// SaveTemplate { name: "template", m: 2 },
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -97,17 +88,19 @@ fn create_list() {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let edits = dom.rebuild().santize();
|
let _edits = dom.rebuild().santize();
|
||||||
assert_eq!(
|
|
||||||
edits.template_mutations,
|
// note: we dont test template edits anymore
|
||||||
[
|
// assert_eq!(
|
||||||
// create template
|
// edits.templates,
|
||||||
CreateElement { name: "div" },
|
// [
|
||||||
CreateStaticText { value: "hello" },
|
// // create template
|
||||||
AppendChildren { m: 1 },
|
// CreateElement { name: "div" },
|
||||||
SaveTemplate { name: "template", m: 1 }
|
// CreateStaticText { value: "hello" },
|
||||||
]
|
// AppendChildren { m: 1 },
|
||||||
);
|
// SaveTemplate { name: "template", m: 1 }
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -122,18 +115,20 @@ fn create_simple() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let edits = dom.rebuild().santize();
|
let edits = dom.rebuild().santize();
|
||||||
assert_eq!(
|
|
||||||
edits.template_mutations,
|
// note: we dont test template edits anymore
|
||||||
[
|
// assert_eq!(
|
||||||
// create template
|
// edits.templates,
|
||||||
CreateElement { name: "div" },
|
// [
|
||||||
CreateElement { name: "div" },
|
// // create template
|
||||||
CreateElement { name: "div" },
|
// CreateElement { name: "div" },
|
||||||
CreateElement { name: "div" },
|
// CreateElement { name: "div" },
|
||||||
// add to root
|
// CreateElement { name: "div" },
|
||||||
SaveTemplate { name: "template", m: 4 }
|
// CreateElement { name: "div" },
|
||||||
]
|
// // add to root
|
||||||
);
|
// SaveTemplate { name: "template", m: 4 }
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn create_components() {
|
fn create_components() {
|
||||||
@@ -158,26 +153,9 @@ fn create_components() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let edits = dom.rebuild().santize();
|
let _edits = dom.rebuild().santize();
|
||||||
assert_eq!(
|
|
||||||
edits.template_mutations,
|
// todo: test this
|
||||||
[
|
|
||||||
// The "child" template
|
|
||||||
CreateElement { name: "h1" },
|
|
||||||
CreateElement { name: "div" },
|
|
||||||
CreateStaticPlaceholder {},
|
|
||||||
AppendChildren { m: 1 },
|
|
||||||
CreateElement { name: "p" },
|
|
||||||
SaveTemplate { name: "template", m: 3 },
|
|
||||||
// Sub template for component children
|
|
||||||
CreateStaticText { value: "abc1" },
|
|
||||||
SaveTemplate { name: "template", m: 1 },
|
|
||||||
CreateStaticText { value: "abc2" },
|
|
||||||
SaveTemplate { name: "template", m: 1 },
|
|
||||||
CreateStaticText { value: "abc3" },
|
|
||||||
SaveTemplate { name: "template", m: 1 }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -195,23 +173,25 @@ fn anchors() {
|
|||||||
|
|
||||||
// note that the template under "false" doesn't show up since it's not loaded
|
// note that the template under "false" doesn't show up since it's not loaded
|
||||||
let edits = dom.rebuild().santize();
|
let edits = dom.rebuild().santize();
|
||||||
assert_eq!(
|
|
||||||
edits.template_mutations,
|
// note: we dont test template edits anymore
|
||||||
[
|
// assert_eq!(
|
||||||
// create each template
|
// edits.templates,
|
||||||
CreateElement { name: "div" },
|
// [
|
||||||
CreateStaticText { value: "hello" },
|
// // create each template
|
||||||
AppendChildren { m: 1 },
|
// CreateElement { name: "div" },
|
||||||
SaveTemplate { m: 1, name: "template" },
|
// CreateStaticText { value: "hello" },
|
||||||
]
|
// AppendChildren { m: 1 },
|
||||||
);
|
// SaveTemplate { m: 1, name: "template" },
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edits.edits,
|
edits.edits,
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
CreatePlaceholder { id: ElementId(2) },
|
CreatePlaceholder { id: ElementId(2) },
|
||||||
AppendChildren { m: 2 }
|
AppendChildren { m: 2, id: ElementId(0) }
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// use dioxus::core::Mutation::*;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiroot() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div { "Hello a" }
|
||||||
|
div { "Hello b" }
|
||||||
|
div { "Hello c" }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// note: we dont test template edits anymore
|
||||||
|
let _templates = dom.rebuild().santize().templates;
|
||||||
|
|
||||||
|
// assert_eq!(
|
||||||
|
// dom.rebuild().santize().templates,
|
||||||
|
// [
|
||||||
|
// CreateElement { name: "div" },
|
||||||
|
// CreateStaticText { value: "Hello a" },
|
||||||
|
// AppendChildren { m: 1 },
|
||||||
|
// CreateElement { name: "div" },
|
||||||
|
// CreateStaticText { value: "Hello b" },
|
||||||
|
// AppendChildren { m: 1 },
|
||||||
|
// CreateElement { name: "div" },
|
||||||
|
// CreateStaticText { value: "Hello c" },
|
||||||
|
// AppendChildren { m: 1 },
|
||||||
|
// SaveTemplate { name: "template", m: 3 }
|
||||||
|
// ]
|
||||||
|
// )
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@ fn empty_fragment_creates_nothing() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
edits.edits,
|
edits.edits,
|
||||||
[
|
[
|
||||||
CreatePlaceholder { id: ElementId(2) },
|
CreatePlaceholder { id: ElementId(1) },
|
||||||
AppendChildren { m: 1 }
|
AppendChildren { id: ElementId(0), m: 1 }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ fn root_fragments_work() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.rebuild().edits.last().unwrap(),
|
vdom.rebuild().edits.last().unwrap(),
|
||||||
&AppendChildren { m: 2 }
|
&AppendChildren { id: ElementId(0), m: 2 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ fn fragments_nested() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.rebuild().edits.last().unwrap(),
|
vdom.rebuild().edits.last().unwrap(),
|
||||||
&AppendChildren { m: 8 }
|
&AppendChildren { id: ElementId(0), m: 8 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ fn fragments_across_components() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
VirtualDom::new(app).rebuild().edits.last().unwrap(),
|
VirtualDom::new(app).rebuild().edits.last().unwrap(),
|
||||||
&AppendChildren { m: 8 }
|
&AppendChildren { id: ElementId(0), m: 8 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +99,6 @@ fn list_fragments() {
|
|||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
VirtualDom::new(app).rebuild().edits.last().unwrap(),
|
VirtualDom::new(app).rebuild().edits.last().unwrap(),
|
||||||
&AppendChildren { m: 7 }
|
&AppendChildren { id: ElementId(0), m: 7 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -27,28 +27,29 @@ fn list_renders() {
|
|||||||
|
|
||||||
let edits = dom.rebuild().santize();
|
let edits = dom.rebuild().santize();
|
||||||
|
|
||||||
assert_eq!(
|
// note: we dont test template edits anymore
|
||||||
edits.template_mutations,
|
// assert_eq!(
|
||||||
[
|
// edits.templates,
|
||||||
// Create the outer div
|
// [
|
||||||
CreateElement { name: "div" },
|
// // Create the outer div
|
||||||
// todo: since this is the only child, we should just use
|
// CreateElement { name: "div" },
|
||||||
// append when modify the values (IE no need for a placeholder)
|
// // todo: since this is the only child, we should just use
|
||||||
CreatePlaceholder { id: ElementId(0) },
|
// // append when modify the values (IE no need for a placeholder)
|
||||||
AppendChildren { m: 1 },
|
// CreateStaticPlaceholder,
|
||||||
SaveTemplate { name: "template", m: 1 },
|
// AppendChildren { m: 1 },
|
||||||
// Create the inner template div
|
// SaveTemplate { name: "template", m: 1 },
|
||||||
CreateElement { name: "div" },
|
// // Create the inner template div
|
||||||
CreateElement { name: "h1" },
|
// CreateElement { name: "div" },
|
||||||
CreateStaticText { value: "hello world! " },
|
// CreateElement { name: "h1" },
|
||||||
AppendChildren { m: 1 },
|
// CreateStaticText { value: "hello world! " },
|
||||||
CreateElement { name: "p" },
|
// AppendChildren { m: 1 },
|
||||||
CreateStaticText { value: "d" },
|
// CreateElement { name: "p" },
|
||||||
AppendChildren { m: 1 },
|
// CreateTextPlaceholder,
|
||||||
AppendChildren { m: 2 },
|
// AppendChildren { m: 1 },
|
||||||
SaveTemplate { name: "template", m: 1 }
|
// AppendChildren { m: 2 },
|
||||||
],
|
// SaveTemplate { name: "template", m: 1 }
|
||||||
);
|
// ],
|
||||||
|
// );
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edits.edits,
|
edits.edits,
|
||||||
@@ -65,7 +66,7 @@ fn list_renders() {
|
|||||||
// Replace the 0th childn on the div with the 3 templates on the stack
|
// Replace the 0th childn on the div with the 3 templates on the stack
|
||||||
ReplacePlaceholder { m: 3, path: &[0] },
|
ReplacePlaceholder { m: 3, path: &[0] },
|
||||||
// Append the container div to the dom
|
// Append the container div to the dom
|
||||||
AppendChildren { m: 1 }
|
AppendChildren { m: 1, id: ElementId(0) }
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ fn nested_passthru_creates() {
|
|||||||
edits.edits,
|
edits.edits,
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
AppendChildren { m: 1 },
|
AppendChildren { m: 1, id: ElementId(0) },
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ fn nested_passthru_creates_add() {
|
|||||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||||
// load div that contains 4
|
// load div that contains 4
|
||||||
LoadTemplate { name: "template", index: 1, id: ElementId(4) },
|
LoadTemplate { name: "template", index: 1, id: ElementId(4) },
|
||||||
AppendChildren { m: 4 },
|
AppendChildren { id: ElementId(0), m: 4 },
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ fn dynamic_node_as_root() {
|
|||||||
let edits = dom.rebuild().santize();
|
let edits = dom.rebuild().santize();
|
||||||
|
|
||||||
// Since the roots were all dynamic, they should not cause any template muations
|
// Since the roots were all dynamic, they should not cause any template muations
|
||||||
assert_eq!(edits.template_mutations, []);
|
assert!(edits.templates.is_empty());
|
||||||
|
|
||||||
// The root node is text, so we just create it on the spot
|
// The root node is text, so we just create it on the spot
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -100,7 +100,7 @@ fn dynamic_node_as_root() {
|
|||||||
[
|
[
|
||||||
CreateTextNode { value: "123", id: ElementId(1) },
|
CreateTextNode { value: "123", id: ElementId(1) },
|
||||||
CreateTextNode { value: "456", id: ElementId(2) },
|
CreateTextNode { value: "456", id: ElementId(2) },
|
||||||
AppendChildren { m: 2 }
|
AppendChildren { id: ElementId(0), m: 2 }
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use dioxus::core::{ElementId, Mutation::*};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
/// As we clean up old templates, the ID for the node should cycle
|
||||||
|
#[test]
|
||||||
|
fn cycling_elements() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
cx.render(match cx.generation() % 2 {
|
||||||
|
0 => rsx! { div { "wasd" } },
|
||||||
|
1 => rsx! { div { "abcd" } },
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let edits = dom.rebuild().santize();
|
||||||
|
assert_eq!(
|
||||||
|
edits.edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||||
|
AppendChildren { m: 1, id: ElementId(0) },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||||
|
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||||
|
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||||
|
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
use dioxus::core::{ElementId, Mutation::*};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
/// When returning sets of components, we do a light diff of the contents to preserve some react-like functionality
|
||||||
|
///
|
||||||
|
/// This means that nav_bar should never get re-created and that we should only be swapping out
|
||||||
|
/// different pointers
|
||||||
|
#[test]
|
||||||
|
fn component_swap() {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let render_phase = cx.use_hook(|| 0);
|
||||||
|
|
||||||
|
*render_phase += 1;
|
||||||
|
|
||||||
|
cx.render(match *render_phase {
|
||||||
|
0 => rsx! {
|
||||||
|
nav_bar {}
|
||||||
|
dash_board {}
|
||||||
|
},
|
||||||
|
1 => rsx! {
|
||||||
|
nav_bar {}
|
||||||
|
dash_results {}
|
||||||
|
},
|
||||||
|
2 => rsx! {
|
||||||
|
nav_bar {}
|
||||||
|
dash_board {}
|
||||||
|
},
|
||||||
|
3 => rsx! {
|
||||||
|
nav_bar {}
|
||||||
|
dash_results {}
|
||||||
|
},
|
||||||
|
4 => rsx! {
|
||||||
|
nav_bar {}
|
||||||
|
dash_board {}
|
||||||
|
},
|
||||||
|
_ => rsx!("blah"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nav_bar(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
h1 {
|
||||||
|
"NavBar"
|
||||||
|
(0..3).map(|_| rsx!(nav_link {}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nav_link(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx!( h1 { "nav_link" } ))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dash_board(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx!( div { "dashboard" } ))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dash_results(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx!( div { "results" } ))
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
let edits = dom.rebuild().santize();
|
||||||
|
assert_eq!(
|
||||||
|
edits.edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||||
|
ReplacePlaceholder { path: &[1], m: 3 },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||||
|
AppendChildren { m: 2, id: ElementId(0) }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||||
|
ReplaceWith { id: ElementId(5), m: 1 }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||||
|
ReplaceWith { id: ElementId(6), m: 1 }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||||
|
ReplaceWith { id: ElementId(5), m: 1 }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,21 +10,21 @@ fn text_diff() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut vdom = VirtualDom::new(app);
|
let mut vdom = VirtualDom::new(app);
|
||||||
vdom.rebuild();
|
_ = vdom.rebuild();
|
||||||
|
|
||||||
vdom.mark_dirty_scope(ScopeId(0));
|
vdom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate().edits,
|
vdom.render_immediate().edits,
|
||||||
[SetText { value: "hello 1", id: ElementId(2) }]
|
[SetText { value: "hello 1", id: ElementId(2) }]
|
||||||
);
|
);
|
||||||
|
|
||||||
vdom.mark_dirty_scope(ScopeId(0));
|
vdom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate().edits,
|
vdom.render_immediate().edits,
|
||||||
[SetText { value: "hello 2", id: ElementId(2) }]
|
[SetText { value: "hello 2", id: ElementId(2) }]
|
||||||
);
|
);
|
||||||
|
|
||||||
vdom.mark_dirty_scope(ScopeId(0));
|
vdom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate().edits,
|
vdom.render_immediate().edits,
|
||||||
[SetText { value: "hello 3", id: ElementId(2) }]
|
[SetText { value: "hello 3", id: ElementId(2) }]
|
||||||
@@ -44,17 +44,41 @@ fn element_swap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut vdom = VirtualDom::new(app);
|
let mut vdom = VirtualDom::new(app);
|
||||||
vdom.rebuild();
|
_ = vdom.rebuild();
|
||||||
|
|
||||||
vdom.mark_dirty_scope(ScopeId(0));
|
vdom.mark_dirty(ScopeId(0));
|
||||||
dbg!(vdom.render_immediate());
|
assert_eq!(
|
||||||
|
vdom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||||
|
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
vdom.mark_dirty_scope(ScopeId(0));
|
vdom.mark_dirty(ScopeId(0));
|
||||||
dbg!(vdom.render_immediate());
|
assert_eq!(
|
||||||
|
vdom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||||
|
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
vdom.mark_dirty_scope(ScopeId(0));
|
vdom.mark_dirty(ScopeId(0));
|
||||||
dbg!(vdom.render_immediate());
|
assert_eq!(
|
||||||
|
vdom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||||
|
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
vdom.mark_dirty_scope(ScopeId(0));
|
vdom.mark_dirty(ScopeId(0));
|
||||||
dbg!(vdom.render_immediate());
|
assert_eq!(
|
||||||
|
vdom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||||
|
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
//! Diffing Tests
|
||||||
|
//!
|
||||||
|
//! These tests only verify that the diffing algorithm works properly for single components.
|
||||||
|
//!
|
||||||
|
//! It does not validated that component lifecycles work properly. This is done in another test file.
|
||||||
|
|
||||||
|
use dioxus::core::{ElementId, Mutation::*};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
/// Should result in moves, but not removals or additions
|
||||||
|
#[test]
|
||||||
|
fn keyed_diffing_out_of_order() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order = match cx.generation() % 2 {
|
||||||
|
0 => &[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9],
|
||||||
|
1 => &[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dom.rebuild().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(3,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(4,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(6,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(8,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(10,) },
|
||||||
|
AppendChildren { m: 10, id: ElementId(0) },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().edits,
|
||||||
|
[
|
||||||
|
PushRoot { id: ElementId(7,) },
|
||||||
|
InsertBefore { id: ElementId(5,), m: 1 },
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should result in moves only
|
||||||
|
#[test]
|
||||||
|
fn keyed_diffing_out_of_order_adds() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order = match cx.generation() % 2 {
|
||||||
|
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
|
||||||
|
1 => &[/**/ 8, 7, 4, 5, 6 /**/],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().edits,
|
||||||
|
[
|
||||||
|
PushRoot { id: ElementId(5,) },
|
||||||
|
PushRoot { id: ElementId(4,) },
|
||||||
|
InsertBefore { id: ElementId(1,), m: 2 },
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should result in moves only
|
||||||
|
#[test]
|
||||||
|
fn keyed_diffing_out_of_order_adds_3() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order = match cx.generation() % 2 {
|
||||||
|
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
|
||||||
|
1 => &[/**/ 4, 8, 7, 5, 6 /**/],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().edits,
|
||||||
|
[
|
||||||
|
PushRoot { id: ElementId(5,) },
|
||||||
|
PushRoot { id: ElementId(4,) },
|
||||||
|
InsertBefore { id: ElementId(2,), m: 2 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should result in moves onl
|
||||||
|
#[test]
|
||||||
|
fn keyed_diffing_out_of_order_adds_4() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order = match cx.generation() % 2 {
|
||||||
|
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
|
||||||
|
1 => &[/**/ 4, 5, 8, 7, 6 /**/],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().edits,
|
||||||
|
[
|
||||||
|
PushRoot { id: ElementId(5,) },
|
||||||
|
PushRoot { id: ElementId(4,) },
|
||||||
|
InsertBefore { id: ElementId(3,), m: 2 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should result in moves onl
|
||||||
|
#[test]
|
||||||
|
fn keyed_diffing_out_of_order_adds_5() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order = match cx.generation() % 2 {
|
||||||
|
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
|
||||||
|
1 => &[/**/ 4, 5, 6, 8, 7 /**/],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().edits,
|
||||||
|
[
|
||||||
|
PushRoot { id: ElementId(5,) },
|
||||||
|
InsertBefore { id: ElementId(4,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should result in moves onl
|
||||||
|
#[test]
|
||||||
|
fn keyed_diffing_additions() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order: &[_] = match cx.generation() % 2 {
|
||||||
|
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
|
||||||
|
1 => &[/**/ 4, 5, 6, 7, 8, 9, 10 /**/],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
|
||||||
|
InsertAfter { id: ElementId(5), m: 2 }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keyed_diffing_additions_and_moves_on_ends() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order: &[_] = match cx.generation() % 2 {
|
||||||
|
0 => &[/**/ 4, 5, 6, 7 /**/],
|
||||||
|
1 => &[/**/ 7, 4, 5, 6, 11, 12 /**/],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
// create 11, 12
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||||
|
InsertAfter { id: ElementId(3), m: 2 },
|
||||||
|
// move 7 to the front
|
||||||
|
PushRoot { id: ElementId(4) },
|
||||||
|
InsertBefore { id: ElementId(1), m: 1 }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keyed_diffing_additions_and_moves_in_middle() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order: &[_] = match cx.generation() % 2 {
|
||||||
|
0 => &[/**/ 1, 2, 3, 4 /**/],
|
||||||
|
1 => &[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
// LIS: 4, 5, 6
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
// create 5, 6
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||||
|
InsertBefore { id: ElementId(3), m: 2 },
|
||||||
|
// create 7, 8
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(8) },
|
||||||
|
InsertBefore { id: ElementId(2), m: 2 },
|
||||||
|
// move 7
|
||||||
|
PushRoot { id: ElementId(4) },
|
||||||
|
InsertBefore { id: ElementId(1), m: 1 }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn controlled_keyed_diffing_out_of_order() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order: &[_] = match cx.generation() % 2 {
|
||||||
|
0 => &[4, 5, 6, 7],
|
||||||
|
1 => &[0, 5, 9, 6, 4],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
// LIS: 5, 6
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
// remove 7
|
||||||
|
Remove { id: ElementId(4,) },
|
||||||
|
// move 4 to after 6
|
||||||
|
PushRoot { id: ElementId(1) },
|
||||||
|
InsertAfter { id: ElementId(3,), m: 1 },
|
||||||
|
// create 9 and insert before 6
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||||
|
InsertBefore { id: ElementId(3,), m: 1 },
|
||||||
|
// create 0 and insert before 5
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||||
|
InsertBefore { id: ElementId(2,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn controlled_keyed_diffing_out_of_order_max_test() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order: &[_] = match cx.generation() % 2 {
|
||||||
|
0 => &[0, 1, 2, 3, 4],
|
||||||
|
1 => &[3, 0, 1, 10, 2],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
Remove { id: ElementId(5,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||||
|
InsertBefore { id: ElementId(3,), m: 1 },
|
||||||
|
PushRoot { id: ElementId(4) },
|
||||||
|
InsertBefore { id: ElementId(1,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// noticed some weird behavior in the desktop interpreter
|
||||||
|
// just making sure it doesnt happen in the core implementation
|
||||||
|
#[test]
|
||||||
|
fn remove_list() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order: &[_] = match cx.generation() % 2 {
|
||||||
|
0 => &[9, 8, 7, 6, 5],
|
||||||
|
1 => &[9, 8],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
Remove { id: ElementId(3) },
|
||||||
|
Remove { id: ElementId(4) },
|
||||||
|
Remove { id: ElementId(5) }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_common_keys() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let order: &[_] = match cx.generation() % 2 {
|
||||||
|
0 => &[1, 2, 3],
|
||||||
|
1 => &[4, 5, 6],
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_immediate().santize().edits,
|
||||||
|
[
|
||||||
|
Remove { id: ElementId(2) },
|
||||||
|
Remove { id: ElementId(3) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||||
|
ReplaceWith { id: ElementId(1), m: 3 }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,12 +21,12 @@ fn list_creates_one_by_one() {
|
|||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||||
AssignId { path: &[0], id: ElementId(2,) },
|
AssignId { path: &[0], id: ElementId(2,) },
|
||||||
AppendChildren { m: 1 },
|
AppendChildren { id: ElementId(0), m: 1 },
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rendering the first item should replace the placeholder with an element
|
// Rendering the first item should replace the placeholder with an element
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[
|
[
|
||||||
@@ -37,35 +37,35 @@ fn list_creates_one_by_one() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Rendering the next item should insert after the previous
|
// Rendering the next item should insert after the previous
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||||
HydrateText { path: &[0], value: "1", id: ElementId(6,) },
|
HydrateText { path: &[0], value: "1", id: ElementId(5,) },
|
||||||
InsertAfter { id: ElementId(3,), m: 1 },
|
InsertAfter { id: ElementId(3,), m: 1 },
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ... and again!
|
// ... and again!
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(6,) },
|
||||||
HydrateText { path: &[0], value: "2", id: ElementId(8,) },
|
HydrateText { path: &[0], value: "2", id: ElementId(7,) },
|
||||||
InsertAfter { id: ElementId(5,), m: 1 },
|
InsertAfter { id: ElementId(2,), m: 1 },
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// once more
|
// once more
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(8,) },
|
||||||
HydrateText { path: &[0], value: "3", id: ElementId(10,) },
|
HydrateText { path: &[0], value: "3", id: ElementId(9,) },
|
||||||
InsertAfter { id: ElementId(7,), m: 1 },
|
InsertAfter { id: ElementId(6,), m: 1 },
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -100,20 +100,20 @@ fn removes_one_by_one() {
|
|||||||
// replace the placeholder in the template with the 3 templates on the stack
|
// replace the placeholder in the template with the 3 templates on the stack
|
||||||
ReplacePlaceholder { m: 3, path: &[0] },
|
ReplacePlaceholder { m: 3, path: &[0] },
|
||||||
// Mount the div
|
// Mount the div
|
||||||
AppendChildren { m: 1 }
|
AppendChildren { id: ElementId(0), m: 1 }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove div(3)
|
// Remove div(3)
|
||||||
// Rendering the first item should replace the placeholder with an element
|
// Rendering the first item should replace the placeholder with an element
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[Remove { id: ElementId(6) }]
|
[Remove { id: ElementId(6) }]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove div(2)
|
// Remove div(2)
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[Remove { id: ElementId(4) }]
|
[Remove { id: ElementId(4) }]
|
||||||
@@ -121,28 +121,28 @@ fn removes_one_by_one() {
|
|||||||
|
|
||||||
// Remove div(1) and replace with a placeholder
|
// Remove div(1) and replace with a placeholder
|
||||||
// todo: this should just be a remove with no placeholder
|
// todo: this should just be a remove with no placeholder
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[
|
[
|
||||||
CreatePlaceholder { id: ElementId(8) },
|
CreatePlaceholder { id: ElementId(3) },
|
||||||
ReplaceWith { id: ElementId(2), m: 1 }
|
ReplaceWith { id: ElementId(2), m: 1 }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// load the 3 and replace the placeholder
|
// load the 3 and replace the placeholder
|
||||||
// todo: this should actually be append to, but replace placeholder is fine for now
|
// todo: this should actually be append to, but replace placeholder is fine for now
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(9) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||||
HydrateText { path: &[0], value: "0", id: ElementId(10) },
|
HydrateText { path: &[0], value: "0", id: ElementId(4) },
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(11) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||||
HydrateText { path: &[0], value: "1", id: ElementId(12) },
|
HydrateText { path: &[0], value: "1", id: ElementId(6) },
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(13) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
|
||||||
HydrateText { path: &[0], value: "2", id: ElementId(14) },
|
HydrateText { path: &[0], value: "2", id: ElementId(8) },
|
||||||
ReplaceWith { id: ElementId(8), m: 3 }
|
ReplaceWith { id: ElementId(3), m: 3 }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -150,10 +150,9 @@ fn removes_one_by_one() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn list_shrink_multiroot() {
|
fn list_shrink_multiroot() {
|
||||||
let mut dom = VirtualDom::new(|cx| {
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
let gen = cx.generation();
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
(0..gen).map(|i| rsx! {
|
(0..cx.generation()).map(|i| rsx! {
|
||||||
div { "{i}" }
|
div { "{i}" }
|
||||||
div { "{i}" }
|
div { "{i}" }
|
||||||
})
|
})
|
||||||
@@ -166,11 +165,11 @@ fn list_shrink_multiroot() {
|
|||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||||
AssignId { path: &[0,], id: ElementId(2,) },
|
AssignId { path: &[0,], id: ElementId(2,) },
|
||||||
AppendChildren { m: 1 }
|
AppendChildren { id: ElementId(0), m: 1 }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[
|
[
|
||||||
@@ -182,27 +181,27 @@ fn list_shrink_multiroot() {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||||
HydrateText { path: &[0], value: "1", id: ElementId(8) },
|
HydrateText { path: &[0], value: "1", id: ElementId(7) },
|
||||||
LoadTemplate { name: "template", index: 1, id: ElementId(9) },
|
LoadTemplate { name: "template", index: 1, id: ElementId(8) },
|
||||||
HydrateText { path: &[0], value: "1", id: ElementId(10) },
|
HydrateText { path: &[0], value: "1", id: ElementId(9) },
|
||||||
InsertAfter { id: ElementId(5), m: 2 }
|
InsertAfter { id: ElementId(5), m: 2 }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(11) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(10) },
|
||||||
HydrateText { path: &[0], value: "2", id: ElementId(12) },
|
HydrateText { path: &[0], value: "2", id: ElementId(11) },
|
||||||
LoadTemplate { name: "template", index: 1, id: ElementId(13) },
|
LoadTemplate { name: "template", index: 1, id: ElementId(12) },
|
||||||
HydrateText { path: &[0], value: "2", id: ElementId(14) },
|
HydrateText { path: &[0], value: "2", id: ElementId(13) },
|
||||||
InsertAfter { id: ElementId(9), m: 2 }
|
InsertAfter { id: ElementId(8), m: 2 }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -245,29 +244,136 @@ fn removes_one_by_one_multiroot() {
|
|||||||
//
|
//
|
||||||
ReplacePlaceholder { path: &[0], m: 6 },
|
ReplacePlaceholder { path: &[0], m: 6 },
|
||||||
//
|
//
|
||||||
AppendChildren { m: 1 }
|
AppendChildren { id: ElementId(0), m: 1 }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[Remove { id: ElementId(10) }, Remove { id: ElementId(12) }]
|
[Remove { id: ElementId(10) }, Remove { id: ElementId(12) }]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[Remove { id: ElementId(6) }, Remove { id: ElementId(8) }]
|
[Remove { id: ElementId(6) }, Remove { id: ElementId(8) }]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty_scope(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate().santize().edits,
|
dom.render_immediate().santize().edits,
|
||||||
[
|
[
|
||||||
Remove { id: ElementId(4) },
|
Remove { id: ElementId(4) },
|
||||||
CreatePlaceholder { id: ElementId(14) },
|
CreatePlaceholder { id: ElementId(5) },
|
||||||
ReplaceWith { id: ElementId(2), m: 1 }
|
ReplaceWith { id: ElementId(2), m: 1 }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_equal_fragments_are_equal_static() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
cx.render(rsx! {
|
||||||
|
(0..5).map(|_| rsx! {
|
||||||
|
div { "hello" }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
assert!(dom.render_immediate().edits.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_equal_fragments_are_equal() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
cx.render(rsx! {
|
||||||
|
(0..5).map(|i| rsx! {
|
||||||
|
div { "hello {i}" }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
assert!(dom.render_immediate().edits.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_many() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let num = match cx.generation() % 3 {
|
||||||
|
0 => 0,
|
||||||
|
1 => 1,
|
||||||
|
2 => 5,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
(0..num).map(|i| rsx! { div { "hello {i}" } })
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let edits = dom.rebuild().santize();
|
||||||
|
assert!(edits.templates.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
edits.edits,
|
||||||
|
[
|
||||||
|
CreatePlaceholder { id: ElementId(1,) },
|
||||||
|
AppendChildren { id: ElementId(0), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
let edits = dom.render_immediate().santize();
|
||||||
|
assert_eq!(
|
||||||
|
edits.edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||||
|
HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) },
|
||||||
|
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
let edits = dom.render_immediate().santize();
|
||||||
|
assert_eq!(
|
||||||
|
edits.edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||||
|
HydrateText { path: &[0,], value: "hello 1", id: ElementId(4,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
|
||||||
|
HydrateText { path: &[0,], value: "hello 2", id: ElementId(6,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
|
||||||
|
HydrateText { path: &[0,], value: "hello 3", id: ElementId(8,) },
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
|
||||||
|
HydrateText { path: &[0,], value: "hello 4", id: ElementId(10,) },
|
||||||
|
InsertAfter { id: ElementId(2,), m: 4 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
let edits = dom.render_immediate().santize();
|
||||||
|
assert_eq!(
|
||||||
|
edits.edits,
|
||||||
|
[
|
||||||
|
Remove { id: ElementId(1,) },
|
||||||
|
Remove { id: ElementId(5,) },
|
||||||
|
Remove { id: ElementId(7,) },
|
||||||
|
Remove { id: ElementId(9,) },
|
||||||
|
CreatePlaceholder { id: ElementId(9,) },
|
||||||
|
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
let edits = dom.render_immediate().santize();
|
||||||
|
assert_eq!(
|
||||||
|
edits.edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||||
|
HydrateText { path: &[0,], value: "hello 0", id: ElementId(10,) },
|
||||||
|
ReplaceWith { id: ElementId(9,), m: 1 },
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
//! It should be possible to swap out templates at runtime, enabling hotreloading
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
use dioxus::core::{ElementId, Mutation};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn basic_syntax_is_a_template(cx: Scope) -> Element {
|
||||||
|
let asd = 123;
|
||||||
|
let var = 123;
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div { key: "12345",
|
||||||
|
class: "asd",
|
||||||
|
class: "{asd}",
|
||||||
|
onclick: move |_| {},
|
||||||
|
div { "{var}" }
|
||||||
|
div {
|
||||||
|
h1 { "var" }
|
||||||
|
p { "you're great!" }
|
||||||
|
div { background_color: "red",
|
||||||
|
h1 { "var" }
|
||||||
|
div { b { "asd" } "not great" }
|
||||||
|
}
|
||||||
|
p { "you're great!" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn dual_stream() {
|
||||||
|
let mut dom = VirtualDom::new(basic_syntax_is_a_template);
|
||||||
|
let edits = dom.rebuild().santize();
|
||||||
|
|
||||||
|
use Mutation::*;
|
||||||
|
assert_eq!(
|
||||||
|
edits.edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
|
SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None },
|
||||||
|
NewEventListener { name: "click", scope: ScopeId(0), id: ElementId(1) },
|
||||||
|
HydrateText { path: &[0, 0], value: "123", id: ElementId(2) },
|
||||||
|
AppendChildren { id: ElementId(0), m: 1 }
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
#![allow(unused, non_upper_case_globals)]
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
//! Tests for the lifecycle of components.
|
||||||
|
use dioxus::core::{ElementId, Mutation::*};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
type Shared<T> = Arc<Mutex<T>>;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn manual_diffing() {
|
||||||
|
struct AppProps {
|
||||||
|
value: Shared<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope<AppProps>) -> Element {
|
||||||
|
let val = cx.props.value.lock().unwrap();
|
||||||
|
cx.render(rsx! { div { "{val}" } })
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = Arc::new(Mutex::new("Hello"));
|
||||||
|
let mut dom = VirtualDom::new_with_props(app, AppProps { value: value.clone() });
|
||||||
|
|
||||||
|
let _ = dom.rebuild();
|
||||||
|
|
||||||
|
*value.lock().unwrap() = "goodbye";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dom.rebuild().santize().edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||||
|
HydrateText { path: &[0], value: "goodbye", id: ElementId(4) },
|
||||||
|
AppendChildren { m: 1, id: ElementId(0) }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn events_generate() {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let count = cx.use_hook(|| 0);
|
||||||
|
|
||||||
|
match *count {
|
||||||
|
0 => cx.render(rsx! {
|
||||||
|
div { onclick: move |_| *count += 1,
|
||||||
|
div { "nested" }
|
||||||
|
"Click me!"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_ => cx.render(rsx!(())),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true);
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
let edits = dom.render_immediate();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
edits.edits,
|
||||||
|
[
|
||||||
|
CreatePlaceholder { id: ElementId(2) },
|
||||||
|
ReplaceWith { id: ElementId(1), m: 1 }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn components_generate() {
|
||||||
|
// fn app(cx: Scope) -> Element {
|
||||||
|
// let render_phase = cx.use_hook(|| 0);
|
||||||
|
// *render_phase += 1;
|
||||||
|
|
||||||
|
// cx.render(match *render_phase {
|
||||||
|
// 1 => rsx_without_templates!("Text0"),
|
||||||
|
// 2 => rsx_without_templates!(div {}),
|
||||||
|
// 3 => rsx_without_templates!("Text2"),
|
||||||
|
// 4 => rsx_without_templates!(Child {}),
|
||||||
|
// 5 => rsx_without_templates!({ None as Option<()> }),
|
||||||
|
// 6 => rsx_without_templates!("text 3"),
|
||||||
|
// 7 => rsx_without_templates!({ (0..2).map(|f| rsx_without_templates!("text {f}")) }),
|
||||||
|
// 8 => rsx_without_templates!(Child {}),
|
||||||
|
// _ => todo!(),
|
||||||
|
// })
|
||||||
|
// };
|
||||||
|
|
||||||
|
// fn Child(cx: Scope) -> Element {
|
||||||
|
// println!("Running child");
|
||||||
|
// cx.render(rsx_without_templates! {
|
||||||
|
// h1 {}
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let mut dom = VirtualDom::new(app);
|
||||||
|
// let edits = dom.rebuild();
|
||||||
|
// assert_eq!(
|
||||||
|
// edits.edits,
|
||||||
|
// [
|
||||||
|
// CreateTextNode { root: Some(1), text: "Text0" },
|
||||||
|
// AppendChildren { root: Some(0), children: vec![1] }
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// assert_eq!(
|
||||||
|
// dom.hard_diff(ScopeId(0)).edits,
|
||||||
|
// [
|
||||||
|
// CreateElement { root: Some(2), tag: "div", children: 0 },
|
||||||
|
// ReplaceWith { root: Some(1), nodes: vec![2] }
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// assert_eq!(
|
||||||
|
// dom.hard_diff(ScopeId(0)).edits,
|
||||||
|
// [
|
||||||
|
// CreateTextNode { root: Some(1), text: "Text2" },
|
||||||
|
// ReplaceWith { root: Some(2), nodes: vec![1] }
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // child {}
|
||||||
|
// assert_eq!(
|
||||||
|
// dom.hard_diff(ScopeId(0)).edits,
|
||||||
|
// [
|
||||||
|
// CreateElement { root: Some(2), tag: "h1", children: 0 },
|
||||||
|
// ReplaceWith { root: Some(1), nodes: vec![2] }
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // placeholder
|
||||||
|
// assert_eq!(
|
||||||
|
// dom.hard_diff(ScopeId(0)).edits,
|
||||||
|
// [
|
||||||
|
// CreatePlaceholder { root: Some(1) },
|
||||||
|
// ReplaceWith { root: Some(2), nodes: vec![1] }
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// assert_eq!(
|
||||||
|
// dom.hard_diff(ScopeId(0)).edits,
|
||||||
|
// [
|
||||||
|
// CreateTextNode { root: Some(2), text: "text 3" },
|
||||||
|
// ReplaceWith { root: Some(1), nodes: vec![2] }
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// assert_eq!(
|
||||||
|
// dom.hard_diff(ScopeId(0)).edits,
|
||||||
|
// [
|
||||||
|
// CreateTextNode { text: "text 0", root: Some(1) },
|
||||||
|
// CreateTextNode { text: "text 1", root: Some(3) },
|
||||||
|
// ReplaceWith { root: Some(2), nodes: vec![1, 3] },
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// assert_eq!(
|
||||||
|
// dom.hard_diff(ScopeId(0)).edits,
|
||||||
|
// [
|
||||||
|
// CreateElement { tag: "h1", root: Some(2), children: 0 },
|
||||||
|
// ReplaceWith { root: Some(1), nodes: vec![2] },
|
||||||
|
// Remove { root: Some(3) },
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
// }
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_core::ElementId;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn miri_rollover() {
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
for x in 0..3 {
|
||||||
|
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
|
||||||
|
dom.process_events();
|
||||||
|
dom.render_immediate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let mut idx = use_state(cx, || 0);
|
||||||
|
let onhover = |h| println!("go!");
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
idx += 1;
|
||||||
|
println!("Clicked");
|
||||||
|
},
|
||||||
|
"+"
|
||||||
|
}
|
||||||
|
button { onclick: move |_| idx -= 1, "-" }
|
||||||
|
ul {
|
||||||
|
(0..**idx).map(|i| rsx! {
|
||||||
|
Child { i: i, onhover: onhover }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline_props]
|
||||||
|
fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
li {
|
||||||
|
onmouseover: move |e| onhover.call(e),
|
||||||
|
"{i}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn app_drops() {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hooks_drop() {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.use_hook(|| String::from("asd"));
|
||||||
|
cx.use_hook(|| String::from("asd"));
|
||||||
|
cx.use_hook(|| String::from("asd"));
|
||||||
|
cx.use_hook(|| String::from("asd"));
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn contexts_drop() {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.provide_context(String::from("asd"));
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
child_comp {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child_comp(cx: Scope) -> Element {
|
||||||
|
let el = cx.consume_context::<String>().unwrap();
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div { "hello {el}" }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tasks_drop() {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.spawn(async {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div { }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn root_props_drop() {
|
||||||
|
struct RootProps(String);
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new_with_props(
|
||||||
|
|cx| cx.render(rsx!( div { "{cx.props.0}" } )),
|
||||||
|
RootProps("asdasd".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diffing_drops_old() {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
match cx.generation() % 2 {
|
||||||
|
0 => rsx!( child_comp1 { name: "asdasd".to_string() }),
|
||||||
|
1 => rsx!( child_comp2 { name: "asdasd".to_string() }),
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline_props]
|
||||||
|
fn child_comp1(cx: Scope, name: String) -> Element {
|
||||||
|
cx.render(rsx! { "Hello {name}" })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline_props]
|
||||||
|
fn child_comp2(cx: Scope, name: String) -> Element {
|
||||||
|
cx.render(rsx! { "Goodbye {name}" })
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
_ = dom.rebuild();
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
/// This test checks that we should release all memory used by the virtualdom when it exits.
|
||||||
|
///
|
||||||
|
/// When miri runs, it'll let us know if we leaked or aliased.
|
||||||
|
#[test]
|
||||||
|
fn test_memory_leak() {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let val = cx.generation();
|
||||||
|
|
||||||
|
cx.spawn(async {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
if val == 2 || val == 4 {
|
||||||
|
return cx.render(rsx!(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = cx.use_hook(|| String::from("numbers: "));
|
||||||
|
|
||||||
|
name.push_str("123 ");
|
||||||
|
|
||||||
|
cx.render(rsx!(
|
||||||
|
div { "Hello, world!" }
|
||||||
|
Child {}
|
||||||
|
Child {}
|
||||||
|
Child {}
|
||||||
|
Child {}
|
||||||
|
Child {}
|
||||||
|
Child {}
|
||||||
|
BorrowedChild { name: name }
|
||||||
|
BorrowedChild { name: name }
|
||||||
|
BorrowedChild { name: name }
|
||||||
|
BorrowedChild { name: name }
|
||||||
|
BorrowedChild { name: name }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
struct BorrowedProps<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn BorrowedChild<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
"goodbye {cx.props.name}"
|
||||||
|
Child {}
|
||||||
|
Child {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Child(cx: Scope) -> Element {
|
||||||
|
render!(div { "goodbye world" })
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
for _ in 0..5 {
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
_ = dom.render_immediate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn memo_works_properly() {
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let val = cx.generation();
|
||||||
|
|
||||||
|
if val == 2 || val == 4 {
|
||||||
|
return cx.render(rsx!(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = cx.use_hook(|| String::from("asd"));
|
||||||
|
|
||||||
|
cx.render(rsx!(
|
||||||
|
div { "Hello, world! {name}" }
|
||||||
|
Child { na: "asdfg".to_string() }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Props)]
|
||||||
|
struct ChildProps {
|
||||||
|
na: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Child(cx: Scope<ChildProps>) -> Element {
|
||||||
|
render!(div { "goodbye world" })
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
|
dom.rebuild();
|
||||||
|
todo!()
|
||||||
|
// dom.hard_diff(ScopeId(0));
|
||||||
|
// dom.hard_diff(ScopeId(0));
|
||||||
|
// dom.hard_diff(ScopeId(0));
|
||||||
|
// dom.hard_diff(ScopeId(0));
|
||||||
|
// dom.hard_diff(ScopeId(0));
|
||||||
|
// dom.hard_diff(ScopeId(0));
|
||||||
|
// dom.hard_diff(ScopeId(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn free_works_on_root_hooks() {
|
||||||
|
/*
|
||||||
|
On Drop, scopearena drops all the hook contents. and props
|
||||||
|
*/
|
||||||
|
#[derive(PartialEq, Clone, Props)]
|
||||||
|
struct AppProps {
|
||||||
|
inner: Rc<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope<AppProps>) -> Element {
|
||||||
|
let name: &AppProps = cx.use_hook(|| cx.props.clone());
|
||||||
|
render!(child_component { inner: name.inner.clone() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child_component(cx: Scope<AppProps>) -> Element {
|
||||||
|
render!(div { "{cx.props.inner}" })
|
||||||
|
}
|
||||||
|
|
||||||
|
let ptr = Rc::new("asdasd".to_string());
|
||||||
|
let mut dom = VirtualDom::new_with_props(app, AppProps { inner: ptr.clone() });
|
||||||
|
let _ = dom.rebuild();
|
||||||
|
|
||||||
|
// ptr gets cloned into props and then into the hook
|
||||||
|
assert_eq!(Rc::strong_count(&ptr), 4);
|
||||||
|
|
||||||
|
drop(dom);
|
||||||
|
|
||||||
|
assert_eq!(Rc::strong_count(&ptr), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn old_props_arent_stale() {
|
||||||
|
// fn app(cx: Scope) -> Element {
|
||||||
|
// dbg!("rendering parent");
|
||||||
|
// let cnt = cx.use_hook(|| 0);
|
||||||
|
// *cnt += 1;
|
||||||
|
|
||||||
|
// if *cnt == 1 {
|
||||||
|
// render!(div { Child { a: "abcdef".to_string() } })
|
||||||
|
// } else {
|
||||||
|
// render!(div { Child { a: "abcdef".to_string() } })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[derive(Props, PartialEq)]
|
||||||
|
// struct ChildProps {
|
||||||
|
// a: String,
|
||||||
|
// }
|
||||||
|
// fn Child(cx: Scope<ChildProps>) -> Element {
|
||||||
|
// dbg!("rendering child", &cx.props.a);
|
||||||
|
// render!(div { "child {cx.props.a}" })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let mut dom = new_dom(app, ());
|
||||||
|
// let _ = dom.rebuild();
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
|
||||||
|
// dbg!("forcing update to child");
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn basic() {
|
||||||
|
// fn app(cx: Scope) -> Element {
|
||||||
|
// render!(div {
|
||||||
|
// Child { a: "abcdef".to_string() }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[derive(Props, PartialEq)]
|
||||||
|
// struct ChildProps {
|
||||||
|
// a: String,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn Child(cx: Scope<ChildProps>) -> Element {
|
||||||
|
// dbg!("rendering child", &cx.props.a);
|
||||||
|
// render!(div { "child {cx.props.a}" })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let mut dom = new_dom(app, ());
|
||||||
|
// let _ = dom.rebuild();
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn leak_thru_children() {
|
||||||
|
// fn app(cx: Scope) -> Element {
|
||||||
|
// cx.render(rsx! {
|
||||||
|
// Child {
|
||||||
|
// name: "asd".to_string(),
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// cx.render(rsx! {
|
||||||
|
// div {}
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[inline_props]
|
||||||
|
// fn Child(cx: Scope, name: String) -> Element {
|
||||||
|
// render!(div { "child {name}" })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let mut dom = new_dom(app, ());
|
||||||
|
// let _ = dom.rebuild();
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn test_pass_thru() {
|
||||||
|
// #[inline_props]
|
||||||
|
// fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element {
|
||||||
|
// cx.render(rsx! {
|
||||||
|
// header {
|
||||||
|
// nav { children }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn NavMenu(cx: Scope) -> Element {
|
||||||
|
// render!( NavBrand {}
|
||||||
|
// div {
|
||||||
|
// NavStart {}
|
||||||
|
// NavEnd {}
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn NavBrand(cx: Scope) -> Element {
|
||||||
|
// render!(div {})
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn NavStart(cx: Scope) -> Element {
|
||||||
|
// render!(div {})
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn NavEnd(cx: Scope) -> Element {
|
||||||
|
// render!(div {})
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[inline_props]
|
||||||
|
// fn MainContainer<'a>(
|
||||||
|
// cx: Scope,
|
||||||
|
// nav: Element<'a>,
|
||||||
|
// body: Element<'a>,
|
||||||
|
// footer: Element<'a>,
|
||||||
|
// ) -> Element {
|
||||||
|
// cx.render(rsx! {
|
||||||
|
// div {
|
||||||
|
// class: "columns is-mobile",
|
||||||
|
// div {
|
||||||
|
// class: "column is-full",
|
||||||
|
// nav,
|
||||||
|
// body,
|
||||||
|
// footer,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn app(cx: Scope) -> Element {
|
||||||
|
// let nav = cx.render(rsx! {
|
||||||
|
// NavContainer {
|
||||||
|
// NavMenu {}
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// let body = cx.render(rsx! {
|
||||||
|
// div {}
|
||||||
|
// });
|
||||||
|
// let footer = cx.render(rsx! {
|
||||||
|
// div {}
|
||||||
|
// });
|
||||||
|
|
||||||
|
// cx.render(rsx! {
|
||||||
|
// MainContainer {
|
||||||
|
// nav: nav,
|
||||||
|
// body: body,
|
||||||
|
// footer: footer,
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let mut dom = new_dom(app, ());
|
||||||
|
// let _ = dom.rebuild();
|
||||||
|
|
||||||
|
// for _ in 0..40 {
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
|
||||||
|
// dom.work_with_deadline(|| false);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -13,10 +13,6 @@ fn root_node_isnt_null() {
|
|||||||
// We haven't built the tree, so trying to get out the root node should fail
|
// We haven't built the tree, so trying to get out the root node should fail
|
||||||
assert!(scope.try_root_node().is_none());
|
assert!(scope.try_root_node().is_none());
|
||||||
|
|
||||||
// There should be no way to gain an invalid pointer
|
|
||||||
assert!(scope.current_frame().node.get().is_null());
|
|
||||||
assert!(scope.previous_frame().node.get().is_null());
|
|
||||||
|
|
||||||
// The height should be 0
|
// The height should be 0
|
||||||
assert_eq!(scope.height(), 0);
|
assert_eq!(scope.height(), 0);
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
use dioxus::core::ElementId;
|
|
||||||
use dioxus::core::{Mutation::*, SuspenseBoundary};
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
use dioxus_core::SuspenseContext;
|
|
||||||
use std::{rc::Rc, time::Duration};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn it_works() {
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
|
|
||||||
let mutations = dom.rebuild().santize();
|
|
||||||
|
|
||||||
// We should at least get the top-level template in
|
|
||||||
assert_eq!(
|
|
||||||
mutations.template_mutations,
|
|
||||||
[
|
|
||||||
CreateElement { name: "div" },
|
|
||||||
CreateStaticText { value: "Waiting for child..." },
|
|
||||||
CreatePlaceholder { id: ElementId(0) },
|
|
||||||
AppendChildren { m: 2 },
|
|
||||||
SaveTemplate { name: "template", m: 1 }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// And we should load it in and assign the placeholder properly
|
|
||||||
assert_eq!(
|
|
||||||
mutations.edits,
|
|
||||||
[
|
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
|
||||||
// hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly?
|
|
||||||
// can we even?
|
|
||||||
AssignId { path: &[1], id: ElementId(3) },
|
|
||||||
AppendChildren { m: 1 },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// wait just a moment, not enough time for the boundary to resolve
|
|
||||||
|
|
||||||
dom.wait_for_work().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
cx.render(rsx!(
|
|
||||||
div {
|
|
||||||
"Waiting for child..."
|
|
||||||
suspense_boundary {}
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn suspense_boundary(cx: Scope) -> Element {
|
|
||||||
cx.use_hook(|| cx.provide_context(Rc::new(SuspenseBoundary::new(cx.scope_id()))));
|
|
||||||
|
|
||||||
// Ensure the right types are found
|
|
||||||
cx.has_context::<SuspenseContext>().unwrap();
|
|
||||||
|
|
||||||
cx.render(rsx!(async_child {}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn async_child(cx: Scope<'_>) -> Element {
|
|
||||||
use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await;
|
|
||||||
cx.render(rsx!(async_text {}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn async_text(cx: Scope<'_>) -> Element {
|
|
||||||
cx.render(rsx!("async_text"))
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use dioxus::core::ElementId;
|
||||||
|
use dioxus::core::{Mutation::*, SuspenseContext};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_core::SuspenseContext;
|
||||||
|
use std::future::IntoFuture;
|
||||||
|
use std::{rc::Rc, time::Duration};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_works() {
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
|
let mutations = dom.rebuild().santize();
|
||||||
|
|
||||||
|
// We should at least get the top-level template in before pausing for the children
|
||||||
|
// note: we dont test template edits anymore
|
||||||
|
// assert_eq!(
|
||||||
|
// mutations.templates,
|
||||||
|
// [
|
||||||
|
// CreateElement { name: "div" },
|
||||||
|
// CreateStaticText { value: "Waiting for child..." },
|
||||||
|
// CreateStaticPlaceholder,
|
||||||
|
// AppendChildren { m: 2 },
|
||||||
|
// SaveTemplate { name: "template", m: 1 }
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// And we should load it in and assign the placeholder properly
|
||||||
|
assert_eq!(
|
||||||
|
mutations.edits,
|
||||||
|
[
|
||||||
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
|
// hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly?
|
||||||
|
// can we even?
|
||||||
|
AssignId { path: &[1], id: ElementId(3) },
|
||||||
|
AppendChildren { m: 1, id: ElementId(0) },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// wait just a moment, not enough time for the boundary to resolve
|
||||||
|
|
||||||
|
dom.wait_for_work().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx!(
|
||||||
|
div {
|
||||||
|
"Waiting for child..."
|
||||||
|
suspense_boundary {}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suspense_boundary(cx: Scope) -> Element {
|
||||||
|
cx.use_hook(|| cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id()))));
|
||||||
|
|
||||||
|
// Ensure the right types are found
|
||||||
|
cx.has_context::<SuspenseContext>().unwrap();
|
||||||
|
|
||||||
|
cx.render(rsx!(async_child {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn async_child(cx: Scope<'_>) -> Element {
|
||||||
|
use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await;
|
||||||
|
cx.render(rsx!(async_text {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn async_text(cx: Scope<'_>) -> Element {
|
||||||
|
let username = use_future!(cx, || async {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
"async child 1"
|
||||||
|
});
|
||||||
|
|
||||||
|
let age = use_future!(cx, || async {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||||
|
println!("long future completed");
|
||||||
|
1234
|
||||||
|
});
|
||||||
|
|
||||||
|
let (_user, _age) = use_future!(cx, || async {
|
||||||
|
tokio::join!(
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)),
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(2))
|
||||||
|
);
|
||||||
|
("async child 1", 1234)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (username, age) = tokio::join!(username.into_future(), age.into_future());
|
||||||
|
|
||||||
|
cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } ))
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ async fn it_works() {
|
|||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = dom.wait_for_work() => {}
|
_ = dom.wait_for_work() => {}
|
||||||
_ = tokio::time::sleep(Duration::from_millis(10)) => {}
|
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// By the time the tasks are finished, we should've accumulated ticks from two tasks
|
// By the time the tasks are finished, we should've accumulated ticks from two tasks
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ impl DesktopController {
|
|||||||
{
|
{
|
||||||
let edits = dom.rebuild();
|
let edits = dom.rebuild();
|
||||||
let mut queue = edit_queue.lock().unwrap();
|
let mut queue = edit_queue.lock().unwrap();
|
||||||
queue.push(serde_json::to_string(&edits.template_mutations).unwrap());
|
queue.push(serde_json::to_string(&edits.templates).unwrap());
|
||||||
queue.push(serde_json::to_string(&edits.edits).unwrap());
|
queue.push(serde_json::to_string(&edits.edits).unwrap());
|
||||||
proxy.send_event(UserWindowEvent::Update).unwrap();
|
proxy.send_event(UserWindowEvent::EditsReady).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -67,7 +67,7 @@ impl DesktopController {
|
|||||||
let name = value.event.clone();
|
let name = value.event.clone();
|
||||||
let el_id = ElementId(value.mounted_dom_id);
|
let el_id = ElementId(value.mounted_dom_id);
|
||||||
if let Some(evt) = decode_event(value) {
|
if let Some(evt) = decode_event(value) {
|
||||||
dom.handle_event(&name, evt, el_id, true, EventPriority::Medium);
|
dom.handle_event(&name, evt, el_id, dioxus_html::events::event_bubbles(&name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,9 +79,9 @@ impl DesktopController {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut queue = edit_queue.lock().unwrap();
|
let mut queue = edit_queue.lock().unwrap();
|
||||||
queue.push(serde_json::to_string(&muts.template_mutations).unwrap());
|
queue.push(serde_json::to_string(&muts.templates).unwrap());
|
||||||
queue.push(serde_json::to_string(&muts.edits).unwrap());
|
queue.push(serde_json::to_string(&muts.edits).unwrap());
|
||||||
let _ = proxy.send_event(UserWindowEvent::Update);
|
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -116,6 +116,8 @@ impl DesktopController {
|
|||||||
|
|
||||||
let (_id, view) = self.webviews.iter_mut().next().unwrap();
|
let (_id, view) = self.webviews.iter_mut().next().unwrap();
|
||||||
|
|
||||||
|
println!("processing pending edits {:?}", new_queue.len());
|
||||||
|
|
||||||
for edit in new_queue.drain(..) {
|
for edit in new_queue.drain(..) {
|
||||||
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ impl DesktopContext {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum UserWindowEvent {
|
pub enum UserWindowEvent {
|
||||||
Update,
|
EditsReady,
|
||||||
|
Initialize,
|
||||||
|
|
||||||
CloseWindow,
|
CloseWindow,
|
||||||
DragWindow,
|
DragWindow,
|
||||||
@@ -213,7 +214,7 @@ pub(super) fn handler(
|
|||||||
println!("user_event: {:?}", user_event);
|
println!("user_event: {:?}", user_event);
|
||||||
|
|
||||||
match user_event {
|
match user_event {
|
||||||
Update => desktop.try_load_ready_webviews(),
|
Initialize | EditsReady => desktop.try_load_ready_webviews(),
|
||||||
CloseWindow => *control_flow = ControlFlow::Exit,
|
CloseWindow => *control_flow = ControlFlow::Exit,
|
||||||
DragWindow => {
|
DragWindow => {
|
||||||
// if the drag_window has any errors, we don't do anything
|
// if the drag_window has any errors, we don't do anything
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cf
|
|||||||
"initialize" => {
|
"initialize" => {
|
||||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
println!("initializing...");
|
println!("initializing...");
|
||||||
let _ = proxy.send_event(UserWindowEvent::Update);
|
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||||
}
|
}
|
||||||
"browser_open" => {
|
"browser_open" => {
|
||||||
let data = message.params();
|
let data = message.params();
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
struct_lit_width = 80
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
#![allow(unused, non_upper_case_globals, non_snake_case)]
|
|
||||||
|
|
||||||
//! Prove that the dom works normally through virtualdom methods.
|
|
||||||
//!
|
|
||||||
//! This methods all use "rebuild" which completely bypasses the scheduler.
|
|
||||||
//! Hard rebuilds don't consume any events from the event queue.
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
|
|
||||||
use dioxus_edit_stream::DomEdit::*;
|
|
||||||
|
|
||||||
fn new_dom<P: 'static + Send>(app: Component<P>, props: P) -> VirtualDom {
|
|
||||||
VirtualDom::new_with_props(app, props)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_original_diff() {
|
|
||||||
static APP: Component = |cx| {
|
|
||||||
cx.render(rsx! {
|
|
||||||
div {
|
|
||||||
div {
|
|
||||||
"Hello, world!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dom = new_dom(APP, ());
|
|
||||||
let mutations = dom.rebuild();
|
|
||||||
assert_eq!(
|
|
||||||
mutations.edits,
|
|
||||||
[
|
|
||||||
// create template
|
|
||||||
CreateElement { root: Some(1), tag: "template", children: 1 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 1 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 1 },
|
|
||||||
CreateTextNode { root: None, text: "Hello, world!" },
|
|
||||||
// clone template
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![2] },
|
|
||||||
// add to root
|
|
||||||
AppendChildren { root: Some(0), children: vec![2] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create() {
|
|
||||||
static APP: Component = |cx| {
|
|
||||||
cx.render(rsx! {
|
|
||||||
div {
|
|
||||||
div {
|
|
||||||
"Hello, world!"
|
|
||||||
div {
|
|
||||||
div {
|
|
||||||
Fragment {
|
|
||||||
"hello"
|
|
||||||
"world"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dom = new_dom(APP, ());
|
|
||||||
let mutations = dom.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
mutations.edits,
|
|
||||||
[
|
|
||||||
// create template
|
|
||||||
CreateElement { root: Some(1), tag: "template", children: 1 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 1 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 2 },
|
|
||||||
CreateTextNode { root: None, text: "Hello, world!" },
|
|
||||||
CreateElement { root: None, tag: "div", children: 1 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 0 },
|
|
||||||
// clone template
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![2] },
|
|
||||||
SetLastNode { id: 2 },
|
|
||||||
FirstChild {},
|
|
||||||
StoreWithId { id: 3 },
|
|
||||||
FirstChild {},
|
|
||||||
NextSibling {},
|
|
||||||
StoreWithId { id: 4 },
|
|
||||||
FirstChild {},
|
|
||||||
StoreWithId { id: 5 },
|
|
||||||
CreateTextNode { root: Some(6), text: "hello" },
|
|
||||||
CreateTextNode { root: Some(7), text: "world" },
|
|
||||||
SetLastNode { id: 5 },
|
|
||||||
AppendChildren { root: None, children: vec![6, 7] },
|
|
||||||
AppendChildren { root: Some(0), children: vec![2] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_list() {
|
|
||||||
static APP: Component = |cx| {
|
|
||||||
cx.render(rsx! {
|
|
||||||
{(0..3).map(|f| rsx!{ div {
|
|
||||||
"hello"
|
|
||||||
}})}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dom = new_dom(APP, ());
|
|
||||||
let mutations = dom.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
mutations.edits,
|
|
||||||
[
|
|
||||||
// create template
|
|
||||||
CreateElement { root: Some(1), tag: "template", children: 1 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 1 },
|
|
||||||
CreateTextNode { root: None, text: "hello" },
|
|
||||||
// clone template
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![2] },
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![3] },
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![4] },
|
|
||||||
// add to root
|
|
||||||
AppendChildren { root: Some(0), children: vec![2, 3, 4] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_simple() {
|
|
||||||
static APP: Component = |cx| {
|
|
||||||
cx.render(rsx! {
|
|
||||||
div {}
|
|
||||||
div {}
|
|
||||||
div {}
|
|
||||||
div {}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dom = new_dom(APP, ());
|
|
||||||
let mutations = dom.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
mutations.edits,
|
|
||||||
[
|
|
||||||
// create template
|
|
||||||
CreateElement { root: Some(1), tag: "template", children: 4 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 0 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 0 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 0 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 0 },
|
|
||||||
// clone template
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![2, 3, 4, 5] },
|
|
||||||
// add to root
|
|
||||||
AppendChildren { root: Some(0), children: vec![2, 3, 4, 5] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn create_components() {
|
|
||||||
static App: Component = |cx| {
|
|
||||||
cx.render(rsx! {
|
|
||||||
Child { "abc1" }
|
|
||||||
Child { "abc2" }
|
|
||||||
Child { "abc3" }
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Props)]
|
|
||||||
struct ChildProps<'a> {
|
|
||||||
children: Element<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
|
|
||||||
cx.render(rsx! {
|
|
||||||
h1 {}
|
|
||||||
div { &cx.props.children }
|
|
||||||
p {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = new_dom(App, ());
|
|
||||||
let mutations = dom.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
mutations.edits,
|
|
||||||
[
|
|
||||||
// create template
|
|
||||||
CreateElement { root: Some(1), tag: "template", children: 3 },
|
|
||||||
CreateElement { root: None, tag: "h1", children: 0 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 0 },
|
|
||||||
CreateElement { root: None, tag: "p", children: 0 },
|
|
||||||
// clone template
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![2, 3, 4] },
|
|
||||||
// update template
|
|
||||||
SetLastNode { id: 2 },
|
|
||||||
NextSibling {},
|
|
||||||
CreateTextNode { root: Some(5), text: "abc1" },
|
|
||||||
SetLastNode { id: 3 },
|
|
||||||
AppendChildren { root: None, children: vec![5] },
|
|
||||||
// clone template
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![6, 7, 8] },
|
|
||||||
SetLastNode { id: 6 },
|
|
||||||
NextSibling {},
|
|
||||||
// update template
|
|
||||||
CreateTextNode { root: Some(9), text: "abc2" },
|
|
||||||
SetLastNode { id: 7 },
|
|
||||||
AppendChildren { root: None, children: vec![9] },
|
|
||||||
// clone template
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![10, 11, 12] },
|
|
||||||
// update template
|
|
||||||
SetLastNode { id: 10 },
|
|
||||||
NextSibling {},
|
|
||||||
CreateTextNode { root: Some(13), text: "abc3" },
|
|
||||||
SetLastNode { id: 11 },
|
|
||||||
AppendChildren { root: None, children: vec![13] },
|
|
||||||
// add to root
|
|
||||||
AppendChildren { root: Some(0), children: vec![2, 3, 4, 6, 7, 8, 10, 11, 12] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn anchors() {
|
|
||||||
static App: Component = |cx| {
|
|
||||||
cx.render(rsx! {
|
|
||||||
{true.then(|| rsx!{ div { "hello" } })}
|
|
||||||
{false.then(|| rsx!{ div { "goodbye" } })}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dom = new_dom(App, ());
|
|
||||||
let mutations = dom.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
mutations.edits,
|
|
||||||
[
|
|
||||||
// create template
|
|
||||||
CreateElement { root: Some(1), tag: "template", children: 1 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 1 },
|
|
||||||
CreateTextNode { root: None, text: "hello" },
|
|
||||||
// clone template
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![2] },
|
|
||||||
CreatePlaceholder { root: Some(3) },
|
|
||||||
// add to root
|
|
||||||
AppendChildren { root: Some(0), children: vec![2, 3] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,831 +0,0 @@
|
|||||||
#![allow(unused, non_upper_case_globals)]
|
|
||||||
|
|
||||||
//! Diffing Tests
|
|
||||||
//!
|
|
||||||
//! These tests only verify that the diffing algorithm works properly for single components.
|
|
||||||
//!
|
|
||||||
//! It does not validated that component lifecycles work properly. This is done in another test file.
|
|
||||||
|
|
||||||
use dioxus::{core_macro::rsx_without_templates, prelude::*};
|
|
||||||
|
|
||||||
fn new_dom() -> VirtualDom {
|
|
||||||
VirtualDom::new(|cx| cx.render(rsx_without_templates!("hi")))
|
|
||||||
}
|
|
||||||
|
|
||||||
use dioxus_core::DomEdit::*;
|
|
||||||
|
|
||||||
/// Should push the text node onto the stack and modify it
|
|
||||||
#[test]
|
|
||||||
fn html_and_rsx_generate_the_same_output() {
|
|
||||||
let dom = new_dom();
|
|
||||||
let (create, change) = dom.diff_lazynodes(
|
|
||||||
rsx_without_templates! ( div { "Hello world" } ),
|
|
||||||
rsx_without_templates! ( div { "Goodbye world" } ),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
create.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(1,), tag: "div", children: 0 },
|
|
||||||
CreateTextNode { root: Some(2,), text: "Hello world" },
|
|
||||||
AppendChildren { root: Some(1,), children: vec![2] },
|
|
||||||
AppendChildren { root: Some(0,), children: vec![1] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[SetText { root: Some(2,), text: "Goodbye world" },]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in 3 elements on the stack
|
|
||||||
#[test]
|
|
||||||
fn fragments_create_properly() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let create = dom.create_vnodes(rsx_without_templates! {
|
|
||||||
div { "Hello a" }
|
|
||||||
div { "Hello b" }
|
|
||||||
div { "Hello c" }
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
create.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(1,), tag: "div", children: 0 },
|
|
||||||
CreateTextNode { root: Some(2,), text: "Hello a" },
|
|
||||||
AppendChildren { root: Some(1,), children: vec![2,] },
|
|
||||||
CreateElement { root: Some(3,), tag: "div", children: 0 },
|
|
||||||
CreateTextNode { root: Some(4,), text: "Hello b" },
|
|
||||||
AppendChildren { root: Some(3,), children: vec![4,] },
|
|
||||||
CreateElement { root: Some(5,), tag: "div", children: 0 },
|
|
||||||
CreateTextNode { root: Some(6,), text: "Hello c" },
|
|
||||||
AppendChildren { root: Some(5,), children: vec![6,] },
|
|
||||||
AppendChildren { root: Some(0,), children: vec![1, 3, 5,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in the creation of an anchor (placeholder) and then a replacewith
|
|
||||||
#[test]
|
|
||||||
fn empty_fragments_create_anchors() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({ (0..0).map(|_f| rsx_without_templates! { div {}}) });
|
|
||||||
let right = rsx_without_templates!({ (0..1).map(|_f| rsx_without_templates! { div {}}) });
|
|
||||||
|
|
||||||
let (create, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
create.edits,
|
|
||||||
[
|
|
||||||
CreatePlaceholder { root: Some(1,) },
|
|
||||||
AppendChildren { root: Some(0,), children: vec![1,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(2,), tag: "div", children: 0 },
|
|
||||||
ReplaceWith { root: Some(1,), nodes: vec![2,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in the creation of an anchor (placeholder) and then a replacewith m=5
|
|
||||||
#[test]
|
|
||||||
fn empty_fragments_create_many_anchors() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({ (0..0).map(|_f| rsx_without_templates! { div {}}) });
|
|
||||||
let right = rsx_without_templates!({ (0..5).map(|_f| rsx_without_templates! { div {}}) });
|
|
||||||
|
|
||||||
let (create, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
create.edits,
|
|
||||||
[
|
|
||||||
CreatePlaceholder { root: Some(1,) },
|
|
||||||
AppendChildren { root: Some(0,), children: vec![1,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(2,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(3,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(4,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(5,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(6,), tag: "div", children: 0 },
|
|
||||||
ReplaceWith { root: Some(1,), nodes: vec![2, 3, 4, 5, 6,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in the creation of an anchor (placeholder) and then a replacewith
|
|
||||||
/// Includes child nodes inside the fragment
|
|
||||||
#[test]
|
|
||||||
fn empty_fragments_create_anchors_with_many_children() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({ (0..0).map(|_| rsx_without_templates! { div {} }) });
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
(0..3).map(|f| {
|
|
||||||
rsx_without_templates! { div { "hello: {f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (create, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
create.edits,
|
|
||||||
[
|
|
||||||
CreatePlaceholder { root: Some(1,) },
|
|
||||||
AppendChildren { root: Some(0,), children: vec![1,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(2,), tag: "div", children: 0 },
|
|
||||||
CreateTextNode { root: Some(3,), text: "hello: 0" },
|
|
||||||
AppendChildren { root: Some(2,), children: vec![3,] },
|
|
||||||
CreateElement { root: Some(4,), tag: "div", children: 0 },
|
|
||||||
CreateTextNode { root: Some(5,), text: "hello: 1" },
|
|
||||||
AppendChildren { root: Some(4,), children: vec![5,] },
|
|
||||||
CreateElement { root: Some(6,), tag: "div", children: 0 },
|
|
||||||
CreateTextNode { root: Some(7,), text: "hello: 2" },
|
|
||||||
AppendChildren { root: Some(6,), children: vec![7,] },
|
|
||||||
ReplaceWith { root: Some(1,), nodes: vec![2, 4, 6,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in every node being pushed and then replaced with an anchor
|
|
||||||
#[test]
|
|
||||||
fn many_items_become_fragment() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
(0..2).map(|_| {
|
|
||||||
rsx_without_templates! { div { "hello" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let right = rsx_without_templates!({ (0..0).map(|_| rsx_without_templates! { div {} }) });
|
|
||||||
|
|
||||||
let (create, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
create.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(1,), tag: "div", children: 0 },
|
|
||||||
CreateTextNode { root: Some(2,), text: "hello" },
|
|
||||||
AppendChildren { root: Some(1,), children: vec![2,] },
|
|
||||||
CreateElement { root: Some(3,), tag: "div", children: 0 },
|
|
||||||
CreateTextNode { root: Some(4,), text: "hello" },
|
|
||||||
AppendChildren { root: Some(3,), children: vec![4,] },
|
|
||||||
AppendChildren { root: Some(0,), children: vec![1, 3,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
CreatePlaceholder { root: Some(5,) },
|
|
||||||
ReplaceWith { root: Some(1,), nodes: vec![5,] },
|
|
||||||
Remove { root: Some(3,) },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in no edits
|
|
||||||
#[test]
|
|
||||||
fn two_equal_fragments_are_equal() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
(0..2).map(|_| {
|
|
||||||
rsx_without_templates! { div { "hello" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
(0..2).map(|_| {
|
|
||||||
rsx_without_templates! { div { "hello" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_create, change) = dom.diff_lazynodes(left, right);
|
|
||||||
assert!(change.edits.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result the creation of more nodes appended after the old last node
|
|
||||||
#[test]
|
|
||||||
fn two_fragments_with_differrent_elements_are_differet() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!(
|
|
||||||
{ (0..2).map(|_| rsx_without_templates! { div { }} ) }
|
|
||||||
p {}
|
|
||||||
);
|
|
||||||
let right = rsx_without_templates!(
|
|
||||||
{ (0..5).map(|_| rsx_without_templates! (h1 { }) ) }
|
|
||||||
p {}
|
|
||||||
);
|
|
||||||
|
|
||||||
let (_create, changes) = dom.diff_lazynodes(left, right);
|
|
||||||
assert_eq!(
|
|
||||||
changes.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(4,), tag: "h1", children: 0 },
|
|
||||||
CreateElement { root: Some(5,), tag: "h1", children: 0 },
|
|
||||||
CreateElement { root: Some(6,), tag: "h1", children: 0 },
|
|
||||||
InsertAfter { root: Some(2,), nodes: vec![4, 5, 6,] },
|
|
||||||
CreateElement { root: Some(7,), tag: "h1", children: 0 },
|
|
||||||
ReplaceWith { root: Some(1,), nodes: vec![7,] }, // notice how 1 gets re-used
|
|
||||||
CreateElement { root: Some(1,), tag: "h1", children: 0 },
|
|
||||||
ReplaceWith { root: Some(2,), nodes: vec![1,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in multiple nodes destroyed - with changes to the first nodes
|
|
||||||
#[test]
|
|
||||||
fn two_fragments_with_differrent_elements_are_differet_shorter() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!(
|
|
||||||
{(0..5).map(|f| {rsx_without_templates! { div { }}})}
|
|
||||||
p {}
|
|
||||||
);
|
|
||||||
let right = rsx_without_templates!(
|
|
||||||
{(0..2).map(|f| {rsx_without_templates! { h1 { }}})}
|
|
||||||
p {}
|
|
||||||
);
|
|
||||||
|
|
||||||
let (create, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
create.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(1,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(2,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(3,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(4,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(5,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(6,), tag: "p", children: 0 },
|
|
||||||
AppendChildren { root: Some(0,), children: vec![1, 2, 3, 4, 5, 6,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// note: key reuse is always the last node that got used
|
|
||||||
// slab maintains a linked list, essentially
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
Remove { root: Some(3,) },
|
|
||||||
Remove { root: Some(4,) },
|
|
||||||
Remove { root: Some(5,) },
|
|
||||||
CreateElement { root: Some(5,), tag: "h1", children: 0 }, // 5 gets reused
|
|
||||||
ReplaceWith { root: Some(1,), nodes: vec![5,] }, // 1 gets deleted
|
|
||||||
CreateElement { root: Some(1,), tag: "h1", children: 0 }, // 1 gets reused
|
|
||||||
ReplaceWith { root: Some(2,), nodes: vec![1,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in multiple nodes destroyed - with no changes
|
|
||||||
#[test]
|
|
||||||
fn two_fragments_with_same_elements_are_differet() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!(
|
|
||||||
{(0..2).map(|f| rsx_without_templates! { div { }})}
|
|
||||||
p {}
|
|
||||||
);
|
|
||||||
let right = rsx_without_templates!(
|
|
||||||
{(0..5).map(|f| rsx_without_templates! { div { }})}
|
|
||||||
p {}
|
|
||||||
);
|
|
||||||
|
|
||||||
let (create, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
create.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(1,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(2,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(3,), tag: "p", children: 0 },
|
|
||||||
AppendChildren { root: Some(0,), children: vec![1, 2, 3,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(4,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(5,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(6,), tag: "div", children: 0 },
|
|
||||||
InsertAfter { root: Some(2,), nodes: vec![4, 5, 6,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// should result in the removal of elements
|
|
||||||
#[test]
|
|
||||||
fn keyed_diffing_order() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!(
|
|
||||||
{(0..5).map(|f| {rsx_without_templates! { div { key: "{f}" }}})}
|
|
||||||
p {"e"}
|
|
||||||
);
|
|
||||||
let right = rsx_without_templates!(
|
|
||||||
{(0..2).map(|f| rsx_without_templates! { div { key: "{f}" }})}
|
|
||||||
p {"e"}
|
|
||||||
);
|
|
||||||
|
|
||||||
let (create, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
Remove { root: Some(3,) },
|
|
||||||
Remove { root: Some(4,) },
|
|
||||||
Remove { root: Some(5,) },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in moves, but not removals or additions
|
|
||||||
#[test]
|
|
||||||
fn keyed_diffing_out_of_order() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_, changes) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
changes.edits,
|
|
||||||
[InsertBefore { root: Some(5,), nodes: vec![7,] },]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in moves only
|
|
||||||
#[test]
|
|
||||||
fn keyed_diffing_out_of_order_adds() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[/**/ 8, 7, 4, 5, 6 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[InsertBefore { root: Some(1,), nodes: vec![5, 4,] },]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/// Should result in moves only
|
|
||||||
#[test]
|
|
||||||
fn keyed_diffing_out_of_order_adds_2() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[/**/ 7, 8, 4, 5, 6 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[InsertBefore { root: Some(1,), nodes: vec![4, 5,] },]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in moves onl
|
|
||||||
#[test]
|
|
||||||
fn keyed_diffing_out_of_order_adds_3() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[/**/ 4, 8, 7, 5, 6 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[InsertBefore { root: Some(2,), nodes: vec![5, 4,] },]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in moves onl
|
|
||||||
#[test]
|
|
||||||
fn keyed_diffing_out_of_order_adds_4() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[/**/ 4, 5, 8, 7, 6 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[InsertBefore { root: Some(3), nodes: vec![5, 4,] },]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should result in moves onl
|
|
||||||
#[test]
|
|
||||||
fn keyed_diffing_out_of_order_adds_5() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[/**/ 4, 5, 6, 8, 7 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_, change) = dom.diff_lazynodes(left, right);
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[InsertBefore { root: Some(4), nodes: vec![5] }]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn keyed_diffing_additions() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[/**/ 4, 5, 6, 7, 8, 9, 10 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_, change) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(6,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(7,), tag: "div", children: 0 },
|
|
||||||
InsertAfter { root: Some(5,), nodes: vec![6, 7,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn keyed_diffing_additions_and_moves_on_ends() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[/**/ 4, 5, 6, 7 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_, change) = dom.diff_lazynodes(left, right);
|
|
||||||
println!("{:?}", change);
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
// create 11, 12
|
|
||||||
CreateElement { root: Some(5), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(6), tag: "div", children: 0 },
|
|
||||||
InsertAfter { root: Some(3), nodes: vec![5, 6] },
|
|
||||||
// // move 7 to the front
|
|
||||||
InsertBefore { root: Some(1), nodes: vec![4] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn keyed_diffing_additions_and_moves_in_middle() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[/**/ 1, 2, 3, 4 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// LIS: 4, 5, 6
|
|
||||||
let (_, change) = dom.diff_lazynodes(left, right);
|
|
||||||
println!("{:#?}", change);
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
// create 5, 6
|
|
||||||
CreateElement { root: Some(5,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(6,), tag: "div", children: 0 },
|
|
||||||
InsertBefore { root: Some(3,), nodes: vec![5, 6,] },
|
|
||||||
// create 7, 8
|
|
||||||
CreateElement { root: Some(7,), tag: "div", children: 0 },
|
|
||||||
CreateElement { root: Some(8,), tag: "div", children: 0 },
|
|
||||||
InsertBefore { root: Some(2,), nodes: vec![7, 8,] },
|
|
||||||
// move 7
|
|
||||||
InsertBefore { root: Some(1,), nodes: vec![4,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn controlled_keyed_diffing_out_of_order() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[4, 5, 6, 7].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[0, 5, 9, 6, 4].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// LIS: 5, 6
|
|
||||||
let (_, changes) = dom.diff_lazynodes(left, right);
|
|
||||||
println!("{:#?}", &changes);
|
|
||||||
assert_eq!(
|
|
||||||
changes.edits,
|
|
||||||
[
|
|
||||||
// remove 7
|
|
||||||
Remove { root: Some(4,) },
|
|
||||||
// move 4 to after 6
|
|
||||||
InsertAfter { root: Some(3,), nodes: vec![1,] },
|
|
||||||
// create 9 and insert before 6
|
|
||||||
CreateElement { root: Some(4,), tag: "div", children: 0 },
|
|
||||||
InsertBefore { root: Some(3,), nodes: vec![4,] },
|
|
||||||
// create 0 and insert before 5
|
|
||||||
CreateElement { root: Some(5,), tag: "div", children: 0 },
|
|
||||||
InsertBefore { root: Some(2,), nodes: vec![5,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn controlled_keyed_diffing_out_of_order_max_test() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
[0, 1, 2, 3, 4].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
[3, 0, 1, 10, 2].iter().map(|f| {
|
|
||||||
rsx_without_templates! { div { key: "{f}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_, changes) = dom.diff_lazynodes(left, right);
|
|
||||||
println!("{:#?}", &changes);
|
|
||||||
assert_eq!(
|
|
||||||
changes.edits,
|
|
||||||
[
|
|
||||||
Remove { root: Some(5,) },
|
|
||||||
CreateElement { root: Some(5,), tag: "div", children: 0 },
|
|
||||||
InsertBefore { root: Some(3,), nodes: vec![5,] },
|
|
||||||
InsertBefore { root: Some(1,), nodes: vec![4,] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// noticed some weird behavior in the desktop interpreter
|
|
||||||
// just making sure it doesnt happen in the core implementation
|
|
||||||
#[test]
|
|
||||||
fn remove_list() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
(0..10).rev().take(5).map(|i| {
|
|
||||||
rsx_without_templates! { Fragment { key: "{i}", "{i}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
(0..10).rev().take(2).map(|i| {
|
|
||||||
rsx_without_templates! { Fragment { key: "{i}", "{i}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (create, changes) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
// dbg!(create);
|
|
||||||
// dbg!(changes);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
changes.edits,
|
|
||||||
// remove 5, 4, 3
|
|
||||||
[
|
|
||||||
Remove { root: Some(3) },
|
|
||||||
Remove { root: Some(4) },
|
|
||||||
Remove { root: Some(5) }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// noticed some weird behavior in the desktop interpreter
|
|
||||||
// just making sure it doesnt happen in the core implementation
|
|
||||||
#[test]
|
|
||||||
fn remove_list_nokeyed() {
|
|
||||||
let dom = new_dom();
|
|
||||||
|
|
||||||
let left = rsx_without_templates!({
|
|
||||||
(0..10).rev().take(5).map(|i| {
|
|
||||||
rsx_without_templates! { Fragment { "{i}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let right = rsx_without_templates!({
|
|
||||||
(0..10).rev().take(2).map(|i| {
|
|
||||||
rsx_without_templates! { Fragment { "{i}" }}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (create, changes) = dom.diff_lazynodes(left, right);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
changes.edits,
|
|
||||||
[
|
|
||||||
// remove 5, 4, 3
|
|
||||||
Remove { root: Some(3) },
|
|
||||||
Remove { root: Some(4) },
|
|
||||||
Remove { root: Some(5) },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn add_nested_elements() {
|
|
||||||
let vdom = new_dom();
|
|
||||||
|
|
||||||
let (_create, change) = vdom.diff_lazynodes(
|
|
||||||
rsx_without_templates! {
|
|
||||||
div{}
|
|
||||||
},
|
|
||||||
rsx_without_templates! {
|
|
||||||
div{
|
|
||||||
div{}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(2), tag: "div", children: 0 },
|
|
||||||
AppendChildren { root: Some(1), children: vec![2] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn add_listeners() {
|
|
||||||
let vdom = new_dom();
|
|
||||||
|
|
||||||
let (_create, change) = vdom.diff_lazynodes(
|
|
||||||
rsx_without_templates! {
|
|
||||||
div{}
|
|
||||||
},
|
|
||||||
rsx_without_templates! {
|
|
||||||
div{
|
|
||||||
onkeyup: |_| {},
|
|
||||||
onkeydown: |_| {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
NewEventListener { event_name: "keyup", scope: ScopeId(0), root: Some(1) },
|
|
||||||
NewEventListener { event_name: "keydown", scope: ScopeId(0), root: Some(1) },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn remove_listeners() {
|
|
||||||
let vdom = new_dom();
|
|
||||||
|
|
||||||
let (_create, change) = vdom.diff_lazynodes(
|
|
||||||
rsx_without_templates! {
|
|
||||||
div{
|
|
||||||
onkeyup: |_| {},
|
|
||||||
onkeydown: |_| {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rsx_without_templates! {
|
|
||||||
div{}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
RemoveEventListener { event: "keyup", root: Some(1) },
|
|
||||||
RemoveEventListener { event: "keydown", root: Some(1) },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn diff_listeners() {
|
|
||||||
let vdom = new_dom();
|
|
||||||
|
|
||||||
let (_create, change) = vdom.diff_lazynodes(
|
|
||||||
rsx_without_templates! {
|
|
||||||
div{
|
|
||||||
onkeydown: |_| {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rsx_without_templates! {
|
|
||||||
div{
|
|
||||||
onkeyup: |_| {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
change.edits,
|
|
||||||
[
|
|
||||||
RemoveEventListener { root: Some(1), event: "keydown" },
|
|
||||||
NewEventListener { event_name: "keyup", scope: ScopeId(0), root: Some(1) }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#![allow(unused, non_upper_case_globals, non_snake_case)]
|
|
||||||
|
|
||||||
//! Prove that the dom works normally through virtualdom methods.
|
|
||||||
//!
|
|
||||||
//! This methods all use "rebuild" which completely bypasses the scheduler.
|
|
||||||
//! Hard rebuilds don't consume any events from the event queue.
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
|
|
||||||
use dioxus_core::{DomEdit::*, ScopeId};
|
|
||||||
|
|
||||||
const IS_LOGGING_ENABLED: bool = false;
|
|
||||||
|
|
||||||
fn new_dom<P: 'static + Send>(app: Component<P>, props: P) -> VirtualDom {
|
|
||||||
VirtualDom::new_with_props(app, props)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This test ensures that if a component aborts early, it is replaced with a placeholder.
|
|
||||||
/// In debug, this should also toss a warning.
|
|
||||||
#[test]
|
|
||||||
fn test_early_abort() {
|
|
||||||
const app: Component = |cx| {
|
|
||||||
let val = cx.use_hook(|| 0);
|
|
||||||
|
|
||||||
*val += 1;
|
|
||||||
|
|
||||||
if *val == 2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
render!(div { "Hello, world!" })
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dom = new_dom(app, ());
|
|
||||||
|
|
||||||
let edits = dom.rebuild();
|
|
||||||
assert_eq!(
|
|
||||||
edits.edits,
|
|
||||||
[
|
|
||||||
// create template
|
|
||||||
CreateElement { root: Some(1), tag: "template", children: 1 },
|
|
||||||
CreateElement { root: None, tag: "div", children: 1 },
|
|
||||||
CreateTextNode { root: None, text: "Hello, world!" },
|
|
||||||
// clone template
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![2] },
|
|
||||||
AppendChildren { root: Some(0), children: vec![2] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
let edits = dom.hard_diff(ScopeId(0));
|
|
||||||
assert_eq!(
|
|
||||||
edits.edits,
|
|
||||||
[
|
|
||||||
CreatePlaceholder { root: Some(3) },
|
|
||||||
ReplaceWith { root: Some(2), nodes: vec![3] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
let edits = dom.hard_diff(ScopeId(0));
|
|
||||||
assert_eq!(
|
|
||||||
edits.edits,
|
|
||||||
[
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![2] },
|
|
||||||
ReplaceWith { root: Some(3), nodes: vec![2] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
#![allow(unused, non_upper_case_globals)]
|
|
||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
//! Tests for the lifecycle of components.
|
|
||||||
use dioxus::{core_macro::rsx_without_templates, prelude::*};
|
|
||||||
use dioxus_core::DomEdit::*;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
type Shared<T> = Arc<Mutex<T>>;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn manual_diffing() {
|
|
||||||
struct AppProps {
|
|
||||||
value: Shared<&'static str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
static App: Component<AppProps> = |cx| {
|
|
||||||
let val = cx.props.value.lock().unwrap();
|
|
||||||
cx.render(rsx_without_templates! { div { "{val}" } })
|
|
||||||
};
|
|
||||||
|
|
||||||
let value = Arc::new(Mutex::new("Hello"));
|
|
||||||
let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone() });
|
|
||||||
|
|
||||||
let _ = dom.rebuild();
|
|
||||||
|
|
||||||
*value.lock().unwrap() = "goodbye";
|
|
||||||
|
|
||||||
let edits = dom.rebuild();
|
|
||||||
|
|
||||||
println!("edits: {:?}", edits);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn events_generate() {
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
let count = cx.use_hook(|| 0);
|
|
||||||
|
|
||||||
let inner = match *count {
|
|
||||||
0 => {
|
|
||||||
rsx_without_templates! {
|
|
||||||
div {
|
|
||||||
onclick: move |_| *count += 1,
|
|
||||||
div {
|
|
||||||
"nested"
|
|
||||||
}
|
|
||||||
"Click me!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => todo!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.render(inner)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
let mut channel = dom.get_scheduler_channel();
|
|
||||||
assert!(dom.has_work());
|
|
||||||
|
|
||||||
let edits = dom.rebuild();
|
|
||||||
assert_eq!(
|
|
||||||
edits.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(1), tag: "div", children: 0 },
|
|
||||||
NewEventListener { event_name: "click", scope: ScopeId(0), root: Some(1) },
|
|
||||||
CreateElement { root: Some(2), tag: "div", children: 0 },
|
|
||||||
CreateTextNode { root: Some(3), text: "nested" },
|
|
||||||
AppendChildren { root: Some(2), children: vec![3] },
|
|
||||||
CreateTextNode { root: Some(4), text: "Click me!" },
|
|
||||||
AppendChildren { root: Some(1), children: vec![2, 4] },
|
|
||||||
AppendChildren { root: Some(0), children: vec![1] }
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn components_generate() {
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
let render_phase = cx.use_hook(|| 0);
|
|
||||||
*render_phase += 1;
|
|
||||||
|
|
||||||
cx.render(match *render_phase {
|
|
||||||
1 => rsx_without_templates!("Text0"),
|
|
||||||
2 => rsx_without_templates!(div {}),
|
|
||||||
3 => rsx_without_templates!("Text2"),
|
|
||||||
4 => rsx_without_templates!(Child {}),
|
|
||||||
5 => rsx_without_templates!({ None as Option<()> }),
|
|
||||||
6 => rsx_without_templates!("text 3"),
|
|
||||||
7 => rsx_without_templates!({ (0..2).map(|f| rsx_without_templates!("text {f}")) }),
|
|
||||||
8 => rsx_without_templates!(Child {}),
|
|
||||||
_ => todo!(),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
fn Child(cx: Scope) -> Element {
|
|
||||||
println!("Running child");
|
|
||||||
cx.render(rsx_without_templates! {
|
|
||||||
h1 {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
let edits = dom.rebuild();
|
|
||||||
assert_eq!(
|
|
||||||
edits.edits,
|
|
||||||
[
|
|
||||||
CreateTextNode { root: Some(1), text: "Text0" },
|
|
||||||
AppendChildren { root: Some(0), children: vec![1] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
dom.hard_diff(ScopeId(0)).edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(2), tag: "div", children: 0 },
|
|
||||||
ReplaceWith { root: Some(1), nodes: vec![2] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
dom.hard_diff(ScopeId(0)).edits,
|
|
||||||
[
|
|
||||||
CreateTextNode { root: Some(1), text: "Text2" },
|
|
||||||
ReplaceWith { root: Some(2), nodes: vec![1] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// child {}
|
|
||||||
assert_eq!(
|
|
||||||
dom.hard_diff(ScopeId(0)).edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(2), tag: "h1", children: 0 },
|
|
||||||
ReplaceWith { root: Some(1), nodes: vec![2] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// placeholder
|
|
||||||
assert_eq!(
|
|
||||||
dom.hard_diff(ScopeId(0)).edits,
|
|
||||||
[
|
|
||||||
CreatePlaceholder { root: Some(1) },
|
|
||||||
ReplaceWith { root: Some(2), nodes: vec![1] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
dom.hard_diff(ScopeId(0)).edits,
|
|
||||||
[
|
|
||||||
CreateTextNode { root: Some(2), text: "text 3" },
|
|
||||||
ReplaceWith { root: Some(1), nodes: vec![2] }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
dom.hard_diff(ScopeId(0)).edits,
|
|
||||||
[
|
|
||||||
CreateTextNode { text: "text 0", root: Some(1) },
|
|
||||||
CreateTextNode { text: "text 1", root: Some(3) },
|
|
||||||
ReplaceWith { root: Some(2), nodes: vec![1, 3] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
dom.hard_diff(ScopeId(0)).edits,
|
|
||||||
[
|
|
||||||
CreateElement { tag: "h1", root: Some(2), children: 0 },
|
|
||||||
ReplaceWith { root: Some(1), nodes: vec![2] },
|
|
||||||
Remove { root: Some(3) },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn component_swap() {
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
let render_phase = cx.use_hook(|| 0);
|
|
||||||
*render_phase += 1;
|
|
||||||
|
|
||||||
cx.render(match *render_phase {
|
|
||||||
0 => rsx_without_templates!(
|
|
||||||
div {
|
|
||||||
NavBar {}
|
|
||||||
Dashboard {}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
1 => rsx_without_templates!(
|
|
||||||
div {
|
|
||||||
NavBar {}
|
|
||||||
Results {}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
2 => rsx_without_templates!(
|
|
||||||
div {
|
|
||||||
NavBar {}
|
|
||||||
Dashboard {}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
3 => rsx_without_templates!(
|
|
||||||
div {
|
|
||||||
NavBar {}
|
|
||||||
Results {}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
4 => rsx_without_templates!(
|
|
||||||
div {
|
|
||||||
NavBar {}
|
|
||||||
Dashboard {}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
_ => rsx_without_templates!("blah"),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
static NavBar: Component = |cx| {
|
|
||||||
println!("running navbar");
|
|
||||||
cx.render(rsx_without_templates! {
|
|
||||||
h1 {
|
|
||||||
"NavBar"
|
|
||||||
{(0..3).map(|f| rsx_without_templates!(NavLink {}))}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
static NavLink: Component = |cx| {
|
|
||||||
println!("running navlink");
|
|
||||||
cx.render(rsx_without_templates! {
|
|
||||||
h1 {
|
|
||||||
"NavLink"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
static Dashboard: Component = |cx| {
|
|
||||||
println!("running dashboard");
|
|
||||||
cx.render(rsx_without_templates! {
|
|
||||||
div {
|
|
||||||
"dashboard"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
static Results: Component = |cx| {
|
|
||||||
println!("running results");
|
|
||||||
cx.render(rsx_without_templates! {
|
|
||||||
div {
|
|
||||||
"results"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
let edits = dom.rebuild();
|
|
||||||
dbg!(&edits);
|
|
||||||
|
|
||||||
let edits = dom.work_with_deadline(|| false);
|
|
||||||
dbg!(&edits);
|
|
||||||
let edits = dom.work_with_deadline(|| false);
|
|
||||||
dbg!(&edits);
|
|
||||||
let edits = dom.work_with_deadline(|| false);
|
|
||||||
dbg!(&edits);
|
|
||||||
let edits = dom.work_with_deadline(|| false);
|
|
||||||
dbg!(&edits);
|
|
||||||
}
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
#![allow(non_snake_case)]
|
|
||||||
/*
|
|
||||||
Stress Miri as much as possible.
|
|
||||||
|
|
||||||
Prove that we don't leak memory and that our methods are safe.
|
|
||||||
|
|
||||||
Specifically:
|
|
||||||
- [ ] VirtualDom drops memory safely
|
|
||||||
- [ ] Borrowed components don't expose invalid pointers
|
|
||||||
- [ ] Async isn't busted
|
|
||||||
*/
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
use dioxus_core::SchedulerMsg;
|
|
||||||
|
|
||||||
fn new_dom<P: 'static + Send>(app: Component<P>, props: P) -> VirtualDom {
|
|
||||||
VirtualDom::new_with_props(app, props)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This test ensures that if a component aborts early, it is replaced with a placeholder.
|
|
||||||
/// In debug, this should also toss a warning.
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn test_memory_leak() {
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
let val = cx.use_hook(|| 0);
|
|
||||||
|
|
||||||
*val += 1;
|
|
||||||
|
|
||||||
if *val == 2 || *val == 4 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = cx.use_hook(|| String::from("asd"));
|
|
||||||
|
|
||||||
cx.render(rsx!(
|
|
||||||
div { "Hello, world!" }
|
|
||||||
Child {}
|
|
||||||
Child {}
|
|
||||||
Child {}
|
|
||||||
Child {}
|
|
||||||
Child {}
|
|
||||||
Child {}
|
|
||||||
BorrowedChild { na: name }
|
|
||||||
BorrowedChild { na: name }
|
|
||||||
BorrowedChild { na: name }
|
|
||||||
BorrowedChild { na: name }
|
|
||||||
BorrowedChild { na: name }
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Props)]
|
|
||||||
struct BorrowedProps<'a> {
|
|
||||||
na: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn BorrowedChild<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element {
|
|
||||||
render!(div {
|
|
||||||
"goodbye {cx.props.na}"
|
|
||||||
Child {}
|
|
||||||
Child {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn Child(cx: Scope) -> Element {
|
|
||||||
render!(div { "goodbye world" })
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = new_dom(app, ());
|
|
||||||
|
|
||||||
dom.rebuild();
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn memo_works_properly() {
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
let val = cx.use_hook(|| 0);
|
|
||||||
|
|
||||||
*val += 1;
|
|
||||||
|
|
||||||
if *val == 2 || *val == 4 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = cx.use_hook(|| String::from("asd"));
|
|
||||||
|
|
||||||
cx.render(rsx!(
|
|
||||||
div { "Hello, world! {name}" }
|
|
||||||
Child { na: "asdfg".to_string() }
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Props)]
|
|
||||||
struct ChildProps {
|
|
||||||
na: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn Child(cx: Scope<ChildProps>) -> Element {
|
|
||||||
render!(div { "goodbye world" })
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = new_dom(app, ());
|
|
||||||
|
|
||||||
dom.rebuild();
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
dom.hard_diff(ScopeId(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn free_works_on_root_props() {
|
|
||||||
fn app(cx: Scope<Custom>) -> Element {
|
|
||||||
cx.render(rsx! {
|
|
||||||
Child { a: "alpha"}
|
|
||||||
Child { a: "beta"}
|
|
||||||
Child { a: "gamma"}
|
|
||||||
Child { a: "delta"}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Props, PartialEq)]
|
|
||||||
struct ChildProps {
|
|
||||||
a: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn Child(cx: Scope<ChildProps>) -> Element {
|
|
||||||
render!("child {cx.props.a}")
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Custom {
|
|
||||||
val: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Custom {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
dbg!("dropped! {}", &self.val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = new_dom(app, Custom { val: String::from("asd") });
|
|
||||||
dom.rebuild();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn free_works_on_borrowed() {
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
cx.render(rsx! {
|
|
||||||
Child { a: "alpha", b: "asd".to_string() }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
#[derive(Props)]
|
|
||||||
struct ChildProps<'a> {
|
|
||||||
a: &'a str,
|
|
||||||
b: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
|
|
||||||
dbg!("rendering child");
|
|
||||||
render!("child {cx.props.a}, {cx.props.b}")
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for ChildProps<'_> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
dbg!("dropped child!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = new_dom(app, ());
|
|
||||||
let _ = dom.rebuild();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn free_works_on_root_hooks() {
|
|
||||||
/*
|
|
||||||
On Drop, scopearena drops all the hook contents.
|
|
||||||
*/
|
|
||||||
|
|
||||||
struct Droppable<T>(T);
|
|
||||||
impl<T> Drop for Droppable<T> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
dbg!("dropping droppable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
let name = cx.use_hook(|| Droppable(String::from("asd")));
|
|
||||||
render!(div { "{name.0}" })
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = new_dom(app, ());
|
|
||||||
let _ = dom.rebuild();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn old_props_arent_stale() {
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
dbg!("rendering parent");
|
|
||||||
let cnt = cx.use_hook(|| 0);
|
|
||||||
*cnt += 1;
|
|
||||||
|
|
||||||
if *cnt == 1 {
|
|
||||||
render!(div { Child { a: "abcdef".to_string() } })
|
|
||||||
} else {
|
|
||||||
render!(div { Child { a: "abcdef".to_string() } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Props, PartialEq)]
|
|
||||||
struct ChildProps {
|
|
||||||
a: String,
|
|
||||||
}
|
|
||||||
fn Child(cx: Scope<ChildProps>) -> Element {
|
|
||||||
dbg!("rendering child", &cx.props.a);
|
|
||||||
render!(div { "child {cx.props.a}" })
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = new_dom(app, ());
|
|
||||||
let _ = dom.rebuild();
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
dbg!("forcing update to child");
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic() {
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
render!(div {
|
|
||||||
Child { a: "abcdef".to_string() }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Props, PartialEq)]
|
|
||||||
struct ChildProps {
|
|
||||||
a: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn Child(cx: Scope<ChildProps>) -> Element {
|
|
||||||
dbg!("rendering child", &cx.props.a);
|
|
||||||
render!(div { "child {cx.props.a}" })
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = new_dom(app, ());
|
|
||||||
let _ = dom.rebuild();
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn leak_thru_children() {
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
cx.render(rsx! {
|
|
||||||
Child {
|
|
||||||
name: "asd".to_string(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cx.render(rsx! {
|
|
||||||
div {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Child(cx: Scope, name: String) -> Element {
|
|
||||||
render!(div { "child {name}" })
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = new_dom(app, ());
|
|
||||||
let _ = dom.rebuild();
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pass_thru() {
|
|
||||||
#[inline_props]
|
|
||||||
fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element {
|
|
||||||
cx.render(rsx! {
|
|
||||||
header {
|
|
||||||
nav { children }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn NavMenu(cx: Scope) -> Element {
|
|
||||||
render!( NavBrand {}
|
|
||||||
div {
|
|
||||||
NavStart {}
|
|
||||||
NavEnd {}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn NavBrand(cx: Scope) -> Element {
|
|
||||||
render!(div {})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn NavStart(cx: Scope) -> Element {
|
|
||||||
render!(div {})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn NavEnd(cx: Scope) -> Element {
|
|
||||||
render!(div {})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn MainContainer<'a>(
|
|
||||||
cx: Scope,
|
|
||||||
nav: Element<'a>,
|
|
||||||
body: Element<'a>,
|
|
||||||
footer: Element<'a>,
|
|
||||||
) -> Element {
|
|
||||||
cx.render(rsx! {
|
|
||||||
div {
|
|
||||||
class: "columns is-mobile",
|
|
||||||
div {
|
|
||||||
class: "column is-full",
|
|
||||||
nav,
|
|
||||||
body,
|
|
||||||
footer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
let nav = cx.render(rsx! {
|
|
||||||
NavContainer {
|
|
||||||
NavMenu {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let body = cx.render(rsx! {
|
|
||||||
div {}
|
|
||||||
});
|
|
||||||
let footer = cx.render(rsx! {
|
|
||||||
div {}
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.render(rsx! {
|
|
||||||
MainContainer {
|
|
||||||
nav: nav,
|
|
||||||
body: body,
|
|
||||||
footer: footer,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = new_dom(app, ());
|
|
||||||
let _ = dom.rebuild();
|
|
||||||
|
|
||||||
for _ in 0..40 {
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
fn basic_syntax_is_a_template(cx: Scope) -> Element {
|
|
||||||
let asd = 123;
|
|
||||||
let var = 123;
|
|
||||||
|
|
||||||
cx.render(rsx! {
|
|
||||||
div { key: "12345",
|
|
||||||
class: "asd",
|
|
||||||
class: "{asd}",
|
|
||||||
onclick: move |_| {},
|
|
||||||
div { "{var}" }
|
|
||||||
div {
|
|
||||||
h1 { "var" }
|
|
||||||
p { "you're great!" }
|
|
||||||
div { background_color: "red",
|
|
||||||
h1 { "var" }
|
|
||||||
div { b { "asd" } "not great" }
|
|
||||||
}
|
|
||||||
p { "you're great!" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// imports come after the test since the rsx! macro is sensitive to its location in the file
|
|
||||||
// (the byte index is used to differentiate sub templates)
|
|
||||||
use dioxus::core::{ElementId, Mutation};
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dual_stream() {
|
|
||||||
let mut dom = VirtualDom::new(basic_syntax_is_a_template);
|
|
||||||
let edits = dom.rebuild();
|
|
||||||
|
|
||||||
use Mutation::*;
|
|
||||||
assert_eq!(
|
|
||||||
edits.template_mutations,
|
|
||||||
vec![
|
|
||||||
CreateElement { name: "div", namespace: None, id: ElementId(1) },
|
|
||||||
SetAttribute { name: "class", value: "asd", id: ElementId(1) },
|
|
||||||
CreateElement { name: "div", namespace: None, id: ElementId(2) },
|
|
||||||
CreatePlaceholder { id: ElementId(3) },
|
|
||||||
AppendChildren { m: 1 },
|
|
||||||
CreateElement { name: "div", namespace: None, id: ElementId(4) },
|
|
||||||
CreateElement { name: "h1", namespace: None, id: ElementId(5) },
|
|
||||||
CreateTextNode { value: "var" },
|
|
||||||
AppendChildren { m: 1 },
|
|
||||||
CreateElement { name: "p", namespace: None, id: ElementId(6) },
|
|
||||||
CreateTextNode { value: "you're great!" },
|
|
||||||
AppendChildren { m: 1 },
|
|
||||||
CreateElement { name: "div", namespace: None, id: ElementId(7) },
|
|
||||||
SetAttribute { name: "background-color", value: "red", id: ElementId(7) },
|
|
||||||
CreateElement { name: "h1", namespace: None, id: ElementId(8) },
|
|
||||||
CreateTextNode { value: "var" },
|
|
||||||
AppendChildren { m: 1 },
|
|
||||||
CreateElement { name: "div", namespace: None, id: ElementId(9) },
|
|
||||||
CreateElement { name: "b", namespace: None, id: ElementId(10) },
|
|
||||||
CreateTextNode { value: "asd" },
|
|
||||||
AppendChildren { m: 1 },
|
|
||||||
CreateTextNode { value: "not great" },
|
|
||||||
AppendChildren { m: 2 },
|
|
||||||
AppendChildren { m: 2 },
|
|
||||||
CreateElement { name: "p", namespace: None, id: ElementId(11) },
|
|
||||||
CreateTextNode { value: "you're great!" },
|
|
||||||
AppendChildren { m: 1 },
|
|
||||||
AppendChildren { m: 4 },
|
|
||||||
AppendChildren { m: 2 },
|
|
||||||
SaveTemplate { name: "packages/dioxus/tests/rsx_syntax.rs:5:15:122", m: 1 }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
edits.edits,
|
|
||||||
vec![
|
|
||||||
LoadTemplate { name: "packages/dioxus/tests/rsx_syntax.rs:5:15:122", index: 0 },
|
|
||||||
AssignId { path: &[], id: ElementId(12) },
|
|
||||||
SetAttribute { name: "class", value: "123", id: ElementId(12) },
|
|
||||||
SetAttribute { name: "onclick", value: "asd", id: ElementId(12) }, // ---- todo: listeners
|
|
||||||
HydrateText { path: &[0, 0], value: "123", id: ElementId(13) },
|
|
||||||
ReplacePlaceholder { m: 1, path: &[0, 0] },
|
|
||||||
AppendChildren { m: 1 }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#![allow(unused, non_upper_case_globals, non_snake_case)]
|
|
||||||
|
|
||||||
use dioxus::{core_macro::rsx_without_templates, prelude::*};
|
|
||||||
use dioxus_core::{DomEdit, Mutations, SchedulerMsg, ScopeId};
|
|
||||||
use std::rc::Rc;
|
|
||||||
use DomEdit::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn shared_state_test() {
|
|
||||||
struct MySharedState(&'static str);
|
|
||||||
|
|
||||||
static App: Component = |cx| {
|
|
||||||
cx.provide_context(Rc::new(MySharedState("world!")));
|
|
||||||
cx.render(rsx_without_templates!(Child {}))
|
|
||||||
};
|
|
||||||
|
|
||||||
static Child: Component = |cx| {
|
|
||||||
let shared = cx.consume_context::<Rc<MySharedState>>()?;
|
|
||||||
cx.render(rsx_without_templates!("Hello, {shared.0}"))
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(App);
|
|
||||||
let Mutations { edits, .. } = dom.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
edits,
|
|
||||||
[
|
|
||||||
CreateTextNode { root: Some(1), text: "Hello, world!" },
|
|
||||||
AppendChildren { root: Some(0), children: vec![1] },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn swap_test() {
|
|
||||||
struct MySharedState(&'static str);
|
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
let val = cx.use_hook(|| 0);
|
|
||||||
*val += 1;
|
|
||||||
|
|
||||||
cx.provide_context(Rc::new(MySharedState("world!")));
|
|
||||||
|
|
||||||
let child = match *val % 2 {
|
|
||||||
0 => rsx_without_templates!(
|
|
||||||
Child1 {
|
|
||||||
Child1 { }
|
|
||||||
Child2 { }
|
|
||||||
}
|
|
||||||
),
|
|
||||||
_ => rsx_without_templates!(
|
|
||||||
Child2 {
|
|
||||||
Child2 { }
|
|
||||||
Child2 { }
|
|
||||||
}
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.render(rsx_without_templates!(
|
|
||||||
Router {
|
|
||||||
div { child }
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Router<'a>(cx: Scope, children: Element<'a>) -> Element<'a> {
|
|
||||||
cx.render(rsx_without_templates!(div { children }))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Child1<'a>(cx: Scope, children: Element<'a>) -> Element {
|
|
||||||
let shared = cx.consume_context::<Rc<MySharedState>>().unwrap();
|
|
||||||
println!("Child1: {}", shared.0);
|
|
||||||
cx.render(rsx_without_templates! {
|
|
||||||
div {
|
|
||||||
"{shared.0}",
|
|
||||||
children
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Child2<'a>(cx: Scope, children: Element<'a>) -> Element {
|
|
||||||
let shared = cx.consume_context::<Rc<MySharedState>>().unwrap();
|
|
||||||
println!("Child2: {}", shared.0);
|
|
||||||
cx.render(rsx_without_templates! {
|
|
||||||
h1 {
|
|
||||||
"{shared.0}",
|
|
||||||
children
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
let Mutations { edits, .. } = dom.rebuild();
|
|
||||||
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
|
||||||
// dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
|
|
||||||
// dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
|
|
||||||
// dom.work_with_deadline(|| false);
|
|
||||||
|
|
||||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
// dom.work_with_deadline(|| false);
|
|
||||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
// dom.work_with_deadline(|| false);
|
|
||||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
|
||||||
// dom.work_with_deadline(|| false);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
use std::{future::IntoFuture, rc::Rc};
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
use dioxus_core::SuspenseBoundary;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn basic_prints() {
|
|
||||||
let mut dom = VirtualDom::new(|cx| {
|
|
||||||
cx.render(rsx! {
|
|
||||||
div {
|
|
||||||
h1 { "var" }
|
|
||||||
suspense_boundary {
|
|
||||||
basic_child { }
|
|
||||||
async_child { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
dbg!(dom.rebuild());
|
|
||||||
|
|
||||||
dom.wait_for_work().await;
|
|
||||||
|
|
||||||
dbg!(dom.rebuild());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn suspense_boundary<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
|
|
||||||
cx.use_hook(|| cx.provide_context(Rc::new(SuspenseBoundary::new(cx.scope_id()))));
|
|
||||||
cx.render(rsx! { children })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn basic_child(cx: Scope) -> Element {
|
|
||||||
cx.render(rsx!( div { "basic child 1" } ))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn async_child(cx: Scope<'_>) -> Element {
|
|
||||||
let username = use_future!(cx, || async {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
||||||
"async child 1"
|
|
||||||
});
|
|
||||||
|
|
||||||
let age = use_future!(cx, || async {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
|
||||||
println!("long future completed");
|
|
||||||
1234
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_user, _age) = use_future!(cx, || async {
|
|
||||||
tokio::join!(
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(1)),
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(2))
|
|
||||||
);
|
|
||||||
("async child 1", 1234)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let (username, age) = tokio::join!(username.into_future(), age.into_future());
|
|
||||||
|
|
||||||
cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } ))
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
#![allow(unused, non_upper_case_globals)]
|
|
||||||
|
|
||||||
//! Rebuilding tests
|
|
||||||
//! ----------------
|
|
||||||
//!
|
|
||||||
//! This tests module ensures that the initial build of the virtualdom is correct.
|
|
||||||
//! This does not include dynamic tests or the diffing algorithm itself.
|
|
||||||
//!
|
|
||||||
//! It does prove that mounting works properly and the correct edit streams are generated.
|
|
||||||
//!
|
|
||||||
//! Don't have a good way to validate, everything is done manually ATM
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
use dioxus_core::DomEdit::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn app_runs() {
|
|
||||||
static App: Component = |cx| render!(div{"hello"} );
|
|
||||||
|
|
||||||
let mut vdom = VirtualDom::new(App);
|
|
||||||
let edits = vdom.rebuild();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fragments_work() {
|
|
||||||
static App: Component = |cx| {
|
|
||||||
cx.render(rsx!(
|
|
||||||
div{"hello"}
|
|
||||||
div{"goodbye"}
|
|
||||||
))
|
|
||||||
};
|
|
||||||
let mut vdom = VirtualDom::new(App);
|
|
||||||
let edits = vdom.rebuild();
|
|
||||||
// should result in a final "appendchildren n=2"
|
|
||||||
dbg!(edits);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lists_work() {
|
|
||||||
static App: Component = |cx| {
|
|
||||||
cx.render(rsx!(
|
|
||||||
h1 {"hello"}
|
|
||||||
(0..6).map(|f| rsx!(span{ "{f}" }))
|
|
||||||
))
|
|
||||||
};
|
|
||||||
let mut vdom = VirtualDom::new(App);
|
|
||||||
let edits = vdom.rebuild();
|
|
||||||
dbg!(edits);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn conditional_rendering() {
|
|
||||||
static App: Component = |cx| {
|
|
||||||
cx.render(rsx!(
|
|
||||||
h1 {"hello"}
|
|
||||||
{true.then(|| rsx!(span{ "a" }))}
|
|
||||||
{false.then(|| rsx!(span{ "b" }))}
|
|
||||||
))
|
|
||||||
};
|
|
||||||
let mut vdom = VirtualDom::new(App);
|
|
||||||
|
|
||||||
let mutations = vdom.rebuild();
|
|
||||||
assert_eq!(
|
|
||||||
mutations.edits,
|
|
||||||
[
|
|
||||||
CreateElement { root: Some(1), tag: "template", children: 3 },
|
|
||||||
CreateElement { root: None, tag: "h1", children: 1 },
|
|
||||||
CreateTextNode { root: None, text: "hello" },
|
|
||||||
CreatePlaceholder { root: None },
|
|
||||||
CreatePlaceholder { root: None },
|
|
||||||
CloneNodeChildren { id: Some(1), new_ids: vec![2, 3, 4] },
|
|
||||||
CreateElement { root: Some(5), tag: "template", children: 1 },
|
|
||||||
CreateElement { root: None, tag: "span", children: 1 },
|
|
||||||
CreateTextNode { root: None, text: "a" },
|
|
||||||
CloneNodeChildren { id: Some(5), new_ids: vec![6] },
|
|
||||||
SetLastNode { id: 3 },
|
|
||||||
ReplaceWith { root: None, nodes: vec![6] },
|
|
||||||
CreatePlaceholder { root: Some(7) },
|
|
||||||
SetLastNode { id: 4 },
|
|
||||||
ReplaceWith { root: None, nodes: vec![7] },
|
|
||||||
AppendChildren { root: Some(0), children: vec![2, 3, 4] }
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn child_components() {
|
|
||||||
static App: Component = |cx| {
|
|
||||||
cx.render(rsx!(
|
|
||||||
{true.then(|| rsx!(Child { }))}
|
|
||||||
{false.then(|| rsx!(Child { }))}
|
|
||||||
))
|
|
||||||
};
|
|
||||||
static Child: Component = |cx| {
|
|
||||||
cx.render(rsx!(
|
|
||||||
h1 {"hello"}
|
|
||||||
h1 {"goodbye"}
|
|
||||||
))
|
|
||||||
};
|
|
||||||
let mut vdom = VirtualDom::new(App);
|
|
||||||
let edits = vdom.rebuild();
|
|
||||||
dbg!(edits);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,28 @@
|
|||||||
// #![deny(missing_docs)]
|
#[macro_export]
|
||||||
//! Useful foundational hooks for Dioxus
|
/// A helper macro for using hooks in async environements.
|
||||||
|
///
|
||||||
|
/// # Usage
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let (data) = use_ref(&cx, || {});
|
||||||
|
///
|
||||||
|
/// let handle_thing = move |_| {
|
||||||
|
/// to_owned![data]
|
||||||
|
/// cx.spawn(async move {
|
||||||
|
/// // do stuff
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
macro_rules! to_owned {
|
||||||
|
($($es:ident),+) => {$(
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut $es = $es.to_owned();
|
||||||
|
)*}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod usecontext;
|
||||||
|
pub use usecontext::*;
|
||||||
|
|
||||||
mod usestate;
|
mod usestate;
|
||||||
pub use usestate::{use_state, UseState};
|
pub use usestate::{use_state, UseState};
|
||||||
@@ -7,8 +30,8 @@ pub use usestate::{use_state, UseState};
|
|||||||
mod useref;
|
mod useref;
|
||||||
pub use useref::*;
|
pub use useref::*;
|
||||||
|
|
||||||
mod use_shared_state;
|
// mod use_shared_state;
|
||||||
pub use use_shared_state::*;
|
// pub use use_shared_state::*;
|
||||||
|
|
||||||
mod usecoroutine;
|
mod usecoroutine;
|
||||||
pub use usecoroutine::*;
|
pub use usecoroutine::*;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ impl<T> ProvidedStateInner<T> {
|
|||||||
pub fn use_context<T: 'static>(cx: &ScopeState) -> Option<UseSharedState<T>> {
|
pub fn use_context<T: 'static>(cx: &ScopeState) -> Option<UseSharedState<T>> {
|
||||||
let state = cx.use_hook(|| {
|
let state = cx.use_hook(|| {
|
||||||
let scope_id = cx.scope_id();
|
let scope_id = cx.scope_id();
|
||||||
let root = cx.consume_context::<ProvidedState<T>>();
|
let root = cx.consume_context::<ProvidedState<T>>().cloned();
|
||||||
|
|
||||||
if let Some(root) = root.as_ref() {
|
if let Some(root) = root.as_ref() {
|
||||||
root.borrow_mut().consumers.insert(scope_id);
|
root.borrow_mut().consumers.insert(scope_id);
|
||||||
@@ -179,6 +179,7 @@ pub fn use_context_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T)
|
|||||||
notify_any: cx.schedule_update_any(),
|
notify_any: cx.schedule_update_any(),
|
||||||
consumers: HashSet::new(),
|
consumers: HashSet::new(),
|
||||||
}));
|
}));
|
||||||
cx.provide_context(state)
|
|
||||||
|
cx.provide_context(state);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use dioxus_core::ScopeState;
|
||||||
|
|
||||||
|
/// Consume some context in the tree
|
||||||
|
pub fn use_context<T: 'static>(cx: &ScopeState) -> Option<&T> {
|
||||||
|
match *cx.use_hook(|| cx.consume_context::<T>().map(|t| t as *const T)) {
|
||||||
|
Some(res) => Some(unsafe { &*res }),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provide some context via the tree and return a reference to it
|
||||||
|
///
|
||||||
|
/// Once the context has been provided, it is immutable. Mutations should be done via interior mutability.
|
||||||
|
pub fn use_context_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) -> &T {
|
||||||
|
let ptr = *cx.use_hook(|| cx.provide_context(f()) as *const T);
|
||||||
|
unsafe { &*ptr }
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ use dioxus_core::{ScopeState, TaskId};
|
|||||||
pub use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
pub use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
|
use crate::{use_context, use_context_provider};
|
||||||
|
|
||||||
/// Maintain a handle over a future that can be paused, resumed, and canceled.
|
/// Maintain a handle over a future that can be paused, resumed, and canceled.
|
||||||
///
|
///
|
||||||
/// This is an upgraded form of [`use_future`] with an integrated channel system.
|
/// This is an upgraded form of [`use_future`] with an integrated channel system.
|
||||||
@@ -59,33 +61,33 @@ use std::future::Future;
|
|||||||
/// }
|
/// }
|
||||||
/// })
|
/// })
|
||||||
/// ```
|
/// ```
|
||||||
pub fn use_coroutine<M, G, F>(cx: &ScopeState, init: G) -> &CoroutineHandle<M>
|
pub fn use_coroutine<M, G, F>(cx: &ScopeState, init: G) -> &Coroutine<M>
|
||||||
where
|
where
|
||||||
M: 'static,
|
M: 'static,
|
||||||
G: FnOnce(UnboundedReceiver<M>) -> F,
|
G: FnOnce(UnboundedReceiver<M>) -> F,
|
||||||
F: Future<Output = ()> + 'static,
|
F: Future<Output = ()> + 'static,
|
||||||
{
|
{
|
||||||
cx.use_hook(|| {
|
use_context_provider(cx, || {
|
||||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||||
let task = cx.push_future(init(rx));
|
let task = cx.push_future(init(rx));
|
||||||
cx.provide_context(CoroutineHandle { tx, task })
|
Coroutine { tx, task }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle to a coroutine higher in the tree
|
/// Get a handle to a coroutine higher in the tree
|
||||||
///
|
///
|
||||||
/// See the docs for [`use_coroutine`] for more details.
|
/// See the docs for [`use_coroutine`] for more details.
|
||||||
pub fn use_coroutine_handle<M: 'static>(cx: &ScopeState) -> Option<&CoroutineHandle<M>> {
|
pub fn use_coroutine_handle<M: 'static>(cx: &ScopeState) -> Option<&Coroutine<M>> {
|
||||||
cx.use_hook(|| cx.consume_context::<CoroutineHandle<M>>())
|
use_context::<Coroutine<M>>(cx)
|
||||||
.as_ref()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CoroutineHandle<T> {
|
pub struct Coroutine<T> {
|
||||||
tx: UnboundedSender<T>,
|
tx: UnboundedSender<T>,
|
||||||
task: TaskId,
|
task: TaskId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Clone for CoroutineHandle<T> {
|
// for use in futures
|
||||||
|
impl<T> Clone for Coroutine<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tx: self.tx.clone(),
|
tx: self.tx.clone(),
|
||||||
@@ -94,7 +96,7 @@ impl<T> Clone for CoroutineHandle<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> CoroutineHandle<T> {
|
impl<T> Coroutine<T> {
|
||||||
/// Get the ID of this coroutine
|
/// Get the ID of this coroutine
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn task_id(&self) -> TaskId {
|
pub fn task_id(&self) -> TaskId {
|
||||||
@@ -112,8 +114,8 @@ mod tests {
|
|||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use dioxus_core::exports::futures_channel::mpsc::unbounded;
|
|
||||||
use dioxus_core::prelude::*;
|
use dioxus_core::prelude::*;
|
||||||
|
use futures_channel::mpsc::unbounded;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
|
|
||||||
fn app(cx: Scope, name: String) -> Element {
|
fn app(cx: Scope, name: String) -> Element {
|
||||||
@@ -127,7 +129,7 @@ mod tests {
|
|||||||
|
|
||||||
let task3 = use_coroutine(&cx, |rx| complex_task(rx, 10));
|
let task3 = use_coroutine(&cx, |rx| complex_task(rx, 10));
|
||||||
|
|
||||||
None
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn view_task(mut rx: UnboundedReceiver<i32>) {
|
async fn view_task(mut rx: UnboundedReceiver<i32>) {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ mod tests {
|
|||||||
//
|
//
|
||||||
});
|
});
|
||||||
|
|
||||||
None
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,7 +343,7 @@ mod tests {
|
|||||||
|
|
||||||
let g = fut.await;
|
let g = fut.await;
|
||||||
|
|
||||||
None
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ pub fn use_state<T: 'static>(
|
|||||||
let update_callback = cx.schedule_update();
|
let update_callback = cx.schedule_update();
|
||||||
let slot = Rc::new(RefCell::new(current_val.clone()));
|
let slot = Rc::new(RefCell::new(current_val.clone()));
|
||||||
let setter = Rc::new({
|
let setter = Rc::new({
|
||||||
dioxus_core::to_owned![update_callback, slot];
|
to_owned![update_callback, slot];
|
||||||
move |new| {
|
move |new| {
|
||||||
{
|
{
|
||||||
let mut slot = slot.borrow_mut();
|
let mut slot = slot.borrow_mut();
|
||||||
@@ -466,7 +466,7 @@ fn api_makes_sense() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
dioxus_core::to_owned![val];
|
to_owned![val];
|
||||||
async move {
|
async move {
|
||||||
val.modify(|f| f + 1);
|
val.modify(|f| f + 1);
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-89
@@ -11,10 +11,10 @@ macro_rules! impl_event {
|
|||||||
) => {
|
) => {
|
||||||
$(
|
$(
|
||||||
$( #[$attr] )*
|
$( #[$attr] )*
|
||||||
pub fn $name<'a>(_cx: &'a ::dioxus_core::ScopeState, _f: impl FnMut(::dioxus_core::UiEvent<$data>) + 'a) -> ::dioxus_core::Attribute<'a> {
|
pub fn $name<'a>(_cx: &'a ::dioxus_core::ScopeState, _f: impl FnMut(::dioxus_core::Event<$data>) + 'a) -> ::dioxus_core::Attribute<'a> {
|
||||||
::dioxus_core::Attribute {
|
::dioxus_core::Attribute {
|
||||||
name: stringify!($name),
|
name: stringify!($name),
|
||||||
value: ::dioxus_core::AttributeValue::new_listener(_cx, _f),
|
value: _cx.listener(_f),
|
||||||
namespace: None,
|
namespace: None,
|
||||||
mounted_element: Default::default(),
|
mounted_element: Default::default(),
|
||||||
volatile: false,
|
volatile: false,
|
||||||
@@ -60,90 +60,90 @@ pub use touch::*;
|
|||||||
pub use transition::*;
|
pub use transition::*;
|
||||||
pub use wheel::*;
|
pub use wheel::*;
|
||||||
|
|
||||||
// pub fn event_bubbles(evt: &str) -> bool {
|
pub fn event_bubbles(evt: &str) -> bool {
|
||||||
// match evt {
|
match evt {
|
||||||
// "copy" => true,
|
"copy" => true,
|
||||||
// "cut" => true,
|
"cut" => true,
|
||||||
// "paste" => true,
|
"paste" => true,
|
||||||
// "compositionend" => true,
|
"compositionend" => true,
|
||||||
// "compositionstart" => true,
|
"compositionstart" => true,
|
||||||
// "compositionupdate" => true,
|
"compositionupdate" => true,
|
||||||
// "keydown" => true,
|
"keydown" => true,
|
||||||
// "keypress" => true,
|
"keypress" => true,
|
||||||
// "keyup" => true,
|
"keyup" => true,
|
||||||
// "focus" => false,
|
"focus" => false,
|
||||||
// "focusout" => true,
|
"focusout" => true,
|
||||||
// "focusin" => true,
|
"focusin" => true,
|
||||||
// "blur" => false,
|
"blur" => false,
|
||||||
// "change" => true,
|
"change" => true,
|
||||||
// "input" => true,
|
"input" => true,
|
||||||
// "invalid" => true,
|
"invalid" => true,
|
||||||
// "reset" => true,
|
"reset" => true,
|
||||||
// "submit" => true,
|
"submit" => true,
|
||||||
// "click" => true,
|
"click" => true,
|
||||||
// "contextmenu" => true,
|
"contextmenu" => true,
|
||||||
// "doubleclick" => true,
|
"doubleclick" => true,
|
||||||
// "dblclick" => true,
|
"dblclick" => true,
|
||||||
// "drag" => true,
|
"drag" => true,
|
||||||
// "dragend" => true,
|
"dragend" => true,
|
||||||
// "dragenter" => false,
|
"dragenter" => false,
|
||||||
// "dragexit" => false,
|
"dragexit" => false,
|
||||||
// "dragleave" => true,
|
"dragleave" => true,
|
||||||
// "dragover" => true,
|
"dragover" => true,
|
||||||
// "dragstart" => true,
|
"dragstart" => true,
|
||||||
// "drop" => true,
|
"drop" => true,
|
||||||
// "mousedown" => true,
|
"mousedown" => true,
|
||||||
// "mouseenter" => false,
|
"mouseenter" => false,
|
||||||
// "mouseleave" => false,
|
"mouseleave" => false,
|
||||||
// "mousemove" => true,
|
"mousemove" => true,
|
||||||
// "mouseout" => true,
|
"mouseout" => true,
|
||||||
// "scroll" => false,
|
"scroll" => false,
|
||||||
// "mouseover" => true,
|
"mouseover" => true,
|
||||||
// "mouseup" => true,
|
"mouseup" => true,
|
||||||
// "pointerdown" => true,
|
"pointerdown" => true,
|
||||||
// "pointermove" => true,
|
"pointermove" => true,
|
||||||
// "pointerup" => true,
|
"pointerup" => true,
|
||||||
// "pointercancel" => true,
|
"pointercancel" => true,
|
||||||
// "gotpointercapture" => true,
|
"gotpointercapture" => true,
|
||||||
// "lostpointercapture" => true,
|
"lostpointercapture" => true,
|
||||||
// "pointerenter" => false,
|
"pointerenter" => false,
|
||||||
// "pointerleave" => false,
|
"pointerleave" => false,
|
||||||
// "pointerover" => true,
|
"pointerover" => true,
|
||||||
// "pointerout" => true,
|
"pointerout" => true,
|
||||||
// "select" => true,
|
"select" => true,
|
||||||
// "touchcancel" => true,
|
"touchcancel" => true,
|
||||||
// "touchend" => true,
|
"touchend" => true,
|
||||||
// "touchmove" => true,
|
"touchmove" => true,
|
||||||
// "touchstart" => true,
|
"touchstart" => true,
|
||||||
// "wheel" => true,
|
"wheel" => true,
|
||||||
// "abort" => false,
|
"abort" => false,
|
||||||
// "canplay" => false,
|
"canplay" => false,
|
||||||
// "canplaythrough" => false,
|
"canplaythrough" => false,
|
||||||
// "durationchange" => false,
|
"durationchange" => false,
|
||||||
// "emptied" => false,
|
"emptied" => false,
|
||||||
// "encrypted" => true,
|
"encrypted" => true,
|
||||||
// "ended" => false,
|
"ended" => false,
|
||||||
// "error" => false,
|
"error" => false,
|
||||||
// "loadeddata" => false,
|
"loadeddata" => false,
|
||||||
// "loadedmetadata" => false,
|
"loadedmetadata" => false,
|
||||||
// "loadstart" => false,
|
"loadstart" => false,
|
||||||
// "pause" => false,
|
"pause" => false,
|
||||||
// "play" => false,
|
"play" => false,
|
||||||
// "playing" => false,
|
"playing" => false,
|
||||||
// "progress" => false,
|
"progress" => false,
|
||||||
// "ratechange" => false,
|
"ratechange" => false,
|
||||||
// "seeked" => false,
|
"seeked" => false,
|
||||||
// "seeking" => false,
|
"seeking" => false,
|
||||||
// "stalled" => false,
|
"stalled" => false,
|
||||||
// "suspend" => false,
|
"suspend" => false,
|
||||||
// "timeupdate" => false,
|
"timeupdate" => false,
|
||||||
// "volumechange" => false,
|
"volumechange" => false,
|
||||||
// "waiting" => false,
|
"waiting" => false,
|
||||||
// "animationstart" => true,
|
"animationstart" => true,
|
||||||
// "animationend" => true,
|
"animationend" => true,
|
||||||
// "animationiteration" => true,
|
"animationiteration" => true,
|
||||||
// "transitionend" => true,
|
"transitionend" => true,
|
||||||
// "toggle" => true,
|
"toggle" => true,
|
||||||
// _ => panic!("unsupported event type {:?}", evt),
|
_ => true,
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type AnimationEvent = UiEvent<AnimationData>;
|
pub type AnimationEvent = Event<AnimationData>;
|
||||||
|
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type ClipboardEvent = UiEvent<ClipboardData>;
|
pub type ClipboardEvent = Event<ClipboardData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ClipboardData {
|
pub struct ClipboardData {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type CompositionEvent = UiEvent<CompositionData>;
|
pub type CompositionEvent = Event<CompositionData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CompositionData {
|
pub struct CompositionData {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
use crate::MouseData;
|
use crate::MouseData;
|
||||||
|
|
||||||
pub type DragEvent = UiEvent<DragData>;
|
pub type DragEvent = Event<DragData>;
|
||||||
|
|
||||||
/// The DragEvent interface is a DOM event that represents a drag and drop interaction. The user initiates a drag by
|
/// The DragEvent interface is a DOM event that represents a drag and drop interaction. The user initiates a drag by
|
||||||
/// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location
|
/// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type FocusEvent = UiEvent<FocusData>;
|
pub type FocusEvent = Event<FocusData>;
|
||||||
|
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::{collections::HashMap, fmt::Debug, sync::Arc};
|
use std::{collections::HashMap, fmt::Debug, sync::Arc};
|
||||||
|
|
||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type FormEvent = UiEvent<FormData>;
|
pub type FormEvent = Event<FormData>;
|
||||||
|
|
||||||
/* DOMEvent: Send + SyncTarget relatedTarget */
|
/* DOMEvent: Send + SyncTarget relatedTarget */
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type ImageEvent = UiEvent<ImageData>;
|
pub type ImageEvent = Event<ImageData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ImageData {
|
pub struct ImageData {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use crate::input_data::{decode_key_location, encode_key_location};
|
use crate::input_data::{decode_key_location, encode_key_location};
|
||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
use keyboard_types::{Code, Key, Location, Modifiers};
|
use keyboard_types::{Code, Key, Location, Modifiers};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
pub type KeyboardEvent = UiEvent<KeyboardData>;
|
pub type KeyboardEvent = Event<KeyboardData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct KeyboardData {
|
pub struct KeyboardData {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type MediaEvent = UiEvent<MediaData>;
|
pub type MediaEvent = Event<MediaData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MediaData {}
|
pub struct MediaData {}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenP
|
|||||||
use crate::input_data::{
|
use crate::input_data::{
|
||||||
decode_mouse_button_set, encode_mouse_button_set, MouseButton, MouseButtonSet,
|
decode_mouse_button_set, encode_mouse_button_set, MouseButton, MouseButtonSet,
|
||||||
};
|
};
|
||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
use keyboard_types::Modifiers;
|
use keyboard_types::Modifiers;
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
|
|
||||||
pub type MouseEvent = UiEvent<MouseData>;
|
pub type MouseEvent = Event<MouseData>;
|
||||||
|
|
||||||
/// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)
|
/// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Default)]
|
||||||
/// Data associated with a mouse event
|
/// Data associated with a mouse event
|
||||||
///
|
///
|
||||||
/// Do not use the deprecated fields; they may change or become private in the future.
|
/// Do not use the deprecated fields; they may change or become private in the future.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type PointerEvent = UiEvent<PointerData>;
|
pub type PointerEvent = Event<PointerData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PointerData {
|
pub struct PointerData {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type ScrollEvent = UiEvent<ScrollData>;
|
pub type ScrollEvent = Event<ScrollData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ScrollData {}
|
pub struct ScrollData {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type SelectionEvent = UiEvent<SelectionData>;
|
pub type SelectionEvent = Event<SelectionData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SelectionData {}
|
pub struct SelectionData {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type ToggleEvent = UiEvent<ToggleData>;
|
pub type ToggleEvent = Event<ToggleData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ToggleData {}
|
pub struct ToggleData {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type TouchEvent = UiEvent<TouchData>;
|
pub type TouchEvent = Event<TouchData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TouchData {
|
pub struct TouchData {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
|
|
||||||
pub type TransitionEvent = UiEvent<TransitionData>;
|
pub type TransitionEvent = Event<TransitionData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TransitionData {
|
pub struct TransitionData {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use dioxus_core::UiEvent;
|
use dioxus_core::Event;
|
||||||
use euclid::UnknownUnit;
|
use euclid::UnknownUnit;
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
|
|
||||||
use crate::geometry::{LinesVector, PagesVector, PixelsVector, WheelDelta};
|
use crate::geometry::{LinesVector, PagesVector, PixelsVector, WheelDelta};
|
||||||
|
|
||||||
pub type WheelEvent = UiEvent<WheelData>;
|
pub type WheelEvent = Event<WheelData>;
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WheelData {
|
pub struct WheelData {
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ pub mod events;
|
|||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
mod global_attributes;
|
mod global_attributes;
|
||||||
pub mod input_data;
|
pub mod input_data;
|
||||||
// #[cfg(feature = "wasm-bind")]
|
mod render_template;
|
||||||
// mod web_sys_bind;
|
#[cfg(feature = "wasm-bind")]
|
||||||
|
mod web_sys_bind;
|
||||||
|
|
||||||
pub use elements::*;
|
pub use elements::*;
|
||||||
pub use events::*;
|
pub use events::*;
|
||||||
pub use global_attributes::*;
|
pub use global_attributes::*;
|
||||||
|
pub use render_template::*;
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::events::*;
|
pub use crate::events::*;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use dioxus_core::{Template, TemplateAttribute, TemplateNode};
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
/// Render a template to an HTML string
|
||||||
|
///
|
||||||
|
/// Useful for sending over the wire. Can be used to with innerHtml to create templates with little work
|
||||||
|
pub fn render_template_to_html(template: &Template) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
|
||||||
|
for root in template.roots {
|
||||||
|
render_template_node(root, &mut out).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_template_node(node: &TemplateNode, out: &mut String) -> std::fmt::Result {
|
||||||
|
match node {
|
||||||
|
TemplateNode::Element {
|
||||||
|
tag,
|
||||||
|
attrs,
|
||||||
|
children,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
write!(out, "<{tag}")?;
|
||||||
|
for attr in *attrs {
|
||||||
|
if let TemplateAttribute::Static { name, value, .. } = attr {
|
||||||
|
write!(out, "{}=\"{}\"", name, value)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for child in *children {
|
||||||
|
render_template_node(child, out)?;
|
||||||
|
}
|
||||||
|
write!(out, "</{tag}>")?;
|
||||||
|
}
|
||||||
|
TemplateNode::Text(t) => write!(out, "{t}")?,
|
||||||
|
TemplateNode::Dynamic(_) => write!(out, "<pre hidden />")?,
|
||||||
|
TemplateNode::DynamicText(t) => write!(out, "<!-- --> {t} <!-- -->")?,
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
|
use crate::events::{
|
||||||
use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
|
|
||||||
use crate::on::{
|
|
||||||
AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData,
|
AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData,
|
||||||
TransitionData, WheelData,
|
TransitionData, WheelData,
|
||||||
};
|
};
|
||||||
|
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
|
||||||
|
use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
|
||||||
use keyboard_types::{Code, Key, Modifiers};
|
use keyboard_types::{Code, Key, Modifiers};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|||||||
@@ -12,86 +12,65 @@ extern "C" {
|
|||||||
pub fn new(arg: Element) -> Interpreter;
|
pub fn new(arg: Element) -> Interpreter;
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn SetNode(this: &Interpreter, id: usize, node: Node);
|
pub fn SaveTemplate(this: &Interpreter, nodes: Vec<Node>, name: &str);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn AppendChildren(this: &Interpreter, root: Option<u64>, children: Vec<u64>);
|
pub fn MountToRoot(this: &Interpreter);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn ReplaceWith(this: &Interpreter, root: Option<u64>, nodes: Vec<u64>);
|
pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn InsertAfter(this: &Interpreter, root: Option<u64>, nodes: Vec<u64>);
|
pub fn CreatePlaceholder(this: &Interpreter, id: u32);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn InsertBefore(this: &Interpreter, root: Option<u64>, nodes: Vec<u64>);
|
pub fn CreateTextNode(this: &Interpreter, value: JsValue, id: u32);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn Remove(this: &Interpreter, root: Option<u64>);
|
pub fn HydrateText(this: &Interpreter, path: &[u8], value: &str, id: u32);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn CreateTextNode(this: &Interpreter, text: JsValue, root: Option<u64>);
|
pub fn LoadTemplate(this: &Interpreter, name: &str, index: u32, id: u32);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn CreateElement(this: &Interpreter, tag: &str, root: Option<u64>, children: u32);
|
pub fn ReplaceWith(this: &Interpreter, id: u32, m: u32);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn CreateElementNs(
|
pub fn ReplacePlaceholder(this: &Interpreter, path: &[u8], m: u32);
|
||||||
this: &Interpreter,
|
|
||||||
tag: &str,
|
|
||||||
root: Option<u64>,
|
|
||||||
ns: &str,
|
|
||||||
children: u32,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn CreatePlaceholder(this: &Interpreter, root: Option<u64>);
|
pub fn InsertAfter(this: &Interpreter, id: u32, n: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn InsertBefore(this: &Interpreter, id: u32, n: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn SetAttribute(this: &Interpreter, id: u32, name: &str, value: JsValue, ns: Option<&str>);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn SetBoolAttribute(this: &Interpreter, id: u32, name: &str, value: bool);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn SetText(this: &Interpreter, id: u32, text: JsValue);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn NewEventListener(
|
pub fn NewEventListener(
|
||||||
this: &Interpreter,
|
this: &Interpreter,
|
||||||
name: &str,
|
name: &str,
|
||||||
root: Option<u64>,
|
id: u32,
|
||||||
handler: &Function,
|
handler: &Function,
|
||||||
bubbles: bool,
|
bubbles: bool,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn RemoveEventListener(this: &Interpreter, root: Option<u64>, name: &str, bubbles: bool);
|
pub fn RemoveEventListener(this: &Interpreter, name: &str, id: u32);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn SetText(this: &Interpreter, root: Option<u64>, text: JsValue);
|
pub fn RemoveAttribute(this: &Interpreter, id: u32, field: &str, ns: Option<&str>);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn SetAttribute(
|
pub fn Remove(this: &Interpreter, id: u32);
|
||||||
this: &Interpreter,
|
|
||||||
root: Option<u64>,
|
|
||||||
field: &str,
|
|
||||||
value: JsValue,
|
|
||||||
ns: Option<&str>,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn RemoveAttribute(this: &Interpreter, root: Option<u64>, field: &str, ns: Option<&str>);
|
pub fn PushRoot(this: &Interpreter, id: u32);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
|
||||||
pub fn CloneNode(this: &Interpreter, root: Option<u64>, new_id: u64);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
|
||||||
pub fn CloneNodeChildren(this: &Interpreter, root: Option<u64>, new_ids: Vec<u64>);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
|
||||||
pub fn FirstChild(this: &Interpreter);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
|
||||||
pub fn NextSibling(this: &Interpreter);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
|
||||||
pub fn ParentNode(this: &Interpreter);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
|
||||||
pub fn StoreWithId(this: &Interpreter, id: u64);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
|
||||||
pub fn SetLastNode(this: &Interpreter, id: u64);
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
export function main() {
|
export function main() {
|
||||||
let root = window.document.getElementById("main");
|
let root = window.document.getElementById("main");
|
||||||
console.log("loading!");
|
|
||||||
if (root != null) {
|
if (root != null) {
|
||||||
window.interpreter = new Interpreter(root);
|
window.interpreter = new Interpreter(root);
|
||||||
console.log("properly loaded!");
|
|
||||||
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +16,6 @@ class ListenerMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create(event_name, element, handler, bubbles) {
|
create(event_name, element, handler, bubbles) {
|
||||||
console.log("creating listener for", event_name, element, handler, bubbles);
|
|
||||||
if (bubbles) {
|
if (bubbles) {
|
||||||
if (this.global[event_name] === undefined) {
|
if (this.global[event_name] === undefined) {
|
||||||
this.global[event_name] = {};
|
this.global[event_name] = {};
|
||||||
@@ -79,6 +76,12 @@ export class Interpreter {
|
|||||||
pop() {
|
pop() {
|
||||||
return this.stack.pop();
|
return this.stack.pop();
|
||||||
}
|
}
|
||||||
|
SaveTemplate(nodes, name) {
|
||||||
|
this.templates[name] = nodes;
|
||||||
|
}
|
||||||
|
MountToRoot() {
|
||||||
|
this.AppendChildren(this.stack.length - 1);
|
||||||
|
}
|
||||||
SetNode(id, node) {
|
SetNode(id, node) {
|
||||||
this.nodes[id] = node;
|
this.nodes[id] = node;
|
||||||
}
|
}
|
||||||
@@ -124,25 +127,13 @@ export class Interpreter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
CreateRawText(text) {
|
CreateRawText(text) {
|
||||||
const node = document.createTextNode(text);
|
this.stack.push(document.createTextNode(text));
|
||||||
this.stack.push(node);
|
|
||||||
}
|
}
|
||||||
CreateTextNode(text, root) {
|
CreateTextNode(text, root) {
|
||||||
const node = document.createTextNode(text);
|
const node = document.createTextNode(text);
|
||||||
this.nodes[root] = node;
|
this.nodes[root] = node;
|
||||||
this.stack.push(node);
|
this.stack.push(node);
|
||||||
}
|
}
|
||||||
CreateElement(tag, root) {
|
|
||||||
const el = document.createElement(tag);
|
|
||||||
this.nodes[root] = el;
|
|
||||||
this.stack.push(el);
|
|
||||||
}
|
|
||||||
CreateElementNs(tag, root, ns) {
|
|
||||||
console.log("creating element", tag, root, ns);
|
|
||||||
let el = document.createElementNS(ns, tag);
|
|
||||||
this.stack.push(el);
|
|
||||||
this.nodes[root] = el;
|
|
||||||
}
|
|
||||||
CreatePlaceholder(root) {
|
CreatePlaceholder(root) {
|
||||||
let el = document.createElement("pre");
|
let el = document.createElement("pre");
|
||||||
el.hidden = true;
|
el.hidden = true;
|
||||||
@@ -162,9 +153,12 @@ export class Interpreter {
|
|||||||
SetText(root, text) {
|
SetText(root, text) {
|
||||||
this.nodes[root].textContent = text;
|
this.nodes[root].textContent = text;
|
||||||
}
|
}
|
||||||
SetAttribute(root, field, value, ns) {
|
SetAttribute(id, field, value, ns) {
|
||||||
|
const node = this.nodes[id];
|
||||||
|
this.SetAttributeInner(node, field, value, ns);
|
||||||
|
}
|
||||||
|
SetAttributeInner(node, field, value, ns) {
|
||||||
const name = field;
|
const name = field;
|
||||||
const node = this.nodes[root];
|
|
||||||
if (ns === "style") {
|
if (ns === "style") {
|
||||||
// ????? why do we need to do this
|
// ????? why do we need to do this
|
||||||
if (node.style === undefined) {
|
if (node.style === undefined) {
|
||||||
@@ -219,8 +213,6 @@ export class Interpreter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleEdits(edits) {
|
handleEdits(edits) {
|
||||||
console.log("handling edits", edits, this.stack.length);
|
|
||||||
|
|
||||||
for (let edit of edits) {
|
for (let edit of edits) {
|
||||||
this.handleEdit(edit);
|
this.handleEdit(edit);
|
||||||
}
|
}
|
||||||
@@ -248,14 +240,11 @@ export class Interpreter {
|
|||||||
let node = this.LoadChild(path);
|
let node = this.LoadChild(path);
|
||||||
node.replaceWith(...els);
|
node.replaceWith(...els);
|
||||||
}
|
}
|
||||||
LoadTemplate(name, index) {
|
LoadTemplate(name, index, id) {
|
||||||
console.log("loading template", name, index);
|
|
||||||
let node = this.templates[name][index].cloneNode(true);
|
let node = this.templates[name][index].cloneNode(true);
|
||||||
|
this.nodes[id] = node;
|
||||||
this.stack.push(node);
|
this.stack.push(node);
|
||||||
}
|
}
|
||||||
SaveTemplate(name, m) {
|
|
||||||
this.templates[name] = this.stack.splice(this.stack.length - m);
|
|
||||||
}
|
|
||||||
handleEdit(edit) {
|
handleEdit(edit) {
|
||||||
switch (edit.type) {
|
switch (edit.type) {
|
||||||
case "AppendChildren":
|
case "AppendChildren":
|
||||||
@@ -264,25 +253,18 @@ export class Interpreter {
|
|||||||
case "AssignId":
|
case "AssignId":
|
||||||
this.AssignId(edit.path, edit.id);
|
this.AssignId(edit.path, edit.id);
|
||||||
break;
|
break;
|
||||||
case "CreateElement":
|
|
||||||
if (edit.namespace !== null && edit.namespace !== undefined) {
|
|
||||||
this.CreateElementNs(edit.name, edit.id, edit.namespace);
|
|
||||||
} else {
|
|
||||||
this.CreateElement(edit.name, edit.id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "CreatePlaceholder":
|
case "CreatePlaceholder":
|
||||||
this.CreatePlaceholder(edit.id);
|
this.CreatePlaceholder(edit.id);
|
||||||
break;
|
break;
|
||||||
case "CreateTextNode":
|
case "CreateTextNode":
|
||||||
this.CreateTextNode(edit.value);
|
this.CreateTextNode(edit.value);
|
||||||
break;
|
break;
|
||||||
case "CreateStaticText":
|
|
||||||
this.CreateTextNode(edit.value);
|
|
||||||
break;
|
|
||||||
case "HydrateText":
|
case "HydrateText":
|
||||||
this.HydrateText(edit.path, edit.value, edit.id);
|
this.HydrateText(edit.path, edit.value, edit.id);
|
||||||
break;
|
break;
|
||||||
|
case "LoadTemplate":
|
||||||
|
this.LoadTemplate(edit.name, edit.index, edit.id);
|
||||||
|
break;
|
||||||
case "PushRoot":
|
case "PushRoot":
|
||||||
this.PushRoot(edit.id);
|
this.PushRoot(edit.id);
|
||||||
break;
|
break;
|
||||||
@@ -293,29 +275,23 @@ export class Interpreter {
|
|||||||
this.ReplacePlaceholder(edit.path, edit.m);
|
this.ReplacePlaceholder(edit.path, edit.m);
|
||||||
break;
|
break;
|
||||||
case "InsertAfter":
|
case "InsertAfter":
|
||||||
this.InsertAfter(edit.id, edit.n);
|
this.InsertAfter(edit.id, edit.m);
|
||||||
break;
|
break;
|
||||||
case "InsertBefore":
|
case "InsertBefore":
|
||||||
this.InsertBefore(edit.id, edit.n);
|
this.InsertBefore(edit.id, edit.m);
|
||||||
break;
|
break;
|
||||||
case "Remove":
|
case "Remove":
|
||||||
this.Remove(edit.id);
|
this.Remove(edit.id);
|
||||||
break;
|
break;
|
||||||
case "LoadTemplate":
|
|
||||||
this.LoadTemplate(edit.name, edit.index);
|
|
||||||
break;
|
|
||||||
case "SaveTemplate":
|
|
||||||
this.SaveTemplate(edit.name, edit.m);
|
|
||||||
break;
|
|
||||||
case "CreateElementNs":
|
|
||||||
this.CreateElementNs(edit.name, edit.id, edit.ns);
|
|
||||||
break;
|
|
||||||
case "SetText":
|
case "SetText":
|
||||||
this.SetText(edit.id, edit.value);
|
this.SetText(edit.id, edit.value);
|
||||||
break;
|
break;
|
||||||
case "SetAttribute":
|
case "SetAttribute":
|
||||||
this.SetAttribute(edit.id, edit.name, edit.value, edit.ns);
|
this.SetAttribute(edit.id, edit.name, edit.value, edit.ns);
|
||||||
break;
|
break;
|
||||||
|
case "SetBoolAttribute":
|
||||||
|
this.SetAttribute(edit.id, edit.name, edit.value, edit.ns);
|
||||||
|
break;
|
||||||
case "RemoveAttribute":
|
case "RemoveAttribute":
|
||||||
this.RemoveAttribute(edit.id, edit.name, edit.ns);
|
this.RemoveAttribute(edit.id, edit.name, edit.ns);
|
||||||
break;
|
break;
|
||||||
@@ -734,8 +710,6 @@ function is_element_node(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function event_bubbles(event) {
|
function event_bubbles(event) {
|
||||||
console.log("event_bubbles", event);
|
|
||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case "copy":
|
case "copy":
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ use std::any::Any;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use dioxus_core::ElementId;
|
use dioxus_core::ElementId;
|
||||||
use dioxus_core::{EventPriority, UserEvent};
|
|
||||||
use dioxus_html::event_bubbles;
|
use dioxus_html::event_bubbles;
|
||||||
use dioxus_html::on::*;
|
use dioxus_html::events::*;
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
pub(crate) struct IpcMessage {
|
pub(crate) struct IpcMessage {
|
||||||
@@ -30,29 +29,29 @@ struct ImEvent {
|
|||||||
contents: serde_json::Value,
|
contents: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
pub fn trigger_from_serialized(val: serde_json::Value) -> () {
|
||||||
let ImEvent {
|
todo!()
|
||||||
event,
|
// let ImEvent {
|
||||||
mounted_dom_id,
|
// event,
|
||||||
contents,
|
// mounted_dom_id,
|
||||||
} = serde_json::from_value(val).unwrap();
|
// contents,
|
||||||
|
// } = serde_json::from_value(val).unwrap();
|
||||||
|
|
||||||
let mounted_dom_id = Some(mounted_dom_id);
|
// let mounted_dom_id = Some(mounted_dom_id);
|
||||||
|
|
||||||
let name = event_name_from_type(&event);
|
// let name = event_name_from_type(&event);
|
||||||
let event = make_synthetic_event(&event, contents);
|
// let event = make_synthetic_event(&event, contents);
|
||||||
|
|
||||||
UserEvent {
|
// UserEvent {
|
||||||
name,
|
// name,
|
||||||
priority: EventPriority::Low,
|
// scope_id: None,
|
||||||
scope_id: None,
|
// element: mounted_dom_id,
|
||||||
element: mounted_dom_id,
|
// data: event,
|
||||||
data: event,
|
// bubbles: event_bubbles(name),
|
||||||
bubbles: event_bubbles(name),
|
// }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Send + Sync> {
|
fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any> {
|
||||||
match name {
|
match name {
|
||||||
"copy" | "cut" | "paste" => {
|
"copy" | "cut" | "paste" => {
|
||||||
//
|
//
|
||||||
|
|||||||
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user