mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-23 12:43:08 +00:00
Merge branch 'jk/templates-v3' into implement-new-mutations-with-native-core
This commit is contained in:
commit
80674e4566
114 changed files with 4272 additions and 5579 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -1,4 +1,3 @@
|
|||
|
||||
[workspace]
|
||||
members = [
|
||||
"packages/dioxus",
|
||||
|
@ -13,17 +12,20 @@ members = [
|
|||
"packages/mobile",
|
||||
"packages/interpreter",
|
||||
"packages/fermi",
|
||||
"packages/tui",
|
||||
"packages/liveview",
|
||||
"packages/autofmt",
|
||||
"packages/rsx",
|
||||
"docs/guide",
|
||||
# "packages/tui",
|
||||
# "packages/native-core",
|
||||
# "packages/native-core-macro",
|
||||
]
|
||||
exclude = [
|
||||
"packages/tui",
|
||||
"packages/native-core",
|
||||
"packages/native-core-macro",
|
||||
# "packages/edit-stream",
|
||||
"docs/guide",
|
||||
]
|
||||
|
||||
|
||||
# This is a "virtual package"
|
||||
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
|
||||
[package]
|
||||
|
|
|
@ -30,7 +30,7 @@ fn app(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! {
|
||||
div {
|
||||
|
@ -46,7 +46,7 @@ struct Query {
|
|||
}
|
||||
|
||||
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)
|
||||
.query::<Query>()
|
||||
|
|
|
@ -165,13 +165,13 @@ fn app(cx: Scope) -> Element {
|
|||
|
||||
// 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 })
|
||||
}
|
||||
|
||||
// Spreading can also be overridden manually
|
||||
Taller {
|
||||
..TallerProps { a: "ballin!", children: Default::default() },
|
||||
..TallerProps { a: "ballin!", children: cx.render(rsx!(()) )},
|
||||
a: "not ballin!"
|
||||
}
|
||||
|
||||
|
|
32
examples/simple_todo.rs
Normal file
32
examples/simple_todo.rs
Normal file
|
@ -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}"
|
||||
}
|
||||
})
|
||||
}
|
|
@ -57,7 +57,10 @@ pub fn app(cx: Scope<()>) -> Element {
|
|||
placeholder: "What needs to be done?",
|
||||
value: "{draft}",
|
||||
autofocus: "true",
|
||||
oninput: move |evt| draft.set(evt.value.clone()),
|
||||
oninput: move |evt| {
|
||||
println!("calling oninput");
|
||||
draft.set(evt.value.clone());
|
||||
},
|
||||
onkeydown: move |evt| {
|
||||
if evt.key() == Key::Enter && !draft.is_empty() {
|
||||
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 editing = if **is_editing { "editing" } else { "" };
|
||||
|
||||
println!("rendering todo entry");
|
||||
|
||||
cx.render(rsx!{
|
||||
li {
|
||||
class: "{completed} {editing}",
|
||||
|
|
|
@ -67,7 +67,7 @@ impl Buffer {
|
|||
match node {
|
||||
BodyNode::Element(el) => self.write_element(el),
|
||||
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),
|
||||
_ => Ok(()),
|
||||
}
|
||||
|
|
|
@ -280,7 +280,7 @@ impl Buffer {
|
|||
}
|
||||
|
||||
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)] => {
|
||||
let attr_len = self.field_len(&comp.fields, &comp.manual_props);
|
||||
|
||||
|
|
|
@ -42,4 +42,3 @@ dioxus = { path = "../dioxus" }
|
|||
[features]
|
||||
default = []
|
||||
serialize = ["serde"]
|
||||
debug_vdom = []
|
||||
|
|
|
@ -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
|
||||
fn app(cx: Scope) -> Element {
|
||||
render!(div { "hello world" })
|
||||
cx.render(rsx!( div { "hello world" } ))
|
||||
}
|
||||
```
|
||||
|
||||
fn main() {
|
||||
let mut renderer = SomeRenderer::new();
|
||||
Then, we'll want to create a new VirtualDom using this app as the root component.
|
||||
|
||||
// Creating a new virtualdom from a component
|
||||
let mut dom = VirtualDom::new(app);
|
||||
```rust, ingore
|
||||
let mut dom = VirtualDom::new(app);
|
||||
```
|
||||
|
||||
// Patching the renderer with the changes to draw the screen
|
||||
let edits = dom.rebuild();
|
||||
renderer.apply(edits);
|
||||
To build the app into a stream of mutations, we'll use [`VirtualDom::rebuild`]:
|
||||
|
||||
// Injecting events
|
||||
dom.handle_message(SchedulerMsg::Event(UserEvent {
|
||||
scope_id: None,
|
||||
priority: EventPriority::High,
|
||||
element: ElementId(0),
|
||||
name: "onclick",
|
||||
data: Arc::new(()),
|
||||
}));
|
||||
```rust, ignore
|
||||
let mutations = dom.rebuild();
|
||||
|
||||
// polling asynchronously
|
||||
dom.wait_for_work().await;
|
||||
apply_edits_to_real_dom(mutations);
|
||||
```
|
||||
|
||||
// working with a deadline
|
||||
if let Some(edits) = dom.work_with_deadline(|| false) {
|
||||
renderer.apply(edits);
|
||||
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:
|
||||
|
||||
```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
|
||||
let scope = dom.get_scope(ScopeId(0)).unwrap();
|
||||
// Render any work without blocking the main thread for too long
|
||||
let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)));
|
||||
|
||||
// iterating through the tree
|
||||
match scope.root_node() {
|
||||
VNodes::Text(vtext) => dbg!(vtext),
|
||||
VNodes::Element(vel) => dbg!(vel),
|
||||
_ => todo!()
|
||||
}
|
||||
// And then apply the edits
|
||||
real_dom.apply(mutations);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
|
||||
|
||||
## 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 crate::{
|
||||
factory::{ComponentReturn, RenderReturn},
|
||||
innerlude::Scoped,
|
||||
nodes::{ComponentReturn, RenderReturn},
|
||||
scopes::{Scope, ScopeState},
|
||||
Element,
|
||||
};
|
||||
|
||||
/// 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 render(&'a self, bump: &'a ScopeState) -> RenderReturn<'a>;
|
||||
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 memo: unsafe fn(&P, &P) -> bool,
|
||||
pub props: P,
|
||||
// pub props: PropsAllocation<P>,
|
||||
_marker: PhantomData<A>,
|
||||
}
|
||||
|
||||
|
@ -35,7 +38,6 @@ where
|
|||
render_fn,
|
||||
memo,
|
||||
props,
|
||||
// props: PropsAllocation::Borrowed(props),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
@ -60,12 +62,12 @@ where
|
|||
}
|
||||
|
||||
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,
|
||||
scope: cx,
|
||||
});
|
||||
|
||||
// Call the render function directly
|
||||
(self.render_fn)(scope).as_return(cx)
|
||||
(self.render_fn)(scope).into_return(cx)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct ElementId(pub usize);
|
||||
|
||||
pub struct ElementRef {
|
||||
pub(crate) struct ElementRef {
|
||||
// the pathway of the real element inside the template
|
||||
pub path: &'static [u8],
|
||||
pub path: ElementPath,
|
||||
|
||||
// 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 {
|
||||
pub fn null() -> Self {
|
||||
pub(crate) fn null() -> Self {
|
||||
Self {
|
||||
template: std::ptr::null_mut(),
|
||||
path: &[],
|
||||
path: ElementPath::Root(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 id = entry.key();
|
||||
|
||||
entry.insert(ElementRef {
|
||||
template: template as *const _ as *mut _,
|
||||
path,
|
||||
path: ElementPath::Deep(path),
|
||||
});
|
||||
|
||||
ElementId(id)
|
||||
}
|
||||
|
||||
pub fn cleanup_element(&mut self, id: ElementId) {
|
||||
self.elements.remove(id.0);
|
||||
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
|
||||
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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
now......
|
||||
impl ElementPath {
|
||||
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.
|
||||
We need to
|
||||
*/
|
||||
impl PartialEq<&[u8]> for ElementPath {
|
||||
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 std::cell::Cell;
|
||||
|
||||
pub struct BumpFrame {
|
||||
pub(crate) struct BumpFrame {
|
||||
pub bump: Bump,
|
||||
pub node: Cell<*mut RenderReturn<'static>>,
|
||||
pub node: Cell<*const RenderReturn<'static>>,
|
||||
}
|
||||
|
||||
impl BumpFrame {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
pub(crate) fn new(capacity: usize) -> Self {
|
||||
let bump = Bump::with_capacity(capacity);
|
||||
Self {
|
||||
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
|
||||
pub unsafe fn load_node<'b>(&self) -> &'b RenderReturn<'b> {
|
||||
unsafe { std::mem::transmute(&*self.node.get()) }
|
||||
pub(crate) unsafe fn try_load_node<'b>(&self) -> Option<&'b RenderReturn<'b>> {
|
||||
let node = self.node.get();
|
||||
|
||||
if node.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe { std::mem::transmute(&*node) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
use crate::factory::RenderReturn;
|
||||
use crate::innerlude::{VComponent, VFragment, VText};
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::innerlude::{VComponent, VText};
|
||||
use crate::mutations::Mutation;
|
||||
use crate::mutations::Mutation::*;
|
||||
use crate::nodes::VNode;
|
||||
use crate::nodes::{DynamicNode, TemplateNode};
|
||||
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.
|
||||
///
|
||||
/// 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 {
|
||||
// The best renderers will have templates prehydrated and registered
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -44,29 +45,36 @@ impl<'b: 'static> VirtualDom {
|
|||
DynamicNode::Text(VText { id: slot, value }) => {
|
||||
let id = self.next_element(template, template.template.node_paths[*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
|
||||
}
|
||||
|
||||
DynamicNode::Fragment(VFragment::Empty(slot)) => {
|
||||
DynamicNode::Placeholder(slot) => {
|
||||
let id = self.next_element(template, template.template.node_paths[*id]);
|
||||
slot.set(id);
|
||||
self.mutations.push(CreatePlaceholder { id });
|
||||
1
|
||||
}
|
||||
|
||||
DynamicNode::Fragment(VFragment::NonEmpty(_))
|
||||
| DynamicNode::Component { .. } => {
|
||||
DynamicNode::Fragment(_) | DynamicNode::Component { .. } => {
|
||||
self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
self.mutations.push(LoadTemplate {
|
||||
name: template.template.id,
|
||||
name: template.template.name,
|
||||
index: root_idx,
|
||||
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
|
||||
let id = match path.len() {
|
||||
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 {
|
||||
let attribute = template.dynamic_attrs.get(attr_id).unwrap();
|
||||
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 {
|
||||
AttributeValue::Text(value) => self.mutations.push(SetAttribute {
|
||||
name: attribute.name,
|
||||
value: *value,
|
||||
ns: attribute.namespace,
|
||||
id,
|
||||
}),
|
||||
AttributeValue::Text(value) => {
|
||||
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||
let unbounded_value = unsafe { std::mem::transmute(*value) };
|
||||
|
||||
self.mutations.push(SetAttribute {
|
||||
name: unbounded_name,
|
||||
value: unbounded_value,
|
||||
ns: attribute.namespace,
|
||||
id,
|
||||
})
|
||||
}
|
||||
AttributeValue::Bool(value) => {
|
||||
self.mutations.push(SetBoolAttribute {
|
||||
name: attribute.name,
|
||||
name: unbounded_name,
|
||||
value: *value,
|
||||
id,
|
||||
})
|
||||
|
@ -104,7 +128,7 @@ impl<'b: 'static> VirtualDom {
|
|||
AttributeValue::Listener(_) => {
|
||||
self.mutations.push(NewEventListener {
|
||||
// all listeners start with "on"
|
||||
event_name: &attribute.name[2..],
|
||||
name: &unbounded_name[2..],
|
||||
scope: cur_scope,
|
||||
id,
|
||||
})
|
||||
|
@ -179,7 +203,7 @@ impl<'b: 'static> VirtualDom {
|
|||
fn register_template(&mut self, template: &'b VNode<'b>) {
|
||||
// First, make sure we mark the template as seen, regardless if we process it
|
||||
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
|
||||
// Quickly run through and see if it's all just dynamic nodes
|
||||
|
@ -199,85 +223,7 @@ impl<'b: 'static> VirtualDom {
|
|||
return;
|
||||
}
|
||||
|
||||
for node in template.template.roots {
|
||||
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() })
|
||||
}
|
||||
}
|
||||
self.mutations.templates.push(template.template);
|
||||
}
|
||||
|
||||
pub(crate) fn create_dynamic_node(
|
||||
|
@ -289,7 +235,8 @@ impl<'b: 'static> VirtualDom {
|
|||
use DynamicNode::*;
|
||||
match node {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
@ -306,59 +253,60 @@ impl<'b: 'static> VirtualDom {
|
|||
// Make sure the text node is assigned to the correct element
|
||||
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
|
||||
self.mutations.push(HydrateText {
|
||||
id: new_id,
|
||||
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
|
||||
0
|
||||
}
|
||||
|
||||
pub(crate) fn create_fragment(
|
||||
pub(crate) fn create_placeholder(
|
||||
&mut self,
|
||||
frag: &'b VFragment<'b>,
|
||||
slot: &Cell<ElementId>,
|
||||
template: &'b VNode<'b>,
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
match frag {
|
||||
VFragment::NonEmpty(nodes) => {
|
||||
nodes.iter().fold(0, |acc, child| acc + self.create(child))
|
||||
}
|
||||
// Allocate a dynamic element reference for this text node
|
||||
let id = self.next_element(template, template.template.node_paths[idx]);
|
||||
|
||||
VFragment::Empty(slot) => {
|
||||
// Allocate a dynamic element reference for this text node
|
||||
let id = self.next_element(template, template.template.node_paths[idx]);
|
||||
// Make sure the text node is assigned to the correct element
|
||||
slot.set(id);
|
||||
|
||||
// Make sure the text node is assigned to the correct element
|
||||
slot.set(id);
|
||||
// Assign the ID to the existing node in the template
|
||||
self.mutations.push(AssignId {
|
||||
path: &template.template.node_paths[idx][1..],
|
||||
id,
|
||||
});
|
||||
|
||||
// Assign the ID to the existing node in the template
|
||||
self.mutations.push(AssignId {
|
||||
path: &template.template.node_paths[idx][1..],
|
||||
id,
|
||||
});
|
||||
|
||||
// Since the placeholder is already in the DOM, we don't create any new nodes
|
||||
0
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
template: &'b VNode<'b>,
|
||||
component: &'b VComponent<'b>,
|
||||
idx: 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 scope = self.new_scope(prop_ptr).id;
|
||||
let unbounded_props = unsafe { std::mem::transmute(props) };
|
||||
|
||||
component.props.replace(Some(props));
|
||||
let scope = self.new_scope(unbounded_props).id;
|
||||
component.scope.set(Some(scope));
|
||||
|
||||
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
||||
|
@ -366,10 +314,7 @@ impl<'b: 'static> VirtualDom {
|
|||
use RenderReturn::*;
|
||||
|
||||
match return_nodes {
|
||||
Sync(Ok(t)) => {
|
||||
self.mount_component(scope, template, t, idx)
|
||||
// self.create(t)
|
||||
}
|
||||
Sync(Ok(t)) => self.mount_component(scope, template, t, idx),
|
||||
Sync(Err(_e)) => todo!("Propogate error upwards"),
|
||||
Async(_) => self.mount_component_placeholder(template, idx, scope),
|
||||
}
|
||||
|
@ -384,7 +329,7 @@ impl<'b: 'static> VirtualDom {
|
|||
) -> usize {
|
||||
// Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary
|
||||
// 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
|
||||
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
|
||||
let boundary = match self.scopes[scope.0].has_context::<SuspenseContext>() {
|
||||
Some(boundary) => boundary,
|
||||
Some(boundary) => unsafe { &*(boundary as *const SuspenseContext) },
|
||||
_ => 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
|
||||
let split_off = unsafe {
|
||||
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);
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use std::cell::Cell;
|
||||
|
||||
use crate::{
|
||||
arena::ElementId,
|
||||
factory::RenderReturn,
|
||||
innerlude::{DirtyScope, VComponent, VFragment, VText},
|
||||
innerlude::{DirtyScope, VComponent, VText},
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{DynamicNode, VNode},
|
||||
scopes::ScopeId,
|
||||
virtual_dom::VirtualDom,
|
||||
|
@ -10,71 +12,66 @@ use crate::{
|
|||
};
|
||||
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use std::cell::Cell;
|
||||
use DynamicNode::*;
|
||||
|
||||
impl<'b: 'static> VirtualDom {
|
||||
pub fn diff_scope(&mut self, scope: ScopeId) {
|
||||
impl<'b> VirtualDom {
|
||||
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
|
||||
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);
|
||||
unsafe {
|
||||
let cur_arena = cur_arena.load_node();
|
||||
let prev_arena = prev_arena.load_node();
|
||||
self.diff_maybe_node(prev_arena, cur_arena);
|
||||
// Load the old and new bump arenas
|
||||
let old = scope_state
|
||||
.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();
|
||||
}
|
||||
|
||||
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};
|
||||
match (left, right) {
|
||||
// diff
|
||||
match (old, new) {
|
||||
(Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
|
||||
|
||||
_ => todo!("handle diffing nonstandard nodes"),
|
||||
// // remove old with placeholder
|
||||
// (Sync(Ok(l)), Sync(None)) | (Sync(Ok(l)), Async(_)) => {
|
||||
// //
|
||||
// let id = self.next_element(l, &[]); // todo!
|
||||
// m.push(Mutation::CreatePlaceholder { id });
|
||||
// self.drop_template(m, l, true);
|
||||
// }
|
||||
// Err cases
|
||||
(Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e),
|
||||
(Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r),
|
||||
(Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ }
|
||||
|
||||
// // remove placeholder with nodes
|
||||
// (Sync(None), Sync(Ok(_))) => {}
|
||||
// (Async(_), Sync(Ok(v))) => {}
|
||||
|
||||
// // nothing... just transfer the placeholders over
|
||||
// (Async(_), Async(_))
|
||||
// | (Sync(None), Sync(None))
|
||||
// | (Sync(None), Async(_))
|
||||
// | (Async(_), Sync(None)) => {}
|
||||
// Async
|
||||
(Sync(Ok(_l)), Async(_)) => todo!(),
|
||||
(Sync(Err(_e)), Async(_)) => todo!(),
|
||||
(Async(_), Sync(Ok(_r))) => todo!(),
|
||||
(Async(_), Sync(Err(_e))) => { /* nothing */ }
|
||||
(Async(_), Async(_)) => { /* nothing */ }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
|
||||
if left_template.template.id != right_template.template.id {
|
||||
fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {
|
||||
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);
|
||||
}
|
||||
|
||||
println!("diffing attributs...");
|
||||
|
||||
for (left_attr, right_attr) in left_template
|
||||
.dynamic_attrs
|
||||
.iter()
|
||||
|
@ -85,36 +82,59 @@ impl<'b: 'static> VirtualDom {
|
|||
.mounted_element
|
||||
.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 {
|
||||
// todo: add more types of attribute values
|
||||
match right_attr.value {
|
||||
AttributeValue::Text(text) => {
|
||||
let name = unsafe { std::mem::transmute(left_attr.name) };
|
||||
let value = unsafe { std::mem::transmute(text) };
|
||||
self.mutations.push(Mutation::SetAttribute {
|
||||
id: left_attr.mounted_element.get(),
|
||||
name: left_attr.name,
|
||||
value: text,
|
||||
ns: right_attr.namespace,
|
||||
name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
// 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
|
||||
.iter()
|
||||
.zip(right_template.dynamic_nodes.iter())
|
||||
.enumerate()
|
||||
{
|
||||
match (left_node, right_node) {
|
||||
(Text(left), Text(right)) => self.diff_vtext(left, right),
|
||||
(Fragment(left), Fragment(right)) => self.diff_vfragment(left, right),
|
||||
(Component(left), Component(right)) => self.diff_vcomponent(left, right),
|
||||
_ => self.replace(left_template, right_template, left_node, right_node),
|
||||
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
|
||||
(Placeholder(left), Placeholder(right)) => {
|
||||
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
|
||||
for (left, right) in left_template
|
||||
.root_ids
|
||||
|
@ -125,31 +145,73 @@ impl<'b: 'static> VirtualDom {
|
|||
}
|
||||
}
|
||||
|
||||
fn diff_vcomponent(&mut self, left: &'b VComponent<'b>, right: &'b VComponent<'b>) {
|
||||
// Due to how templates work, we should never get two different components. The only way we could enter
|
||||
// this codepath is through "light_diff", but we check there that the pointers are the same
|
||||
assert_eq!(left.render_fn, right.render_fn);
|
||||
fn replace_placeholder_with_nodes(&mut self, l: &'b Cell<ElementId>, r: &'b [VNode<'b>]) {
|
||||
let m = self.create_children(r);
|
||||
let id = l.get();
|
||||
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
|
||||
let scope_id = left.scope.get().unwrap();
|
||||
right.scope.set(Some(scope_id));
|
||||
|
||||
// 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();
|
||||
|
||||
// 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
|
||||
// This also implicitly drops the new props since they're not used
|
||||
if left.static_props && unsafe { old.memoize(new.as_ref()) } {
|
||||
return right.props.set(Some(old));
|
||||
if left.static_props && unsafe { old.as_ref().unwrap().memoize(new.as_ref()) } {
|
||||
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
|
||||
self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new.as_ref()) };
|
||||
right.props.set(Some(new));
|
||||
self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new) };
|
||||
|
||||
// Now run the component and diff it
|
||||
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
|
||||
/// conditions:
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust, ignore
|
||||
/// if enabled {
|
||||
/// rsx!{ Component { enabled_sign: "abc" } }
|
||||
/// } 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,
|
||||
/// then you should be passing in separate props instead.
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let props = if enabled {
|
||||
/// ComponentProps { enabled_sign: "abc" }
|
||||
/// } else {
|
||||
|
@ -196,10 +257,11 @@ impl<'b: 'static> VirtualDom {
|
|||
/// ```
|
||||
fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
|
||||
match matching_components(left, right) {
|
||||
None => self.replace_template(left, right),
|
||||
None => self.replace(left, right),
|
||||
Some(components) => components
|
||||
.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
|
||||
/// different.
|
||||
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 {
|
||||
self.mutations.push(Mutation::SetText {
|
||||
id: left.id.get(),
|
||||
value: right.value,
|
||||
});
|
||||
let value = unsafe { std::mem::transmute(right.value) };
|
||||
self.mutations.push(Mutation::SetText { id, value });
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_vfragment(&mut self, left: &'b VFragment<'b>, right: &'b VFragment<'b>) {
|
||||
use VFragment::*;
|
||||
match (left, right) {
|
||||
(Empty(l), Empty(r)) => r.set(l.get()),
|
||||
(Empty(l), NonEmpty(r)) => self.replace_placeholder_with_nodes(l, r),
|
||||
(NonEmpty(l), Empty(r)) => self.replace_nodes_with_placeholder(l, r),
|
||||
(NonEmpty(old), NonEmpty(new)) => self.diff_non_empty_fragment(new, old),
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_placeholder_with_nodes(&mut self, l: &'b Cell<ElementId>, r: &'b [VNode<'b>]) {
|
||||
let created = self.create_children(r);
|
||||
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"),
|
||||
};
|
||||
}
|
||||
};
|
||||
/// Remove all the top-level nodes, returning the firstmost root ElementId
|
||||
///
|
||||
/// All IDs will be garbage collected
|
||||
fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId {
|
||||
let id = match node.dynamic_root(0) {
|
||||
None => node.root_ids[0].get(),
|
||||
Some(Text(t)) => t.id.get(),
|
||||
Some(Placeholder(e)) => e.get(),
|
||||
Some(Fragment(nodes)) => {
|
||||
let id = self.replace_inner(&nodes[0]);
|
||||
self.remove_nodes(&nodes[1..]);
|
||||
id
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
let root = node.template.roots[idx];
|
||||
match root {
|
||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => {
|
||||
self.mutations.push(Mutation::Remove {
|
||||
id: node.root_ids[idx].get(),
|
||||
})
|
||||
match node.dynamic_root(idx) {
|
||||
Some(Text(i)) => {
|
||||
let id = i.id.get();
|
||||
self.mutations.push(Mutation::Remove { id });
|
||||
self.reclaim(id);
|
||||
}
|
||||
|
||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
||||
match &node.dynamic_nodes[id] {
|
||||
Text(i) => self.mutations.push(Mutation::Remove { id: i.id.get() }),
|
||||
Fragment(VFragment::Empty(e)) => {
|
||||
self.mutations.push(Mutation::Remove { id: e.get() })
|
||||
}
|
||||
Fragment(VFragment::NonEmpty(nodes)) => self.remove_nodes(nodes),
|
||||
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_node(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
};
|
||||
}
|
||||
Some(Placeholder(e)) => {
|
||||
let id = e.get();
|
||||
self.mutations.push(Mutation::Remove { id });
|
||||
self.reclaim(id);
|
||||
}
|
||||
Some(Fragment(nodes)) => self.remove_nodes(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.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 old_is_keyed = old[0].key.is_some();
|
||||
debug_assert!(
|
||||
|
@ -318,6 +395,9 @@ impl<'b: 'static> VirtualDom {
|
|||
old.iter().all(|o| o.key.is_some() == old_is_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 {
|
||||
self.diff_keyed_children(old, new);
|
||||
} else {
|
||||
|
@ -340,7 +420,9 @@ impl<'b: 'static> VirtualDom {
|
|||
debug_assert!(!new.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::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()),
|
||||
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
|
||||
// create the new children afresh.
|
||||
if shared_keys.is_empty() {
|
||||
if let Some(first_old) = old.get(0) {
|
||||
if old.get(0).is_some() {
|
||||
self.remove_nodes(&old[1..]);
|
||||
let nodes_created = self.create_children(new);
|
||||
self.replace_node_with_on_stack(first_old, nodes_created);
|
||||
self.replace_many(&old[0], new);
|
||||
} else {
|
||||
// I think this is wrong - why are we appending?
|
||||
// only valid of the if there are no trailing elements
|
||||
// self.create_and_append_children(new);
|
||||
|
||||
todo!()
|
||||
todo!("we should never be appending - just creating N");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -573,8 +654,7 @@ impl<'b: 'static> VirtualDom {
|
|||
for child in old {
|
||||
let key = child.key.unwrap();
|
||||
if !shared_keys.contains(&key) {
|
||||
todo!("remove node");
|
||||
// self.remove_nodes( [child]);
|
||||
self.remove_node(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
|
||||
/// Wont generate mutations for the inner nodes
|
||||
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>) {
|
||||
for (idx, root) in node.template.roots.iter().enumerate() {
|
||||
let id = match root {
|
||||
TemplateNode::Text(_) | TemplateNode::Element { .. } => node.root_ids[idx].get(),
|
||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
||||
match &node.dynamic_nodes[*id] {
|
||||
Text(t) => t.id.get(),
|
||||
Fragment(VFragment::Empty(t)) => t.get(),
|
||||
Fragment(VFragment::NonEmpty(t)) => return self.remove_nodes(t),
|
||||
Component(comp) => return self.remove_component(comp.scope.get().unwrap()),
|
||||
}
|
||||
}
|
||||
for (idx, _) in node.template.roots.iter().enumerate() {
|
||||
let id = match node.dynamic_root(idx) {
|
||||
Some(Text(t)) => t.id.get(),
|
||||
Some(Placeholder(t)) => t.get(),
|
||||
Some(Fragment(t)) => return self.remove_nodes(t),
|
||||
Some(Component(comp)) => return self.remove_component(comp.scope.get().unwrap()),
|
||||
None => node.root_ids[idx].get(),
|
||||
};
|
||||
|
||||
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) {
|
||||
|
@ -727,8 +801,42 @@ impl<'b: 'static> VirtualDom {
|
|||
}
|
||||
|
||||
/// Push all the real nodes on the stack
|
||||
fn push_all_real_nodes(&mut self, node: &VNode) -> usize {
|
||||
todo!()
|
||||
fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
|
||||
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 {
|
||||
|
@ -747,68 +855,58 @@ impl<'b: 'static> VirtualDom {
|
|||
self.mutations.push(Mutation::InsertAfter { id, m })
|
||||
}
|
||||
|
||||
fn find_first_element(&self, node: &VNode) -> ElementId {
|
||||
match &node.template.roots[0] {
|
||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => node.root_ids[0].get(),
|
||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
||||
match &node.dynamic_nodes[*id] {
|
||||
Text(t) => t.id.get(),
|
||||
Fragment(VFragment::NonEmpty(t)) => self.find_first_element(&t[0]),
|
||||
Fragment(VFragment::Empty(t)) => t.get(),
|
||||
Component(comp) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match self.scopes[scope.0].root_node() {
|
||||
RenderReturn::Sync(Ok(t)) => self.find_first_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
}
|
||||
fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
|
||||
match node.dynamic_root(0) {
|
||||
None => node.root_ids[0].get(),
|
||||
Some(Text(t)) => t.id.get(),
|
||||
Some(Fragment(t)) => self.find_first_element(&t[0]),
|
||||
Some(Placeholder(t)) => t.get(),
|
||||
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.find_first_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_last_element(&self, node: &VNode) -> ElementId {
|
||||
match node.template.roots.last().unwrap() {
|
||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => {
|
||||
node.root_ids.last().unwrap().get()
|
||||
}
|
||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
||||
match &node.dynamic_nodes[*id] {
|
||||
Text(t) => t.id.get(),
|
||||
Fragment(VFragment::NonEmpty(t)) => self.find_last_element(t.last().unwrap()),
|
||||
Fragment(VFragment::Empty(t)) => t.get(),
|
||||
Component(comp) => {
|
||||
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 find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
|
||||
match node.dynamic_root(node.template.roots.len() - 1) {
|
||||
None => node.root_ids.last().unwrap().get(),
|
||||
Some(Text(t)) => t.id.get(),
|
||||
Some(Fragment(t)) => self.find_last_element(t.last().unwrap()),
|
||||
Some(Placeholder(t)) => t.get(),
|
||||
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.find_last_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_template(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
|
||||
todo!("replacing should work!")
|
||||
fn replace(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
|
||||
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(
|
||||
&mut self,
|
||||
left_template: &'b VNode<'b>,
|
||||
right_template: &'b VNode<'b>,
|
||||
left: &'b DynamicNode<'b>,
|
||||
right: &'b DynamicNode<'b>,
|
||||
) {
|
||||
// Remove all but the first root
|
||||
for root in &left_template.template.roots[1..] {
|
||||
match root {
|
||||
TemplateNode::Element { .. } => todo!(),
|
||||
TemplateNode::Text(_) => todo!(),
|
||||
TemplateNode::Dynamic(_) => todo!(),
|
||||
TemplateNode::DynamicText(_) => todo!(),
|
||||
}
|
||||
}
|
||||
fn replace_many(&mut self, left: &'b VNode<'b>, right: &'b [VNode<'b>]) {
|
||||
let first = self.find_first_element(left);
|
||||
let id = self.replace_inner(left);
|
||||
let created = self.create_children(right);
|
||||
self.mutations.push(Mutation::ReplaceWith {
|
||||
id: first,
|
||||
m: created,
|
||||
});
|
||||
self.try_reclaim(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -836,7 +934,7 @@ fn matching_components<'a>(
|
|||
_ => return None,
|
||||
};
|
||||
|
||||
(l.render_fn == r.render_fn).then(|| (l, r))
|
||||
Some((l, r))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
@ -847,6 +945,7 @@ fn matching_components<'a>(
|
|||
/// - for text - we can use SetTextContent
|
||||
/// - for clearning children we can use RemoveChildren
|
||||
/// - for appending children we can use AppendChildren
|
||||
#[allow(dead_code)]
|
||||
fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool {
|
||||
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)>>,
|
||||
id: ScopeId,
|
||||
}
|
||||
|
||||
/// Catch all errors from the children and bubble them up to this component
|
||||
///
|
||||
/// Returns the error and scope that caused the error
|
||||
pub fn use_catch_error(cx: &ScopeState) -> Option<&(anyhow::Error, ScopeId)> {
|
||||
let err_ctx = use_error_context(cx);
|
||||
|
||||
let out = cx.use_hook(|| None);
|
||||
|
||||
if let Some(error) = err_ctx.error.take() {
|
||||
*out = Some(error);
|
||||
impl ErrorBoundary {
|
||||
pub fn new(id: ScopeId) -> Self {
|
||||
Self {
|
||||
error: RefCell::new(None),
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
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() })))
|
||||
}
|
||||
|
|
|
@ -1,109 +1,110 @@
|
|||
use bumpalo::boxed::Box as BumpBox;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
fmt::Debug,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
pub struct UiEvent<T: 'static + ?Sized> {
|
||||
pub(crate) bubbles: Rc<Cell<bool>>,
|
||||
/// A wrapper around some generic data that handles the event's state
|
||||
///
|
||||
///
|
||||
/// 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) propogates: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl UiEvent<dyn Any> {
|
||||
pub fn downcast<T: 'static + Sized>(self) -> Option<UiEvent<T>> {
|
||||
Some(UiEvent {
|
||||
bubbles: self.bubbles,
|
||||
data: self.data.downcast().ok()?,
|
||||
})
|
||||
impl<T> Event<T> {
|
||||
/// 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();
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[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 {
|
||||
Self {
|
||||
bubbles: self.bubbles.clone(),
|
||||
propogates: self.propogates.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>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&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 {
|
||||
f.debug_struct("UiEvent")
|
||||
.field("bubble_state", &self.bubbles)
|
||||
.field("bubble_state", &self.propogates)
|
||||
.field("data", &self.data)
|
||||
.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.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ```rust, ignore
|
||||
///
|
||||
/// rsx!{
|
||||
/// MyComponent { onclick: move |evt| log::info!("clicked"), }
|
||||
/// MyComponent { onclick: move |evt| log::info!("clicked") }
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Props)]
|
||||
|
@ -132,21 +132,23 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>;
|
|||
///
|
||||
/// ```
|
||||
pub struct EventHandler<'bump, T = ()> {
|
||||
/// The (optional) callback that the user specified
|
||||
/// Uses a `RefCell` to allow for interior mutability, and FnMut closures.
|
||||
pub callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
||||
pub(super) callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
||||
}
|
||||
|
||||
impl<'a, T> Default for EventHandler<'a, T> {
|
||||
impl<T> Default for EventHandler<'_, T> {
|
||||
fn default() -> 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> {
|
||||
/// 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) {
|
||||
if let Some(callback) = self.callback.borrow_mut().as_mut() {
|
||||
callback(event);
|
||||
|
@ -154,6 +156,8 @@ impl<T> EventHandler<'_, T> {
|
|||
}
|
||||
|
||||
/// Forcibly drop the internal handler callback, releasing memory
|
||||
///
|
||||
/// This will force any future calls to "call" to not doing anything
|
||||
pub fn release(&self) {
|
||||
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.
|
||||
#[allow(non_upper_case_globals, non_snake_case)]
|
||||
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 {
|
||||
node_id: children.node_id.clone(),
|
||||
key: children.key.clone(),
|
||||
parent: children.parent.clone(),
|
||||
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
|
||||
//! 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,
|
||||
//! 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.
|
||||
///
|
||||
/// 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
|
||||
|
@ -27,13 +27,11 @@ pub struct LazyNodes<'a, 'b> {
|
|||
inner: StackNodeStorage<'a, 'b>,
|
||||
}
|
||||
|
||||
pub type NodeFactory<'a> = &'a ScopeState;
|
||||
|
||||
type StackHeapSize = [usize; 16];
|
||||
|
||||
enum StackNodeStorage<'a, 'b> {
|
||||
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> {
|
||||
|
@ -42,11 +40,11 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||
/// 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
|
||||
/// 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
|
||||
let mut slot = Some(val);
|
||||
|
||||
let val = move |fac: Option<NodeFactory<'a>>| {
|
||||
let val = move |fac: Option<&'a ScopeState>| {
|
||||
fac.map(
|
||||
slot.take()
|
||||
.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.
|
||||
pub fn new_boxed<F>(inner: F) -> Self
|
||||
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
|
||||
let mut slot = Some(inner);
|
||||
|
||||
Self {
|
||||
inner: StackNodeStorage::Heap(Box::new(move |fac: Option<NodeFactory<'a>>| {
|
||||
inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| {
|
||||
fac.map(
|
||||
slot.take()
|
||||
.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
|
||||
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!(
|
||||
ptr as *const u8, &val as *const _ as *const u8,
|
||||
|
@ -162,12 +160,10 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||
/// ```rust, ignore
|
||||
/// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
|
||||
///
|
||||
/// let fac = NodeFactory::new(&cx);
|
||||
///
|
||||
/// let node = f.call(cac);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> {
|
||||
pub fn call(self, f: &'a ScopeState) -> VNode<'a> {
|
||||
match self.inner {
|
||||
StackNodeStorage::Heap(mut lazy) => {
|
||||
lazy(Some(f)).expect("Closure should not be called twice")
|
||||
|
@ -184,18 +180,18 @@ struct 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 data = buf.as_ref();
|
||||
|
||||
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>()
|
||||
- 1;
|
||||
|
||||
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..]) };
|
||||
|
||||
self.dropped = true;
|
||||
|
@ -210,14 +206,14 @@ impl Drop for LazyStack {
|
|||
let LazyStack { buf, .. } = self;
|
||||
let data = buf.as_ref();
|
||||
|
||||
let info_size = mem::size_of::<
|
||||
*mut dyn FnMut(Option<NodeFactory<'_>>) -> Option<VNode<'_>>,
|
||||
>() / mem::size_of::<usize>()
|
||||
- 1;
|
||||
let info_size =
|
||||
mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>>>()
|
||||
/ mem::size_of::<usize>()
|
||||
- 1;
|
||||
|
||||
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..]) };
|
||||
|
||||
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 {
|
||||
(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);
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod any_props;
|
||||
mod arena;
|
||||
mod bump_frame;
|
||||
|
@ -6,9 +9,7 @@ mod diff;
|
|||
mod dirty_scope;
|
||||
mod error_boundary;
|
||||
mod events;
|
||||
mod factory;
|
||||
mod fragment;
|
||||
mod garbage;
|
||||
mod lazynodes;
|
||||
mod mutations;
|
||||
mod nodes;
|
||||
|
@ -21,10 +22,12 @@ mod virtual_dom;
|
|||
pub(crate) mod innerlude {
|
||||
pub use crate::arena::*;
|
||||
pub use crate::dirty_scope::*;
|
||||
pub use crate::error_boundary::*;
|
||||
pub use crate::events::*;
|
||||
pub use crate::fragment::*;
|
||||
pub use crate::lazynodes::*;
|
||||
pub use crate::mutations::*;
|
||||
pub use crate::nodes::RenderReturn;
|
||||
pub use crate::nodes::*;
|
||||
pub use crate::properties::*;
|
||||
pub use crate::scheduler::*;
|
||||
|
@ -64,45 +67,13 @@ pub(crate) mod innerlude {
|
|||
/// )
|
||||
/// ```
|
||||
pub type Component<P = ()> = fn(Scope<P>) -> Element;
|
||||
|
||||
/// A list of attributes
|
||||
pub type Attributes<'a> = Option<&'a [Attribute<'a>]>;
|
||||
}
|
||||
|
||||
pub use crate::innerlude::{
|
||||
// AnyAttributeValue, AnyEvent,
|
||||
fc_to_builder,
|
||||
Attribute,
|
||||
AttributeValue,
|
||||
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,
|
||||
fc_to_builder, Attribute, AttributeValue, Component, DynamicNode, Element, ElementId, Event,
|
||||
Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope,
|
||||
ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, TemplateAttribute,
|
||||
TemplateNode, VComponent, VNode, VText, VirtualDom,
|
||||
};
|
||||
|
||||
/// 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`].
|
||||
pub mod prelude {
|
||||
pub use crate::innerlude::{
|
||||
fc_to_builder, Element, EventHandler, EventPriority, Fragment, LazyNodes, NodeFactory,
|
||||
Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute,
|
||||
TemplateNode, UiEvent, VNode, VirtualDom,
|
||||
fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope,
|
||||
ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode,
|
||||
VirtualDom,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -120,28 +91,4 @@ pub mod exports {
|
|||
//! Important dependencies that are used by the rest of the library
|
||||
//! Feel free to just add the dependencies in your own Crates.toml
|
||||
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();
|
||||
)*}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
/// The ID of the subtree that these edits are targetting
|
||||
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>>,
|
||||
}
|
||||
|
||||
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
|
||||
///
|
||||
/// Used really only for testing
|
||||
pub fn santize(mut self) -> Self {
|
||||
for edit in self
|
||||
.template_mutations
|
||||
.iter_mut()
|
||||
.chain(self.edits.iter_mut())
|
||||
{
|
||||
match edit {
|
||||
Mutation::LoadTemplate { name, .. } => *name = "template",
|
||||
Mutation::SaveTemplate { name, .. } => *name = "template",
|
||||
_ => {}
|
||||
for edit in self.edits.iter_mut() {
|
||||
if let Mutation::LoadTemplate { name, .. } = edit {
|
||||
*name = "template"
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Deref for Mutations<'a> {
|
||||
type Target = Vec<Mutation<'a>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.edits
|
||||
/// Push a new mutation into the dom_edits list
|
||||
pub(crate) fn push(&mut self, mutation: Mutation<'static>) {
|
||||
self.edits.push(mutation)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Mutations<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.edits
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
each subtree has its own numbering scheme
|
||||
*/
|
||||
|
||||
/// A `Mutation` represents a single instruction for the renderer to use to modify the UI tree to match the state
|
||||
/// of the Dioxus VirtualDom.
|
||||
///
|
||||
/// These edits can be serialized and sent over the network or through any interface
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
|
@ -61,52 +62,111 @@ each subtree has its own numbering scheme
|
|||
)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Mutation<'a> {
|
||||
/// Add these m children to the target element
|
||||
AppendChildren {
|
||||
/// The ID of the element being mounted to
|
||||
id: ElementId,
|
||||
|
||||
/// The number of nodes on the stack
|
||||
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 {
|
||||
/// 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 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,
|
||||
},
|
||||
|
||||
CreateElement {
|
||||
name: &'a str,
|
||||
},
|
||||
CreateElementNamespace {
|
||||
name: &'a str,
|
||||
namespace: &'a str,
|
||||
},
|
||||
/// Create an placeholder int he DOM that we will use later.
|
||||
///
|
||||
/// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
|
||||
CreatePlaceholder {
|
||||
id: ElementId,
|
||||
},
|
||||
CreateStaticPlaceholder,
|
||||
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,
|
||||
/// 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,
|
||||
},
|
||||
|
||||
// Take the current element and replace it with the element with the given id.
|
||||
ReplaceWith {
|
||||
/// Create a node specifically for text with the given value
|
||||
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,
|
||||
},
|
||||
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// Replace an existing element in the template at the given path with the m nodes on the stack
|
||||
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],
|
||||
|
||||
/// The number of nodes on the stack to use to replace
|
||||
m: usize,
|
||||
},
|
||||
|
||||
|
@ -128,47 +188,46 @@ pub enum Mutation<'a> {
|
|||
m: usize,
|
||||
},
|
||||
|
||||
SaveTemplate {
|
||||
name: &'static str,
|
||||
m: usize,
|
||||
},
|
||||
|
||||
/// Set the value of a node's attribute.
|
||||
SetAttribute {
|
||||
/// The name of the attribute to set.
|
||||
name: &'a str,
|
||||
/// The value of the attribute.
|
||||
value: &'a str,
|
||||
|
||||
/// The ID of the node to set the attribute of.
|
||||
id: ElementId,
|
||||
|
||||
// value: &'bump str,
|
||||
/// The (optional) namespace of the attribute.
|
||||
/// For instance, "style" is in the "style" namespace.
|
||||
ns: Option<&'a str>,
|
||||
},
|
||||
|
||||
SetStaticAttribute {
|
||||
name: &'a str,
|
||||
value: &'a str,
|
||||
ns: Option<&'a str>,
|
||||
},
|
||||
|
||||
/// Set the value of a node's attribute.
|
||||
SetBoolAttribute {
|
||||
/// The name of the attribute to set.
|
||||
name: &'a str,
|
||||
|
||||
/// The value of the attribute.
|
||||
value: bool,
|
||||
|
||||
/// The ID of the node to set the attribute of.
|
||||
id: ElementId,
|
||||
},
|
||||
|
||||
SetInnerText {
|
||||
value: &'a str,
|
||||
},
|
||||
|
||||
/// Set the textcontent of a node.
|
||||
SetText {
|
||||
/// The textcontent of the node
|
||||
value: &'a str,
|
||||
|
||||
/// The ID of the node to set the textcontent of.
|
||||
id: ElementId,
|
||||
},
|
||||
|
||||
/// Create a new Event Listener.
|
||||
NewEventListener {
|
||||
/// 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.
|
||||
scope: ScopeId,
|
||||
|
@ -179,13 +238,22 @@ pub enum Mutation<'a> {
|
|||
|
||||
/// Remove an existing Event Listener.
|
||||
RemoveEventListener {
|
||||
/// The name of the event to remove.
|
||||
name: &'a str,
|
||||
|
||||
/// The ID of the node to remove.
|
||||
id: ElementId,
|
||||
|
||||
/// The name of the event to remove.
|
||||
event: &'a str,
|
||||
},
|
||||
|
||||
/// Remove a particular node from the DOM
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
fmt::Arguments,
|
||||
future::Future,
|
||||
};
|
||||
|
||||
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
|
||||
///
|
||||
/// The dynamic parts of the template are stored separately from the static parts. This allows faster diffing by skipping
|
||||
/// static parts of the template.
|
||||
#[derive(Debug, Clone)]
|
||||
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.
|
||||
///
|
||||
/// 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> {
|
||||
/// Create a template with no nodes that will be skipped over during diffing
|
||||
pub fn empty() -> Element<'a> {
|
||||
Ok(VNode {
|
||||
node_id: Cell::new(ElementId(0)),
|
||||
key: None,
|
||||
parent: None,
|
||||
root_ids: &[],
|
||||
dynamic_nodes: &[],
|
||||
dynamic_attrs: &[],
|
||||
template: Template {
|
||||
id: "dioxus-empty",
|
||||
name: "dioxus-empty",
|
||||
roots: &[],
|
||||
node_paths: &[],
|
||||
attr_paths: &[],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Template<'a> {
|
||||
pub id: &'a str,
|
||||
pub roots: &'a [TemplateNode<'a>],
|
||||
pub node_paths: &'a [&'a [u8]],
|
||||
pub attr_paths: &'a [&'a [u8]],
|
||||
}
|
||||
|
||||
/// 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(_))
|
||||
/// Load a dynamic root at the given index
|
||||
///
|
||||
/// Returns [`None`] if the root is actually a static node (Element/Text)
|
||||
pub fn dynamic_root(&self, idx: usize) -> Option<&'a DynamicNode<'a>> {
|
||||
match &self.template.roots[idx] {
|
||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => None,
|
||||
TemplateNode::Dynamic(id) | TemplateNode::DynamicText(id) => {
|
||||
Some(&self.dynamic_nodes[*id])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
/// The name of this component
|
||||
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,
|
||||
|
||||
/// The assigned Scope for this component
|
||||
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(crate) props: Cell<Option<Box<dyn AnyProps<'a> + '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)]
|
||||
pub struct VText<'a> {
|
||||
pub id: Cell<ElementId>,
|
||||
/// The actual text itself
|
||||
pub value: &'a str,
|
||||
|
||||
/// The ID of this node in the real DOM
|
||||
pub id: Cell<ElementId>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum VFragment<'a> {
|
||||
Empty(Cell<ElementId>),
|
||||
NonEmpty(&'a [VNode<'a>]),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// An attribute of the TemplateNode, created at compile time
|
||||
#[derive(Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum TemplateAttribute<'a> {
|
||||
/// This attribute is entirely known at compile time, enabling
|
||||
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,
|
||||
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),
|
||||
}
|
||||
|
||||
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
|
||||
#[derive(Debug)]
|
||||
pub struct Attribute<'a> {
|
||||
/// The name of the attribute.
|
||||
pub name: &'a str,
|
||||
|
||||
/// The value of the attribute
|
||||
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>,
|
||||
|
||||
/// The element in the DOM that this attribute belongs to
|
||||
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,
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
/// Text attribute
|
||||
Text(&'a str),
|
||||
Float(f32),
|
||||
Int(i32),
|
||||
|
||||
/// A float
|
||||
Float(f64),
|
||||
|
||||
/// Signed integer
|
||||
Int(i64),
|
||||
|
||||
/// Boolean
|
||||
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,
|
||||
}
|
||||
|
||||
impl<'a> AttributeValue<'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,
|
||||
})
|
||||
}
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event<dyn Any>) + 'a>;
|
||||
|
||||
impl<'a> std::fmt::Debug for AttributeValue<'a> {
|
||||
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::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||
(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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AttributeValue<'a> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait AnyValue {
|
||||
fn any_cmp(&self, other: &dyn AnyValue) -> bool;
|
||||
fn our_typeid(&self) -> TypeId;
|
||||
|
@ -231,9 +366,185 @@ impl<T: PartialEq + Any> AnyValue for T {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_are_the_sizes() {
|
||||
dbg!(std::mem::size_of::<VNode>());
|
||||
dbg!(std::mem::size_of::<Template>());
|
||||
dbg!(std::mem::size_of::<TemplateNode>());
|
||||
#[doc(hidden)]
|
||||
pub trait ComponentReturn<'a, A = ()> {
|
||||
fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a>;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
/// An ID representing an ongoing suspended component
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct SuspenseId(pub usize);
|
||||
pub(crate) struct SuspenseId(pub usize);
|
||||
|
||||
pub type SuspenseContext = Rc<SuspenseBoundary>;
|
||||
|
||||
/// Essentially a fiber in React
|
||||
pub struct SuspenseBoundary {
|
||||
pub id: ScopeId,
|
||||
pub waiting_on: RefCell<HashSet<SuspenseId>>,
|
||||
pub mutations: RefCell<Mutations<'static>>,
|
||||
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()>>,
|
||||
/// A boundary in the VirtualDom that captures all suspended components below it
|
||||
pub struct SuspenseContext {
|
||||
pub(crate) id: ScopeId,
|
||||
pub(crate) waiting_on: RefCell<HashSet<SuspenseId>>,
|
||||
pub(crate) mutations: RefCell<Mutations<'static>>,
|
||||
pub(crate) placeholder: Cell<Option<ElementId>>,
|
||||
pub(crate) created_on_stack: Cell<usize>,
|
||||
}
|
||||
|
||||
impl SuspenseBoundary {
|
||||
impl SuspenseContext {
|
||||
/// Create a new boundary for suspense
|
||||
pub fn new(id: ScopeId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
waiting_on: Default::default(),
|
||||
mutations: RefCell::new(Mutations::new()),
|
||||
mutations: RefCell::new(Mutations::default()),
|
||||
placeholder: Cell::new(None),
|
||||
created_on_stack: Cell::new(0),
|
||||
onresolve: None,
|
||||
onstart: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SuspenseLeaf {
|
||||
pub id: SuspenseId,
|
||||
pub scope_id: ScopeId,
|
||||
pub tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pub notified: Cell<bool>,
|
||||
pub task: *mut dyn Future<Output = Element<'static>>,
|
||||
pub(crate) id: SuspenseId,
|
||||
pub(crate) scope_id: ScopeId,
|
||||
pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pub(crate) notified: Cell<bool>,
|
||||
pub(crate) task: *mut dyn Future<Output = Element<'static>>,
|
||||
}
|
||||
|
||||
impl RcWake for SuspenseLeaf {
|
||||
|
|
|
@ -2,9 +2,12 @@ use super::{waker::RcWake, Scheduler, SchedulerMsg};
|
|||
use crate::ScopeId;
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::task::Context;
|
||||
use std::{pin::Pin, rc::Rc, task::Poll};
|
||||
use std::{pin::Pin, rc::Rc};
|
||||
|
||||
/// 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))]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct TaskId(pub usize);
|
||||
|
@ -12,22 +15,9 @@ pub struct TaskId(pub usize);
|
|||
/// the task itself is the waker
|
||||
pub(crate) struct LocalTask {
|
||||
pub scope: ScopeId,
|
||||
pub(super) task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||
id: TaskId,
|
||||
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 {
|
||||
|
|
|
@ -2,9 +2,9 @@ use futures_util::FutureExt;
|
|||
use std::task::{Context, Poll};
|
||||
|
||||
use crate::{
|
||||
factory::RenderReturn,
|
||||
innerlude::{Mutation, Mutations, SuspenseContext},
|
||||
TaskId, VNode, VirtualDom,
|
||||
nodes::RenderReturn,
|
||||
ScopeId, TaskId, VNode, VirtualDom,
|
||||
};
|
||||
|
||||
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
|
||||
/// 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 task = &tasks[id.0];
|
||||
|
||||
let waker = task.waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
// 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
|
||||
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");
|
||||
|
||||
let leaf = self
|
||||
|
@ -53,9 +64,8 @@ impl VirtualDom {
|
|||
// we should attach them to that component and then render its children
|
||||
// continue rendering the tree until we hit yet another suspended component
|
||||
if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
|
||||
let fiber = &self.scopes[leaf.scope_id.0]
|
||||
.consume_context::<SuspenseContext>()
|
||||
.unwrap();
|
||||
// safety: we're not going to modify the suspense context but we don't want to make a clone of it
|
||||
let fiber = self.acquire_suspense_boundary(leaf.scope_id);
|
||||
|
||||
println!("ready pool");
|
||||
|
||||
|
@ -78,19 +88,22 @@ impl VirtualDom {
|
|||
let template: &VNode = unsafe { std::mem::transmute(template) };
|
||||
let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) };
|
||||
|
||||
todo!();
|
||||
// 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,
|
||||
// });
|
||||
std::mem::swap(&mut self.mutations, mutations);
|
||||
|
||||
// for leaf in self.collected_leaves.drain(..) {
|
||||
// fiber.waiting_on.borrow_mut().insert(leaf);
|
||||
// }
|
||||
let place_holder_id = scope.placeholder.get().unwrap();
|
||||
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() {
|
||||
println!("fiber is finished!");
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use crate::{
|
||||
any_props::AnyProps,
|
||||
bump_frame::BumpFrame,
|
||||
factory::RenderReturn,
|
||||
innerlude::DirtyScope,
|
||||
innerlude::{SuspenseId, SuspenseLeaf},
|
||||
nodes::RenderReturn,
|
||||
scheduler::RcWake,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
virtual_dom::VirtualDom,
|
||||
AttributeValue, DynamicNode, VNode,
|
||||
};
|
||||
use futures_util::FutureExt;
|
||||
use std::{
|
||||
|
@ -17,17 +18,17 @@ use std::{
|
|||
};
|
||||
|
||||
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 entry = self.scopes.vacant_entry();
|
||||
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
||||
let id = ScopeId(entry.key());
|
||||
|
||||
entry.insert(ScopeState {
|
||||
entry.insert(Box::new(ScopeState {
|
||||
parent,
|
||||
id,
|
||||
height,
|
||||
props,
|
||||
props: Some(props),
|
||||
placeholder: Default::default(),
|
||||
node_arena_1: BumpFrame::new(50),
|
||||
node_arena_2: BumpFrame::new(50),
|
||||
|
@ -38,23 +39,69 @@ impl VirtualDom {
|
|||
hook_idx: Default::default(),
|
||||
shared_contexts: Default::default(),
|
||||
tasks: self.scheduler.clone(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn acquire_current_scope_raw(&mut self) -> Option<*mut ScopeState> {
|
||||
self.scope_stack
|
||||
.last()
|
||||
.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 {
|
||||
// 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 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);
|
||||
|
||||
// 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()
|
||||
};
|
||||
|
||||
|
@ -113,7 +160,7 @@ impl VirtualDom {
|
|||
let frame = scope.previous_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);
|
||||
|
||||
// And move the render generation forward by one
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
use crate::{
|
||||
any_props::AnyProps,
|
||||
any_props::VProps,
|
||||
arena::ElementId,
|
||||
bump_frame::BumpFrame,
|
||||
factory::RenderReturn,
|
||||
innerlude::{DynamicNode, EventHandler, VComponent, VText},
|
||||
innerlude::{Scheduler, SchedulerMsg},
|
||||
lazynodes::LazyNodes,
|
||||
Element, TaskId,
|
||||
nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn},
|
||||
Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||
};
|
||||
use bumpalo::Bump;
|
||||
use std::future::Future;
|
||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Arguments,
|
||||
future::Future,
|
||||
rc::Rc,
|
||||
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
|
||||
/// 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`].
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// `ScopeId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`ScopeID`]s will never be reused
|
||||
/// once a component has been unmounted.
|
||||
/// `ScopeId` is a `usize` that acts a key for the internal slab of Scopes. This means that the key is not unqiue across
|
||||
/// 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))]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
||||
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(crate) render_cnt: Cell<usize>,
|
||||
|
||||
|
@ -82,12 +86,12 @@ pub struct ScopeState {
|
|||
pub(crate) tasks: Rc<Scheduler>,
|
||||
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>>,
|
||||
}
|
||||
|
||||
impl ScopeState {
|
||||
pub fn current_frame(&self) -> &BumpFrame {
|
||||
impl<'src> ScopeState {
|
||||
pub(crate) fn current_frame(&self) -> &BumpFrame {
|
||||
match self.render_cnt.get() % 2 {
|
||||
0 => &self.node_arena_1,
|
||||
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 {
|
||||
1 => &self.node_arena_1,
|
||||
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
|
||||
///
|
||||
/// 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.
|
||||
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
|
||||
|
@ -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.
|
||||
///
|
||||
/// 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()
|
||||
.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.
|
||||
///
|
||||
/// 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();
|
||||
|
||||
if ptr.is_null() {
|
||||
|
@ -165,9 +178,9 @@ impl ScopeState {
|
|||
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`.
|
||||
///
|
||||
|
@ -186,9 +199,9 @@ impl ScopeState {
|
|||
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
|
||||
///
|
||||
|
@ -205,7 +218,7 @@ impl ScopeState {
|
|||
|
||||
/// 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> {
|
||||
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
||||
Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
|
||||
|
@ -228,7 +241,7 @@ impl ScopeState {
|
|||
|
||||
/// 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) {
|
||||
self.tasks
|
||||
.sender
|
||||
|
@ -237,20 +250,17 @@ impl ScopeState {
|
|||
}
|
||||
|
||||
/// Return any context of type T if it exists on this scope
|
||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
self.shared_contexts
|
||||
.borrow()
|
||||
.get(&TypeId::of::<T>())
|
||||
.and_then(|shared| shared.downcast_ref::<T>())
|
||||
.cloned()
|
||||
pub fn has_context<T: 'static>(&self) -> Option<&T> {
|
||||
let contextex = self.shared_contexts.borrow();
|
||||
let val = contextex.get(&TypeId::of::<T>())?;
|
||||
let as_concrete = val.downcast_ref::<T>()? as *const T;
|
||||
Some(unsafe { &*as_concrete })
|
||||
}
|
||||
|
||||
/// Try to retrieve a shared state with type `T` from any parent scope.
|
||||
///
|
||||
/// The state will be cloned and returned, if it exists.
|
||||
///
|
||||
/// We recommend wrapping the state in an `Rc` or `Arc` to avoid deep cloning.
|
||||
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
/// To release the borrow, use `cloned` if the context is clone.
|
||||
pub fn consume_context<T: 'static>(&self) -> Option<&T> {
|
||||
if let Some(this_ctx) = self.has_context() {
|
||||
return Some(this_ctx);
|
||||
}
|
||||
|
@ -260,26 +270,24 @@ impl ScopeState {
|
|||
// safety: all parent pointers are valid thanks to the bump arena
|
||||
let parent = unsafe { &*parent_ptr };
|
||||
if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
||||
return Some(
|
||||
shared
|
||||
.downcast_ref::<T>()
|
||||
.expect("Context of type T should exist")
|
||||
.clone(),
|
||||
);
|
||||
let as_concrete = shared.downcast_ref::<T>()? as *const T;
|
||||
return Some(unsafe { &*as_concrete });
|
||||
}
|
||||
search_parent = parent.parent;
|
||||
}
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
/// the context via Rc/Weak.
|
||||
/// If a state is provided that already exists, the new value will not be inserted. Instead, this method will
|
||||
/// 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
|
||||
///
|
||||
|
@ -296,12 +304,20 @@ impl ScopeState {
|
|||
/// render!(div { "hello {state.0}" })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||
self.shared_contexts
|
||||
.borrow_mut()
|
||||
.insert(TypeId::of::<T>(), Box::new(value.clone()))
|
||||
.and_then(|f| f.downcast::<T>().ok());
|
||||
value
|
||||
pub fn provide_context<T: 'static>(&self, value: T) -> &T {
|
||||
let mut contexts = self.shared_contexts.borrow_mut();
|
||||
|
||||
let any = match contexts.get(&TypeId::of::<T>()) {
|
||||
Some(item) => item.downcast_ref::<T>().unwrap() as *const T,
|
||||
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.
|
||||
|
@ -337,7 +353,7 @@ impl ScopeState {
|
|||
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
|
||||
///
|
||||
|
@ -350,10 +366,126 @@ impl ScopeState {
|
|||
/// 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))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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::{
|
||||
any_props::VProps,
|
||||
arena::{ElementId, ElementRef},
|
||||
factory::RenderReturn,
|
||||
innerlude::{DirtyScope, Mutations, Scheduler, SchedulerMsg},
|
||||
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{Template, TemplateId},
|
||||
scheduler::{SuspenseBoundary, SuspenseId},
|
||||
scheduler::SuspenseId,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
AttributeValue, Element, EventPriority, Scope, SuspenseContext, UiEvent,
|
||||
AttributeValue, Element, Event, Scope, SuspenseContext,
|
||||
};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use slab::Slab;
|
||||
|
@ -94,9 +94,9 @@ use std::{
|
|||
/// ## Building an event loop around Dioxus:
|
||||
///
|
||||
/// 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 {
|
||||
/// cx.render(rsx!{
|
||||
/// cx.render(rsx! {
|
||||
/// 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
|
||||
/// [`VirtualDom::render_with_deadline`] method:
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust, ignore
|
||||
/// let dom = VirtualDom::new(app);
|
||||
///
|
||||
/// 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
|
||||
/// no suspended work is left.
|
||||
///
|
||||
/// ```
|
||||
/// ```rust, ignore
|
||||
/// let dom = VirtualDom::new(app);
|
||||
///
|
||||
/// let deadline = tokio::time::sleep(Duration::from_millis(20));
|
||||
|
@ -149,7 +149,7 @@ use std::{
|
|||
/// ```
|
||||
pub struct VirtualDom {
|
||||
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) scheduler: Rc<Scheduler>,
|
||||
|
||||
|
@ -224,10 +224,7 @@ impl VirtualDom {
|
|||
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
|
||||
/// let mutations = dom.rebuild();
|
||||
/// ```
|
||||
pub fn new_with_props<P>(root: fn(Scope<P>) -> Element, root_props: P) -> Self
|
||||
where
|
||||
P: 'static,
|
||||
{
|
||||
pub fn new_with_props<P: 'static>(root: fn(Scope<P>) -> Element, root_props: P) -> Self {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
let mut dom = Self {
|
||||
rx,
|
||||
|
@ -239,20 +236,23 @@ impl VirtualDom {
|
|||
dirty_scopes: BTreeSet::new(),
|
||||
collected_leaves: 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,
|
||||
|_, _| unreachable!(),
|
||||
root_props,
|
||||
))));
|
||||
)));
|
||||
|
||||
// The root component is always a suspense boundary for any async children
|
||||
// This could be unexpected, so we might rethink this behavior later
|
||||
//
|
||||
// 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
|
||||
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
|
||||
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
|
||||
|
@ -285,7 +285,7 @@ impl VirtualDom {
|
|||
/// Manually mark a scope as requiring a re-render
|
||||
///
|
||||
/// 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;
|
||||
self.dirty_scopes.insert(DirtyScope { height, id });
|
||||
}
|
||||
|
@ -324,7 +324,6 @@ impl VirtualDom {
|
|||
data: Rc<dyn Any>,
|
||||
element: ElementId,
|
||||
bubbles: bool,
|
||||
_priority: EventPriority,
|
||||
) {
|
||||
/*
|
||||
------------------------
|
||||
|
@ -351,8 +350,8 @@ impl VirtualDom {
|
|||
let mut listeners = vec![];
|
||||
|
||||
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
|
||||
let uievent = UiEvent {
|
||||
bubbles: Rc::new(Cell::new(bubbles)),
|
||||
let uievent = Event {
|
||||
propogates: Rc::new(Cell::new(bubbles)),
|
||||
data,
|
||||
};
|
||||
|
||||
|
@ -363,15 +362,13 @@ impl VirtualDom {
|
|||
let target_path = el_ref.path;
|
||||
|
||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||
fn is_path_ascendant(small: &[u8], big: &[u8]) -> bool {
|
||||
small.len() >= big.len() && small == &big[..small.len()]
|
||||
}
|
||||
println!("{:?} \n {:?} \n {:?}", attr, name, element);
|
||||
|
||||
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
|
||||
// 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);
|
||||
|
||||
// Break if the event doesn't bubble anyways
|
||||
|
@ -382,18 +379,23 @@ impl VirtualDom {
|
|||
// Break if this is the exact target element.
|
||||
// This means we won't call two listeners with the same name on the same element. This should be
|
||||
// documented, or be rejected from the rsx! macro outright
|
||||
if this_path == target_path {
|
||||
if target_path == this_path {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("calling listeners: {:?}", listeners);
|
||||
|
||||
// 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
|
||||
for listener in listeners.drain(..).rev() {
|
||||
if let AttributeValue::Listener(listener) = listener {
|
||||
listener.borrow_mut()(uievent.clone());
|
||||
if !uievent.bubbles.get() {
|
||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||
cb(uievent.clone());
|
||||
}
|
||||
|
||||
if !uievent.propogates.get() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -401,6 +403,8 @@ impl VirtualDom {
|
|||
|
||||
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
|
||||
}
|
||||
|
||||
println!("all listeners exhausted");
|
||||
}
|
||||
|
||||
/// Wait for the scheduler to have any work.
|
||||
|
@ -426,7 +430,7 @@ impl VirtualDom {
|
|||
match some_msg.take() {
|
||||
// If a bunch of messages are ready in a sequence, try to pop them off synchronously
|
||||
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::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
|
||||
},
|
||||
|
@ -450,12 +454,15 @@ impl VirtualDom {
|
|||
}
|
||||
}
|
||||
|
||||
/// Swap the current mutations with a new
|
||||
fn finalize<'a>(&'a mut self) -> Mutations<'a> {
|
||||
// todo: make this a routine
|
||||
let mut out = Mutations::new();
|
||||
std::mem::swap(&mut self.mutations, &mut out);
|
||||
out
|
||||
/// Process all events in the queue until there are no more left
|
||||
pub fn process_events(&mut self) {
|
||||
while let Ok(Some(msg)) = self.rx.try_next() {
|
||||
match msg {
|
||||
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
||||
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.
|
||||
|
@ -478,12 +485,15 @@ impl VirtualDom {
|
|||
///
|
||||
/// 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() } {
|
||||
// Rebuilding implies we append the created elements to the root
|
||||
RenderReturn::Sync(Ok(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
|
||||
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.
|
||||
///
|
||||
/// If no suspense trees are present
|
||||
pub async fn render_with_deadline<'a>(
|
||||
&'a mut self,
|
||||
deadline: impl Future<Output = ()>,
|
||||
) -> Mutations<'a> {
|
||||
pub async fn render_with_deadline(&mut self, deadline: impl Future<Output = ()>) -> Mutations {
|
||||
println!("render with deadline");
|
||||
pin_mut!(deadline);
|
||||
|
||||
loop {
|
||||
|
@ -529,9 +537,12 @@ impl VirtualDom {
|
|||
let context = scope.has_context::<SuspenseContext>().unwrap();
|
||||
|
||||
self.mutations
|
||||
.extend(context.mutations.borrow_mut().template_mutations.drain(..));
|
||||
.templates
|
||||
.extend(context.mutations.borrow_mut().templates.drain(..));
|
||||
|
||||
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?
|
||||
self.mutations.push(Mutation::ReplaceWith {
|
||||
|
@ -551,7 +562,7 @@ impl VirtualDom {
|
|||
}
|
||||
|
||||
// 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
|
||||
self.run_scope(dirty.id);
|
||||
|
@ -570,7 +581,8 @@ impl VirtualDom {
|
|||
boundary_mut
|
||||
.mutations
|
||||
.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
|
||||
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 {
|
||||
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
|
||||
|
||||
|
||||
NodeFactory
|
||||
- [] rsx, html, NodeFactory generate the same structures
|
||||
|
||||
Diffing
|
||||
- [x] create elements
|
||||
- [x] create text
|
||||
|
@ -19,15 +16,13 @@ Diffing
|
|||
- [x] keyed diffing
|
||||
- [x] keyed diffing out of order
|
||||
- [x] keyed diffing with prefix/suffix
|
||||
- [x] suspended nodes work
|
||||
- [x] suspended nodes work
|
||||
|
||||
Lifecycle
|
||||
- [] Components mount properly
|
||||
- [] Components create new child components
|
||||
- [] Replaced components unmount old components and mount new
|
||||
- [] Post-render effects are called
|
||||
- []
|
||||
|
||||
|
||||
Shared Context
|
||||
- [] Shared context propagates downwards
|
||||
|
@ -37,7 +32,7 @@ Suspense
|
|||
- [] use_suspense generates suspended nodes
|
||||
|
||||
|
||||
Hooks
|
||||
Hooks
|
||||
- [] Drop order is maintained
|
||||
- [] Shared hook state is okay
|
||||
- [] use_hook works
|
75
packages/core/tests/attr_cleanup.rs
Normal file
75
packages/core/tests/attr_cleanup.rs
Normal file
|
@ -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) },
|
||||
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,) },
|
||||
ReplacePlaceholder { path: &[1,], 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
|
||||
|
||||
use std::{error::Error as StdError, marker::PhantomData, string::ParseError};
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
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 {
|
||||
// propgates error upwards, does not give a reason, lets Dioxus figure it out
|
||||
let value = cx.use_hook(|| "123123123.123".parse::<f32>()).reject()?;
|
||||
let raw = match cx.generation() % 2 {
|
||||
0 => "123.123",
|
||||
1 => "123.123.123",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// propgates error upwards, gives a reason
|
||||
let value = cx
|
||||
.use_hook(|| "123123123.123".parse::<f32>())
|
||||
.reject_because("Parsing float failed")?;
|
||||
let value = raw.parse::<f32>()?;
|
||||
|
||||
let value = cx.use_hook(|| "123123123.123".parse::<f32>()).clone_err()?;
|
||||
|
||||
let value = cx
|
||||
.use_hook(|| "123123123.123".parse::<f32>())
|
||||
.as_ref()
|
||||
.map_err(Clone::clone)?;
|
||||
|
||||
let value = cx
|
||||
.use_hook(|| "123123123.123".parse::<f32>())
|
||||
.as_ref()
|
||||
.map_err(|_| anyhow!("Parsing float failed"))?;
|
||||
|
||||
todo!()
|
||||
cx.render(rsx! {
|
||||
div { "hello {value}" }
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_goes() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
|
||||
dom.render_immediate();
|
||||
}
|
||||
|
|
51
packages/core/tests/context_api.rs
Normal file
51
packages/core/tests/context_api.rs
Normal file
|
@ -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();
|
||||
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!(
|
||||
edits.edits,
|
||||
[
|
||||
// add to root
|
||||
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();
|
||||
assert_eq!(
|
||||
edits.template_mutations,
|
||||
[
|
||||
// create template
|
||||
CreateElement { name: "div" },
|
||||
CreateElement { name: "div" },
|
||||
CreateStaticText { value: "Hello, world!" },
|
||||
CreateElement { name: "div" },
|
||||
CreateElement { name: "div" },
|
||||
CreateStaticPlaceholder {},
|
||||
AppendChildren { m: 1 },
|
||||
AppendChildren { m: 1 },
|
||||
AppendChildren { m: 2 },
|
||||
AppendChildren { m: 1 },
|
||||
SaveTemplate { name: "template", m: 1 },
|
||||
// The fragment child template
|
||||
CreateStaticText { value: "hello" },
|
||||
CreateStaticText { value: "world" },
|
||||
SaveTemplate { name: "template", m: 2 },
|
||||
]
|
||||
);
|
||||
let _edits = dom.rebuild().santize();
|
||||
|
||||
// todo: we don't test template mutations anymore since the templates are passed along
|
||||
|
||||
// assert_eq!(
|
||||
// edits.templates,
|
||||
// [
|
||||
// // create template
|
||||
// CreateElement { name: "div" },
|
||||
// CreateElement { name: "div" },
|
||||
// CreateStaticText { value: "Hello, world!" },
|
||||
// CreateElement { name: "div" },
|
||||
// CreateElement { name: "div" },
|
||||
// CreateStaticPlaceholder {},
|
||||
// AppendChildren { m: 1 },
|
||||
// AppendChildren { m: 1 },
|
||||
// AppendChildren { m: 2 },
|
||||
// AppendChildren { m: 1 },
|
||||
// SaveTemplate { name: "template", m: 1 },
|
||||
// // The fragment child template
|
||||
// CreateStaticText { value: "hello" },
|
||||
// CreateStaticText { value: "world" },
|
||||
// SaveTemplate { name: "template", m: 2 },
|
||||
// ]
|
||||
// );
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -97,17 +88,19 @@ fn create_list() {
|
|||
})
|
||||
});
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
assert_eq!(
|
||||
edits.template_mutations,
|
||||
[
|
||||
// create template
|
||||
CreateElement { name: "div" },
|
||||
CreateStaticText { value: "hello" },
|
||||
AppendChildren { m: 1 },
|
||||
SaveTemplate { name: "template", m: 1 }
|
||||
]
|
||||
);
|
||||
let _edits = dom.rebuild().santize();
|
||||
|
||||
// note: we dont test template edits anymore
|
||||
// assert_eq!(
|
||||
// edits.templates,
|
||||
// [
|
||||
// // create template
|
||||
// CreateElement { name: "div" },
|
||||
// CreateStaticText { value: "hello" },
|
||||
// AppendChildren { m: 1 },
|
||||
// SaveTemplate { name: "template", m: 1 }
|
||||
// ]
|
||||
// );
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -122,18 +115,20 @@ fn create_simple() {
|
|||
});
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
assert_eq!(
|
||||
edits.template_mutations,
|
||||
[
|
||||
// create template
|
||||
CreateElement { name: "div" },
|
||||
CreateElement { name: "div" },
|
||||
CreateElement { name: "div" },
|
||||
CreateElement { name: "div" },
|
||||
// add to root
|
||||
SaveTemplate { name: "template", m: 4 }
|
||||
]
|
||||
);
|
||||
|
||||
// note: we dont test template edits anymore
|
||||
// assert_eq!(
|
||||
// edits.templates,
|
||||
// [
|
||||
// // create template
|
||||
// CreateElement { name: "div" },
|
||||
// CreateElement { name: "div" },
|
||||
// CreateElement { name: "div" },
|
||||
// CreateElement { name: "div" },
|
||||
// // add to root
|
||||
// SaveTemplate { name: "template", m: 4 }
|
||||
// ]
|
||||
// );
|
||||
}
|
||||
#[test]
|
||||
fn create_components() {
|
||||
|
@ -158,26 +153,9 @@ fn create_components() {
|
|||
})
|
||||
}
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
assert_eq!(
|
||||
edits.template_mutations,
|
||||
[
|
||||
// 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 }
|
||||
]
|
||||
);
|
||||
let _edits = dom.rebuild().santize();
|
||||
|
||||
// todo: test this
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -195,23 +173,25 @@ fn anchors() {
|
|||
|
||||
// note that the template under "false" doesn't show up since it's not loaded
|
||||
let edits = dom.rebuild().santize();
|
||||
assert_eq!(
|
||||
edits.template_mutations,
|
||||
[
|
||||
// create each template
|
||||
CreateElement { name: "div" },
|
||||
CreateStaticText { value: "hello" },
|
||||
AppendChildren { m: 1 },
|
||||
SaveTemplate { m: 1, name: "template" },
|
||||
]
|
||||
);
|
||||
|
||||
// note: we dont test template edits anymore
|
||||
// assert_eq!(
|
||||
// edits.templates,
|
||||
// [
|
||||
// // create each template
|
||||
// CreateElement { name: "div" },
|
||||
// CreateStaticText { value: "hello" },
|
||||
// AppendChildren { m: 1 },
|
||||
// SaveTemplate { m: 1, name: "template" },
|
||||
// ]
|
||||
// );
|
||||
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
CreatePlaceholder { id: ElementId(2) },
|
||||
AppendChildren { m: 2 }
|
||||
AppendChildren { m: 2, id: ElementId(0) }
|
||||
]
|
||||
)
|
||||
}
|
||||
|
|
32
packages/core/tests/create_element.rs
Normal file
32
packages/core/tests/create_element.rs
Normal file
|
@ -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!(
|
||||
edits.edits,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(2) },
|
||||
AppendChildren { m: 1 }
|
||||
CreatePlaceholder { id: ElementId(1) },
|
||||
AppendChildren { id: ElementId(0), m: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ fn root_fragments_work() {
|
|||
|
||||
assert_eq!(
|
||||
vdom.rebuild().edits.last().unwrap(),
|
||||
&AppendChildren { m: 2 }
|
||||
&AppendChildren { id: ElementId(0), m: 2 }
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ fn fragments_nested() {
|
|||
|
||||
assert_eq!(
|
||||
vdom.rebuild().edits.last().unwrap(),
|
||||
&AppendChildren { m: 8 }
|
||||
&AppendChildren { id: ElementId(0), m: 8 }
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ fn fragments_across_components() {
|
|||
|
||||
assert_eq!(
|
||||
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!(
|
||||
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();
|
||||
|
||||
assert_eq!(
|
||||
edits.template_mutations,
|
||||
[
|
||||
// Create the outer div
|
||||
CreateElement { name: "div" },
|
||||
// todo: since this is the only child, we should just use
|
||||
// append when modify the values (IE no need for a placeholder)
|
||||
CreatePlaceholder { id: ElementId(0) },
|
||||
AppendChildren { m: 1 },
|
||||
SaveTemplate { name: "template", m: 1 },
|
||||
// Create the inner template div
|
||||
CreateElement { name: "div" },
|
||||
CreateElement { name: "h1" },
|
||||
CreateStaticText { value: "hello world! " },
|
||||
AppendChildren { m: 1 },
|
||||
CreateElement { name: "p" },
|
||||
CreateStaticText { value: "d" },
|
||||
AppendChildren { m: 1 },
|
||||
AppendChildren { m: 2 },
|
||||
SaveTemplate { name: "template", m: 1 }
|
||||
],
|
||||
);
|
||||
// note: we dont test template edits anymore
|
||||
// assert_eq!(
|
||||
// edits.templates,
|
||||
// [
|
||||
// // Create the outer div
|
||||
// CreateElement { name: "div" },
|
||||
// // todo: since this is the only child, we should just use
|
||||
// // append when modify the values (IE no need for a placeholder)
|
||||
// CreateStaticPlaceholder,
|
||||
// AppendChildren { m: 1 },
|
||||
// SaveTemplate { name: "template", m: 1 },
|
||||
// // Create the inner template div
|
||||
// CreateElement { name: "div" },
|
||||
// CreateElement { name: "h1" },
|
||||
// CreateStaticText { value: "hello world! " },
|
||||
// AppendChildren { m: 1 },
|
||||
// CreateElement { name: "p" },
|
||||
// CreateTextPlaceholder,
|
||||
// AppendChildren { m: 1 },
|
||||
// AppendChildren { m: 2 },
|
||||
// SaveTemplate { name: "template", m: 1 }
|
||||
// ],
|
||||
// );
|
||||
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
|
@ -65,7 +66,7 @@ fn list_renders() {
|
|||
// Replace the 0th childn on the div with the 3 templates on the stack
|
||||
ReplacePlaceholder { m: 3, path: &[0] },
|
||||
// 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,
|
||||
[
|
||||
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) },
|
||||
// load div that contains 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();
|
||||
|
||||
// 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
|
||||
assert_eq!(
|
||||
|
@ -100,7 +100,7 @@ fn dynamic_node_as_root() {
|
|||
[
|
||||
CreateTextNode { value: "123", id: ElementId(1) },
|
||||
CreateTextNode { value: "456", id: ElementId(2) },
|
||||
AppendChildren { m: 2 }
|
||||
AppendChildren { id: ElementId(0), m: 2 }
|
||||
]
|
||||
)
|
||||
}
|
||||
|
|
51
packages/core/tests/cycle.rs
Normal file
51
packages/core/tests/cycle.rs
Normal file
|
@ -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 },
|
||||
]
|
||||
);
|
||||
}
|
102
packages/core/tests/diff_component.rs
Normal file
102
packages/core/tests/diff_component.rs
Normal file
|
@ -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);
|
||||
vdom.rebuild();
|
||||
_ = vdom.rebuild();
|
||||
|
||||
vdom.mark_dirty_scope(ScopeId(0));
|
||||
vdom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
vdom.render_immediate().edits,
|
||||
[SetText { value: "hello 1", id: ElementId(2) }]
|
||||
);
|
||||
|
||||
vdom.mark_dirty_scope(ScopeId(0));
|
||||
vdom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
vdom.render_immediate().edits,
|
||||
[SetText { value: "hello 2", id: ElementId(2) }]
|
||||
);
|
||||
|
||||
vdom.mark_dirty_scope(ScopeId(0));
|
||||
vdom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
vdom.render_immediate().edits,
|
||||
[SetText { value: "hello 3", id: ElementId(2) }]
|
||||
|
@ -44,17 +44,41 @@ fn element_swap() {
|
|||
}
|
||||
|
||||
let mut vdom = VirtualDom::new(app);
|
||||
vdom.rebuild();
|
||||
_ = vdom.rebuild();
|
||||
|
||||
vdom.mark_dirty_scope(ScopeId(0));
|
||||
dbg!(vdom.render_immediate());
|
||||
vdom.mark_dirty(ScopeId(0));
|
||||
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));
|
||||
dbg!(vdom.render_immediate());
|
||||
vdom.mark_dirty(ScopeId(0));
|
||||
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));
|
||||
dbg!(vdom.render_immediate());
|
||||
vdom.mark_dirty(ScopeId(0));
|
||||
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));
|
||||
dbg!(vdom.render_immediate());
|
||||
vdom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
vdom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
356
packages/core/tests/diff_keyed_list.rs
Normal file
356
packages/core/tests/diff_keyed_list.rs
Normal file
|
@ -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,) },
|
||||
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
|
||||
dom.mark_dirty_scope(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
|
@ -37,35 +37,35 @@ fn list_creates_one_by_one() {
|
|||
);
|
||||
|
||||
// Rendering the next item should insert after the previous
|
||||
dom.mark_dirty_scope(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(6,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(5,) },
|
||||
InsertAfter { id: ElementId(3,), m: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
// ... and again!
|
||||
dom.mark_dirty_scope(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
|
||||
HydrateText { path: &[0], value: "2", id: ElementId(8,) },
|
||||
InsertAfter { id: ElementId(5,), m: 1 },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6,) },
|
||||
HydrateText { path: &[0], value: "2", id: ElementId(7,) },
|
||||
InsertAfter { id: ElementId(2,), m: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
// once more
|
||||
dom.mark_dirty_scope(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
|
||||
HydrateText { path: &[0], value: "3", id: ElementId(10,) },
|
||||
InsertAfter { id: ElementId(7,), m: 1 },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(8,) },
|
||||
HydrateText { path: &[0], value: "3", id: ElementId(9,) },
|
||||
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
|
||||
ReplacePlaceholder { m: 3, path: &[0] },
|
||||
// Mount the div
|
||||
AppendChildren { m: 1 }
|
||||
AppendChildren { id: ElementId(0), m: 1 }
|
||||
]
|
||||
);
|
||||
|
||||
// Remove div(3)
|
||||
// Rendering the first item should replace the placeholder with an element
|
||||
dom.mark_dirty_scope(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[Remove { id: ElementId(6) }]
|
||||
);
|
||||
|
||||
// Remove div(2)
|
||||
dom.mark_dirty_scope(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[Remove { id: ElementId(4) }]
|
||||
|
@ -121,28 +121,28 @@ fn removes_one_by_one() {
|
|||
|
||||
// Remove div(1) and replace with a placeholder
|
||||
// todo: this should just be a remove with no placeholder
|
||||
dom.mark_dirty_scope(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(8) },
|
||||
CreatePlaceholder { id: ElementId(3) },
|
||||
ReplaceWith { id: ElementId(2), m: 1 }
|
||||
]
|
||||
);
|
||||
|
||||
// load the 3 and replace the placeholder
|
||||
// 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!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(9) },
|
||||
HydrateText { path: &[0], value: "0", id: ElementId(10) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(11) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(12) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(13) },
|
||||
HydrateText { path: &[0], value: "2", id: ElementId(14) },
|
||||
ReplaceWith { id: ElementId(8), m: 3 }
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
HydrateText { path: &[0], value: "0", id: ElementId(4) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(6) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
|
||||
HydrateText { path: &[0], value: "2", id: ElementId(8) },
|
||||
ReplaceWith { id: ElementId(3), m: 3 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -150,10 +150,9 @@ fn removes_one_by_one() {
|
|||
#[test]
|
||||
fn list_shrink_multiroot() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let gen = cx.generation();
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
(0..gen).map(|i| rsx! {
|
||||
(0..cx.generation()).map(|i| rsx! {
|
||||
div { "{i}" }
|
||||
div { "{i}" }
|
||||
})
|
||||
|
@ -166,11 +165,11 @@ fn list_shrink_multiroot() {
|
|||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
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!(
|
||||
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!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(8) },
|
||||
LoadTemplate { name: "template", index: 1, id: ElementId(9) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(10) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(7) },
|
||||
LoadTemplate { name: "template", index: 1, id: ElementId(8) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(9) },
|
||||
InsertAfter { id: ElementId(5), m: 2 }
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty_scope(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(11) },
|
||||
HydrateText { path: &[0], value: "2", id: ElementId(12) },
|
||||
LoadTemplate { name: "template", index: 1, id: ElementId(13) },
|
||||
HydrateText { path: &[0], value: "2", id: ElementId(14) },
|
||||
InsertAfter { id: ElementId(9), m: 2 }
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(10) },
|
||||
HydrateText { path: &[0], value: "2", id: ElementId(11) },
|
||||
LoadTemplate { name: "template", index: 1, id: ElementId(12) },
|
||||
HydrateText { path: &[0], value: "2", id: ElementId(13) },
|
||||
InsertAfter { id: ElementId(8), m: 2 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -245,29 +244,136 @@ fn removes_one_by_one_multiroot() {
|
|||
//
|
||||
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!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[Remove { id: ElementId(10) }, Remove { id: ElementId(12) }]
|
||||
);
|
||||
|
||||
dom.mark_dirty_scope(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[Remove { id: ElementId(6) }, Remove { id: ElementId(8) }]
|
||||
);
|
||||
|
||||
dom.mark_dirty_scope(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
Remove { id: ElementId(4) },
|
||||
CreatePlaceholder { id: ElementId(14) },
|
||||
CreatePlaceholder { id: ElementId(5) },
|
||||
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 },
|
||||
]
|
||||
)
|
||||
}
|
||||
|
|
1
packages/core/tests/hotreloading.rs
Normal file
1
packages/core/tests/hotreloading.rs
Normal file
|
@ -0,0 +1 @@
|
|||
//! It should be possible to swap out templates at runtime, enabling hotreloading
|
42
packages/core/tests/kitchen_sink.rs
Normal file
42
packages/core/tests/kitchen_sink.rs
Normal file
|
@ -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 }
|
||||
],
|
||||
);
|
||||
}
|
168
packages/core/tests/lifecycle.rs
Normal file
168
packages/core/tests/lifecycle.rs
Normal file
|
@ -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) },
|
||||
// ]
|
||||
// );
|
||||
// }
|
49
packages/core/tests/miri_full_app.rs
Normal file
49
packages/core/tests/miri_full_app.rs
Normal file
|
@ -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}"
|
||||
}
|
||||
})
|
||||
}
|
127
packages/core/tests/miri_simple.rs
Normal file
127
packages/core/tests/miri_simple.rs
Normal file
|
@ -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();
|
||||
}
|
351
packages/core/tests/miri_stress.rs
Normal file
351
packages/core/tests/miri_stress.rs
Normal file
|
@ -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
|
||||
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
|
||||
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"))
|
||||
}
|
91
packages/core/tests/suspense.rs
Normal file
91
packages/core/tests/suspense.rs
Normal file
|
@ -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! {
|
||||
_ = 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
|
||||
|
|
|
@ -54,9 +54,9 @@ impl DesktopController {
|
|||
{
|
||||
let edits = dom.rebuild();
|
||||
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());
|
||||
proxy.send_event(UserWindowEvent::Update).unwrap();
|
||||
proxy.send_event(UserWindowEvent::EditsReady).unwrap();
|
||||
}
|
||||
|
||||
loop {
|
||||
|
@ -67,7 +67,7 @@ impl DesktopController {
|
|||
let name = value.event.clone();
|
||||
let el_id = ElementId(value.mounted_dom_id);
|
||||
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();
|
||||
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());
|
||||
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();
|
||||
|
||||
println!("processing pending edits {:?}", new_queue.len());
|
||||
|
||||
for edit in new_queue.drain(..) {
|
||||
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
||||
.unwrap();
|
||||
|
|
|
@ -161,7 +161,8 @@ impl DesktopContext {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub enum UserWindowEvent {
|
||||
Update,
|
||||
EditsReady,
|
||||
Initialize,
|
||||
|
||||
CloseWindow,
|
||||
DragWindow,
|
||||
|
@ -213,7 +214,7 @@ pub(super) fn handler(
|
|||
println!("user_event: {:?}", user_event);
|
||||
|
||||
match user_event {
|
||||
Update => desktop.try_load_ready_webviews(),
|
||||
Initialize | EditsReady => desktop.try_load_ready_webviews(),
|
||||
CloseWindow => *control_flow = ControlFlow::Exit,
|
||||
DragWindow => {
|
||||
// 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" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
println!("initializing...");
|
||||
let _ = proxy.send_event(UserWindowEvent::Update);
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
"browser_open" => {
|
||||
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)]
|
||||
//! Useful foundational hooks for Dioxus
|
||||
#[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();
|
||||
)*}
|
||||
}
|
||||
|
||||
mod usecontext;
|
||||
pub use usecontext::*;
|
||||
|
||||
mod usestate;
|
||||
pub use usestate::{use_state, UseState};
|
||||
|
@ -7,8 +30,8 @@ pub use usestate::{use_state, UseState};
|
|||
mod useref;
|
||||
pub use useref::*;
|
||||
|
||||
mod use_shared_state;
|
||||
pub use use_shared_state::*;
|
||||
// mod use_shared_state;
|
||||
// pub use use_shared_state::*;
|
||||
|
||||
mod usecoroutine;
|
||||
pub use usecoroutine::*;
|
||||
|
|
|
@ -63,7 +63,7 @@ impl<T> ProvidedStateInner<T> {
|
|||
pub fn use_context<T: 'static>(cx: &ScopeState) -> Option<UseSharedState<T>> {
|
||||
let state = cx.use_hook(|| {
|
||||
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() {
|
||||
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(),
|
||||
consumers: HashSet::new(),
|
||||
}));
|
||||
cx.provide_context(state)
|
||||
|
||||
cx.provide_context(state);
|
||||
});
|
||||
}
|
||||
|
|
17
packages/hooks/src/usecontext.rs
Normal file
17
packages/hooks/src/usecontext.rs
Normal file
|
@ -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};
|
||||
use std::future::Future;
|
||||
|
||||
use crate::{use_context, use_context_provider};
|
||||
|
||||
/// 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.
|
||||
|
@ -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
|
||||
M: 'static,
|
||||
G: FnOnce(UnboundedReceiver<M>) -> F,
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
cx.use_hook(|| {
|
||||
use_context_provider(cx, || {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
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
|
||||
///
|
||||
/// See the docs for [`use_coroutine`] for more details.
|
||||
pub fn use_coroutine_handle<M: 'static>(cx: &ScopeState) -> Option<&CoroutineHandle<M>> {
|
||||
cx.use_hook(|| cx.consume_context::<CoroutineHandle<M>>())
|
||||
.as_ref()
|
||||
pub fn use_coroutine_handle<M: 'static>(cx: &ScopeState) -> Option<&Coroutine<M>> {
|
||||
use_context::<Coroutine<M>>(cx)
|
||||
}
|
||||
|
||||
pub struct CoroutineHandle<T> {
|
||||
pub struct Coroutine<T> {
|
||||
tx: UnboundedSender<T>,
|
||||
task: TaskId,
|
||||
}
|
||||
|
||||
impl<T> Clone for CoroutineHandle<T> {
|
||||
// for use in futures
|
||||
impl<T> Clone for Coroutine<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
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
|
||||
#[must_use]
|
||||
pub fn task_id(&self) -> TaskId {
|
||||
|
@ -112,8 +114,8 @@ mod tests {
|
|||
#![allow(unused)]
|
||||
|
||||
use super::*;
|
||||
use dioxus_core::exports::futures_channel::mpsc::unbounded;
|
||||
use dioxus_core::prelude::*;
|
||||
use futures_channel::mpsc::unbounded;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
fn app(cx: Scope, name: String) -> Element {
|
||||
|
@ -127,7 +129,7 @@ mod tests {
|
|||
|
||||
let task3 = use_coroutine(&cx, |rx| complex_task(rx, 10));
|
||||
|
||||
None
|
||||
todo!()
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
None
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ pub fn use_state<T: 'static>(
|
|||
let update_callback = cx.schedule_update();
|
||||
let slot = Rc::new(RefCell::new(current_val.clone()));
|
||||
let setter = Rc::new({
|
||||
dioxus_core::to_owned![update_callback, slot];
|
||||
to_owned![update_callback, slot];
|
||||
move |new| {
|
||||
{
|
||||
let mut slot = slot.borrow_mut();
|
||||
|
@ -466,7 +466,7 @@ fn api_makes_sense() {
|
|||
}
|
||||
|
||||
cx.spawn({
|
||||
dioxus_core::to_owned![val];
|
||||
to_owned![val];
|
||||
async move {
|
||||
val.modify(|f| f + 1);
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@ macro_rules! impl_event {
|
|||
) => {
|
||||
$(
|
||||
$( #[$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 {
|
||||
name: stringify!($name),
|
||||
value: ::dioxus_core::AttributeValue::new_listener(_cx, _f),
|
||||
value: _cx.listener(_f),
|
||||
namespace: None,
|
||||
mounted_element: Default::default(),
|
||||
volatile: false,
|
||||
|
@ -60,90 +60,90 @@ pub use touch::*;
|
|||
pub use transition::*;
|
||||
pub use wheel::*;
|
||||
|
||||
// pub fn event_bubbles(evt: &str) -> bool {
|
||||
// match evt {
|
||||
// "copy" => true,
|
||||
// "cut" => true,
|
||||
// "paste" => true,
|
||||
// "compositionend" => true,
|
||||
// "compositionstart" => true,
|
||||
// "compositionupdate" => true,
|
||||
// "keydown" => true,
|
||||
// "keypress" => true,
|
||||
// "keyup" => true,
|
||||
// "focus" => false,
|
||||
// "focusout" => true,
|
||||
// "focusin" => true,
|
||||
// "blur" => false,
|
||||
// "change" => true,
|
||||
// "input" => true,
|
||||
// "invalid" => true,
|
||||
// "reset" => true,
|
||||
// "submit" => true,
|
||||
// "click" => true,
|
||||
// "contextmenu" => true,
|
||||
// "doubleclick" => true,
|
||||
// "dblclick" => true,
|
||||
// "drag" => true,
|
||||
// "dragend" => true,
|
||||
// "dragenter" => false,
|
||||
// "dragexit" => false,
|
||||
// "dragleave" => true,
|
||||
// "dragover" => true,
|
||||
// "dragstart" => true,
|
||||
// "drop" => true,
|
||||
// "mousedown" => true,
|
||||
// "mouseenter" => false,
|
||||
// "mouseleave" => false,
|
||||
// "mousemove" => true,
|
||||
// "mouseout" => true,
|
||||
// "scroll" => false,
|
||||
// "mouseover" => true,
|
||||
// "mouseup" => true,
|
||||
// "pointerdown" => true,
|
||||
// "pointermove" => true,
|
||||
// "pointerup" => true,
|
||||
// "pointercancel" => true,
|
||||
// "gotpointercapture" => true,
|
||||
// "lostpointercapture" => true,
|
||||
// "pointerenter" => false,
|
||||
// "pointerleave" => false,
|
||||
// "pointerover" => true,
|
||||
// "pointerout" => true,
|
||||
// "select" => true,
|
||||
// "touchcancel" => true,
|
||||
// "touchend" => true,
|
||||
// "touchmove" => true,
|
||||
// "touchstart" => true,
|
||||
// "wheel" => true,
|
||||
// "abort" => false,
|
||||
// "canplay" => false,
|
||||
// "canplaythrough" => false,
|
||||
// "durationchange" => false,
|
||||
// "emptied" => false,
|
||||
// "encrypted" => true,
|
||||
// "ended" => false,
|
||||
// "error" => false,
|
||||
// "loadeddata" => false,
|
||||
// "loadedmetadata" => false,
|
||||
// "loadstart" => false,
|
||||
// "pause" => false,
|
||||
// "play" => false,
|
||||
// "playing" => false,
|
||||
// "progress" => false,
|
||||
// "ratechange" => false,
|
||||
// "seeked" => false,
|
||||
// "seeking" => false,
|
||||
// "stalled" => false,
|
||||
// "suspend" => false,
|
||||
// "timeupdate" => false,
|
||||
// "volumechange" => false,
|
||||
// "waiting" => false,
|
||||
// "animationstart" => true,
|
||||
// "animationend" => true,
|
||||
// "animationiteration" => true,
|
||||
// "transitionend" => true,
|
||||
// "toggle" => true,
|
||||
// _ => panic!("unsupported event type {:?}", evt),
|
||||
// }
|
||||
// }
|
||||
pub fn event_bubbles(evt: &str) -> bool {
|
||||
match evt {
|
||||
"copy" => true,
|
||||
"cut" => true,
|
||||
"paste" => true,
|
||||
"compositionend" => true,
|
||||
"compositionstart" => true,
|
||||
"compositionupdate" => true,
|
||||
"keydown" => true,
|
||||
"keypress" => true,
|
||||
"keyup" => true,
|
||||
"focus" => false,
|
||||
"focusout" => true,
|
||||
"focusin" => true,
|
||||
"blur" => false,
|
||||
"change" => true,
|
||||
"input" => true,
|
||||
"invalid" => true,
|
||||
"reset" => true,
|
||||
"submit" => true,
|
||||
"click" => true,
|
||||
"contextmenu" => true,
|
||||
"doubleclick" => true,
|
||||
"dblclick" => true,
|
||||
"drag" => true,
|
||||
"dragend" => true,
|
||||
"dragenter" => false,
|
||||
"dragexit" => false,
|
||||
"dragleave" => true,
|
||||
"dragover" => true,
|
||||
"dragstart" => true,
|
||||
"drop" => true,
|
||||
"mousedown" => true,
|
||||
"mouseenter" => false,
|
||||
"mouseleave" => false,
|
||||
"mousemove" => true,
|
||||
"mouseout" => true,
|
||||
"scroll" => false,
|
||||
"mouseover" => true,
|
||||
"mouseup" => true,
|
||||
"pointerdown" => true,
|
||||
"pointermove" => true,
|
||||
"pointerup" => true,
|
||||
"pointercancel" => true,
|
||||
"gotpointercapture" => true,
|
||||
"lostpointercapture" => true,
|
||||
"pointerenter" => false,
|
||||
"pointerleave" => false,
|
||||
"pointerover" => true,
|
||||
"pointerout" => true,
|
||||
"select" => true,
|
||||
"touchcancel" => true,
|
||||
"touchend" => true,
|
||||
"touchmove" => true,
|
||||
"touchstart" => true,
|
||||
"wheel" => true,
|
||||
"abort" => false,
|
||||
"canplay" => false,
|
||||
"canplaythrough" => false,
|
||||
"durationchange" => false,
|
||||
"emptied" => false,
|
||||
"encrypted" => true,
|
||||
"ended" => false,
|
||||
"error" => false,
|
||||
"loadeddata" => false,
|
||||
"loadedmetadata" => false,
|
||||
"loadstart" => false,
|
||||
"pause" => false,
|
||||
"play" => false,
|
||||
"playing" => false,
|
||||
"progress" => false,
|
||||
"ratechange" => false,
|
||||
"seeked" => false,
|
||||
"seeking" => false,
|
||||
"stalled" => false,
|
||||
"suspend" => false,
|
||||
"timeupdate" => false,
|
||||
"volumechange" => false,
|
||||
"waiting" => false,
|
||||
"animationstart" => true,
|
||||
"animationend" => true,
|
||||
"animationiteration" => true,
|
||||
"transitionend" => true,
|
||||
"toggle" => true,
|
||||
_ => 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))]
|
||||
#[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))]
|
||||
#[derive(Debug, Clone)]
|
||||
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))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompositionData {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use std::any::Any;
|
||||
|
||||
use dioxus_core::UiEvent;
|
||||
use dioxus_core::Event;
|
||||
|
||||
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
|
||||
/// 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))]
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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 */
|
||||
#[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))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImageData {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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 std::convert::TryInto;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub type KeyboardEvent = UiEvent<KeyboardData>;
|
||||
pub type KeyboardEvent = Event<KeyboardData>;
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone)]
|
||||
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))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MediaData {}
|
||||
|
|
|
@ -2,15 +2,15 @@ use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenP
|
|||
use crate::input_data::{
|
||||
decode_mouse_button_set, encode_mouse_button_set, MouseButton, MouseButtonSet,
|
||||
};
|
||||
use dioxus_core::UiEvent;
|
||||
use dioxus_core::Event;
|
||||
use keyboard_types::Modifiers;
|
||||
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)
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
/// Data associated with a mouse event
|
||||
///
|
||||
/// 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))]
|
||||
#[derive(Debug, Clone)]
|
||||
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))]
|
||||
#[derive(Debug, Clone)]
|
||||
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))]
|
||||
#[derive(Debug, Clone)]
|
||||
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))]
|
||||
#[derive(Debug, Clone)]
|
||||
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))]
|
||||
#[derive(Debug, Clone)]
|
||||
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))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TransitionData {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use dioxus_core::UiEvent;
|
||||
use dioxus_core::Event;
|
||||
use euclid::UnknownUnit;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
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))]
|
||||
#[derive(Clone)]
|
||||
pub struct WheelData {
|
||||
|
|
|
@ -18,12 +18,14 @@ pub mod events;
|
|||
pub mod geometry;
|
||||
mod global_attributes;
|
||||
pub mod input_data;
|
||||
// #[cfg(feature = "wasm-bind")]
|
||||
// mod web_sys_bind;
|
||||
mod render_template;
|
||||
#[cfg(feature = "wasm-bind")]
|
||||
mod web_sys_bind;
|
||||
|
||||
pub use elements::*;
|
||||
pub use events::*;
|
||||
pub use global_attributes::*;
|
||||
pub use render_template::*;
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::events::*;
|
||||
|
|
41
packages/html/src/render_template.rs
Normal file
41
packages/html/src/render_template.rs
Normal file
|
@ -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::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
|
||||
use crate::on::{
|
||||
use crate::events::{
|
||||
AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData,
|
||||
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 std::convert::TryInto;
|
||||
use std::str::FromStr;
|
||||
|
|
|
@ -12,86 +12,65 @@ extern "C" {
|
|||
pub fn new(arg: Element) -> Interpreter;
|
||||
|
||||
#[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)]
|
||||
pub fn AppendChildren(this: &Interpreter, root: Option<u64>, children: Vec<u64>);
|
||||
pub fn MountToRoot(this: &Interpreter);
|
||||
|
||||
#[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)]
|
||||
pub fn InsertAfter(this: &Interpreter, root: Option<u64>, nodes: Vec<u64>);
|
||||
pub fn CreatePlaceholder(this: &Interpreter, id: u32);
|
||||
|
||||
#[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)]
|
||||
pub fn Remove(this: &Interpreter, root: Option<u64>);
|
||||
pub fn HydrateText(this: &Interpreter, path: &[u8], value: &str, id: u32);
|
||||
|
||||
#[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)]
|
||||
pub fn CreateElement(this: &Interpreter, tag: &str, root: Option<u64>, children: u32);
|
||||
pub fn ReplaceWith(this: &Interpreter, id: u32, m: u32);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn CreateElementNs(
|
||||
this: &Interpreter,
|
||||
tag: &str,
|
||||
root: Option<u64>,
|
||||
ns: &str,
|
||||
children: u32,
|
||||
);
|
||||
pub fn ReplacePlaceholder(this: &Interpreter, path: &[u8], m: u32);
|
||||
|
||||
#[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)]
|
||||
pub fn NewEventListener(
|
||||
this: &Interpreter,
|
||||
name: &str,
|
||||
root: Option<u64>,
|
||||
id: u32,
|
||||
handler: &Function,
|
||||
bubbles: bool,
|
||||
);
|
||||
|
||||
#[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)]
|
||||
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)]
|
||||
pub fn SetAttribute(
|
||||
this: &Interpreter,
|
||||
root: Option<u64>,
|
||||
field: &str,
|
||||
value: JsValue,
|
||||
ns: Option<&str>,
|
||||
);
|
||||
pub fn Remove(this: &Interpreter, id: u32);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn RemoveAttribute(this: &Interpreter, root: Option<u64>, field: &str, ns: Option<&str>);
|
||||
|
||||
#[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);
|
||||
pub fn PushRoot(this: &Interpreter, id: u32);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
export function main() {
|
||||
let root = window.document.getElementById("main");
|
||||
console.log("loading!");
|
||||
if (root != null) {
|
||||
window.interpreter = new Interpreter(root);
|
||||
console.log("properly loaded!");
|
||||
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +16,6 @@ class ListenerMap {
|
|||
}
|
||||
|
||||
create(event_name, element, handler, bubbles) {
|
||||
console.log("creating listener for", event_name, element, handler, bubbles);
|
||||
if (bubbles) {
|
||||
if (this.global[event_name] === undefined) {
|
||||
this.global[event_name] = {};
|
||||
|
@ -79,6 +76,12 @@ export class Interpreter {
|
|||
pop() {
|
||||
return this.stack.pop();
|
||||
}
|
||||
SaveTemplate(nodes, name) {
|
||||
this.templates[name] = nodes;
|
||||
}
|
||||
MountToRoot() {
|
||||
this.AppendChildren(this.stack.length - 1);
|
||||
}
|
||||
SetNode(id, node) {
|
||||
this.nodes[id] = node;
|
||||
}
|
||||
|
@ -124,25 +127,13 @@ export class Interpreter {
|
|||
}
|
||||
}
|
||||
CreateRawText(text) {
|
||||
const node = document.createTextNode(text);
|
||||
this.stack.push(node);
|
||||
this.stack.push(document.createTextNode(text));
|
||||
}
|
||||
CreateTextNode(text, root) {
|
||||
const node = document.createTextNode(text);
|
||||
this.nodes[root] = 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) {
|
||||
let el = document.createElement("pre");
|
||||
el.hidden = true;
|
||||
|
@ -162,9 +153,12 @@ export class Interpreter {
|
|||
SetText(root, 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 node = this.nodes[root];
|
||||
if (ns === "style") {
|
||||
// ????? why do we need to do this
|
||||
if (node.style === undefined) {
|
||||
|
@ -219,8 +213,6 @@ export class Interpreter {
|
|||
}
|
||||
}
|
||||
handleEdits(edits) {
|
||||
console.log("handling edits", edits, this.stack.length);
|
||||
|
||||
for (let edit of edits) {
|
||||
this.handleEdit(edit);
|
||||
}
|
||||
|
@ -248,14 +240,11 @@ export class Interpreter {
|
|||
let node = this.LoadChild(path);
|
||||
node.replaceWith(...els);
|
||||
}
|
||||
LoadTemplate(name, index) {
|
||||
console.log("loading template", name, index);
|
||||
LoadTemplate(name, index, id) {
|
||||
let node = this.templates[name][index].cloneNode(true);
|
||||
this.nodes[id] = node;
|
||||
this.stack.push(node);
|
||||
}
|
||||
SaveTemplate(name, m) {
|
||||
this.templates[name] = this.stack.splice(this.stack.length - m);
|
||||
}
|
||||
handleEdit(edit) {
|
||||
switch (edit.type) {
|
||||
case "AppendChildren":
|
||||
|
@ -264,25 +253,18 @@ export class Interpreter {
|
|||
case "AssignId":
|
||||
this.AssignId(edit.path, edit.id);
|
||||
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":
|
||||
this.CreatePlaceholder(edit.id);
|
||||
break;
|
||||
case "CreateTextNode":
|
||||
this.CreateTextNode(edit.value);
|
||||
break;
|
||||
case "CreateStaticText":
|
||||
this.CreateTextNode(edit.value);
|
||||
break;
|
||||
case "HydrateText":
|
||||
this.HydrateText(edit.path, edit.value, edit.id);
|
||||
break;
|
||||
case "LoadTemplate":
|
||||
this.LoadTemplate(edit.name, edit.index, edit.id);
|
||||
break;
|
||||
case "PushRoot":
|
||||
this.PushRoot(edit.id);
|
||||
break;
|
||||
|
@ -293,29 +275,23 @@ export class Interpreter {
|
|||
this.ReplacePlaceholder(edit.path, edit.m);
|
||||
break;
|
||||
case "InsertAfter":
|
||||
this.InsertAfter(edit.id, edit.n);
|
||||
this.InsertAfter(edit.id, edit.m);
|
||||
break;
|
||||
case "InsertBefore":
|
||||
this.InsertBefore(edit.id, edit.n);
|
||||
this.InsertBefore(edit.id, edit.m);
|
||||
break;
|
||||
case "Remove":
|
||||
this.Remove(edit.id);
|
||||
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":
|
||||
this.SetText(edit.id, edit.value);
|
||||
break;
|
||||
case "SetAttribute":
|
||||
this.SetAttribute(edit.id, edit.name, edit.value, edit.ns);
|
||||
break;
|
||||
case "SetBoolAttribute":
|
||||
this.SetAttribute(edit.id, edit.name, edit.value, edit.ns);
|
||||
break;
|
||||
case "RemoveAttribute":
|
||||
this.RemoveAttribute(edit.id, edit.name, edit.ns);
|
||||
break;
|
||||
|
@ -734,8 +710,6 @@ function is_element_node(node) {
|
|||
}
|
||||
|
||||
function event_bubbles(event) {
|
||||
console.log("event_bubbles", event);
|
||||
|
||||
switch (event) {
|
||||
case "copy":
|
||||
return true;
|
||||
|
|
|
@ -6,9 +6,8 @@ use std::any::Any;
|
|||
use std::sync::Arc;
|
||||
|
||||
use dioxus_core::ElementId;
|
||||
use dioxus_core::{EventPriority, UserEvent};
|
||||
use dioxus_html::event_bubbles;
|
||||
use dioxus_html::on::*;
|
||||
use dioxus_html::events::*;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct IpcMessage {
|
||||
|
@ -30,29 +29,29 @@ struct ImEvent {
|
|||
contents: serde_json::Value,
|
||||
}
|
||||
|
||||
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
||||
let ImEvent {
|
||||
event,
|
||||
mounted_dom_id,
|
||||
contents,
|
||||
} = serde_json::from_value(val).unwrap();
|
||||
pub fn trigger_from_serialized(val: serde_json::Value) -> () {
|
||||
todo!()
|
||||
// let ImEvent {
|
||||
// event,
|
||||
// mounted_dom_id,
|
||||
// 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 event = make_synthetic_event(&event, contents);
|
||||
// let name = event_name_from_type(&event);
|
||||
// let event = make_synthetic_event(&event, contents);
|
||||
|
||||
UserEvent {
|
||||
name,
|
||||
priority: EventPriority::Low,
|
||||
scope_id: None,
|
||||
element: mounted_dom_id,
|
||||
data: event,
|
||||
bubbles: event_bubbles(name),
|
||||
}
|
||||
// UserEvent {
|
||||
// name,
|
||||
// scope_id: None,
|
||||
// element: mounted_dom_id,
|
||||
// data: event,
|
||||
// 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 {
|
||||
"copy" | "cut" | "paste" => {
|
||||
//
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue