Merge branch 'master' into jk/autofmt-fixues
This commit is contained in:
commit
09b38574db
336 files changed
+19973
-16881
No files matched your search
@@ -3,7 +3,7 @@ use std::{
|
||||
fmt::{Result, Write},
|
||||
};
|
||||
|
||||
use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed};
|
||||
use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, IfmtInput};
|
||||
use proc_macro2::{LineColumn, Span};
|
||||
use syn::{spanned::Spanned, Expr};
|
||||
|
||||
@@ -67,13 +67,14 @@ impl Buffer {
|
||||
match node {
|
||||
BodyNode::Element(el) => self.write_element(el),
|
||||
BodyNode::Component(component) => self.write_component(component),
|
||||
BodyNode::Text(text) => self.write_text(&text.text),
|
||||
BodyNode::RawExpr(exp) => self.write_raw_expr(&exp.expr),
|
||||
BodyNode::Text(text) => self.write_text(text),
|
||||
BodyNode::RawExpr(exp) => self.write_raw_expr(exp),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_text(&mut self, text: &syn::LitStr) -> Result {
|
||||
write!(self.buf, "\"{}\"", text.value())
|
||||
pub fn write_text(&mut self, text: &IfmtInput) -> Result {
|
||||
write!(self.buf, "\"{}\"", text.source.as_ref().unwrap().value())
|
||||
}
|
||||
|
||||
pub fn consume(self) -> Option<String> {
|
||||
@@ -165,13 +166,13 @@ impl Buffer {
|
||||
|
||||
total += match &attr.attr {
|
||||
ElementAttr::AttrText { value, name } => {
|
||||
value.value().len() + name.span().line_length() + 3
|
||||
value.source.as_ref().unwrap().value().len() + name.span().line_length() + 3
|
||||
}
|
||||
ElementAttr::AttrExpression { name, value } => {
|
||||
value.span().line_length() + name.span().line_length() + 3
|
||||
}
|
||||
ElementAttr::CustomAttrText { value, name } => {
|
||||
value.value().len() + name.value().len() + 3
|
||||
value.source.as_ref().unwrap().value().len() + name.value().len() + 3
|
||||
}
|
||||
ElementAttr::CustomAttrExpression { name, value } => {
|
||||
name.value().len() + value.span().line_length() + 3
|
||||
|
||||
@@ -167,7 +167,12 @@ impl Buffer {
|
||||
write!(self.buf, "{}: {}", name, out)?;
|
||||
}
|
||||
ContentField::Formatted(s) => {
|
||||
write!(self.buf, "{}: \"{}\"", name, s.value())?;
|
||||
write!(
|
||||
self.buf,
|
||||
"{}: \"{}\"",
|
||||
name,
|
||||
s.source.as_ref().unwrap().value()
|
||||
)?;
|
||||
}
|
||||
ContentField::OnHandlerRaw(exp) => {
|
||||
let out = prettyplease::unparse_expr(exp);
|
||||
@@ -209,7 +214,7 @@ impl Buffer {
|
||||
let attr_len = fields
|
||||
.iter()
|
||||
.map(|field| match &field.content {
|
||||
ContentField::Formatted(s) => s.value().len() ,
|
||||
ContentField::Formatted(s) => s.source.as_ref().unwrap().value().len() ,
|
||||
ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => {
|
||||
let formatted = prettyplease::unparse_expr(exp);
|
||||
let len = if formatted.contains('\n') {
|
||||
|
||||
@@ -136,7 +136,7 @@ impl Buffer {
|
||||
fn write_attributes(
|
||||
&mut self,
|
||||
attributes: &[ElementAttrNamed],
|
||||
key: &Option<syn::LitStr>,
|
||||
key: &Option<IfmtInput>,
|
||||
sameline: bool,
|
||||
) -> Result {
|
||||
let mut attr_iter = attributes.iter().peekable();
|
||||
@@ -145,7 +145,11 @@ impl Buffer {
|
||||
if !sameline {
|
||||
self.indented_tabbed_line()?;
|
||||
}
|
||||
write!(self.buf, "key: \"{}\"", key.value())?;
|
||||
write!(
|
||||
self.buf,
|
||||
"key: \"{}\"",
|
||||
key.source.as_ref().unwrap().value()
|
||||
)?;
|
||||
if !attributes.is_empty() {
|
||||
write!(self.buf, ",")?;
|
||||
if sameline {
|
||||
@@ -182,7 +186,11 @@ impl Buffer {
|
||||
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
|
||||
match &attr.attr {
|
||||
ElementAttr::AttrText { name, value } => {
|
||||
write!(self.buf, "{name}: \"{value}\"", value = value.value())?;
|
||||
write!(
|
||||
self.buf,
|
||||
"{name}: \"{value}\"",
|
||||
value = value.source.as_ref().unwrap().value()
|
||||
)?;
|
||||
}
|
||||
ElementAttr::AttrExpression { name, value } => {
|
||||
let out = prettyplease::unparse_expr(value);
|
||||
@@ -194,7 +202,7 @@ impl Buffer {
|
||||
self.buf,
|
||||
"\"{name}\": \"{value}\"",
|
||||
name = name.value(),
|
||||
value = value.value()
|
||||
value = value.source.as_ref().unwrap().value()
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -276,7 +284,7 @@ impl Buffer {
|
||||
}
|
||||
|
||||
match children {
|
||||
[BodyNode::Text(ref text)] => Some(text.text.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);
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ proc-macro = true
|
||||
proc-macro2 = { version = "1.0" }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||
dioxus-rsx = { path = "../rsx" }
|
||||
dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true }
|
||||
dioxus-rsx = { path = "../rsx", version = "0.0.1" }
|
||||
|
||||
# testing
|
||||
[dev-dependencies]
|
||||
@@ -28,4 +27,3 @@ trybuild = "1.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
hot-reload = ["dioxus-rsx-interpreter"]
|
||||
@@ -9,6 +9,7 @@ use syn::{
|
||||
pub struct InlinePropsBody {
|
||||
pub attrs: Vec<Attribute>,
|
||||
pub vis: syn::Visibility,
|
||||
pub maybe_async: Option<Token![async]>,
|
||||
pub fn_token: Token![fn],
|
||||
pub ident: Ident,
|
||||
pub cx_token: Box<Pat>,
|
||||
@@ -25,6 +26,7 @@ pub struct InlinePropsBody {
|
||||
impl Parse for InlinePropsBody {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
|
||||
let maybe_async: Option<Token![async]> = input.parse().ok();
|
||||
let vis: Visibility = input.parse()?;
|
||||
|
||||
let fn_token = input.parse()?;
|
||||
@@ -57,6 +59,7 @@ impl Parse for InlinePropsBody {
|
||||
|
||||
Ok(Self {
|
||||
vis,
|
||||
maybe_async,
|
||||
fn_token,
|
||||
ident,
|
||||
generics,
|
||||
@@ -84,6 +87,7 @@ impl ToTokens for InlinePropsBody {
|
||||
block,
|
||||
cx_token,
|
||||
attrs,
|
||||
maybe_async,
|
||||
..
|
||||
} = self;
|
||||
|
||||
@@ -151,7 +155,7 @@ impl ToTokens for InlinePropsBody {
|
||||
}
|
||||
|
||||
#(#attrs)*
|
||||
#vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output
|
||||
#maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output
|
||||
#where_clause
|
||||
{
|
||||
let #struct_name { #(#field_names),* } = &cx.props;
|
||||
|
||||
@@ -11,8 +11,7 @@ use dioxus_rsx as rsx;
|
||||
#[proc_macro]
|
||||
pub fn format_args_f(input: TokenStream) -> TokenStream {
|
||||
use rsx::*;
|
||||
let item = parse_macro_input!(input as IfmtInput);
|
||||
format_args_f_impl(item)
|
||||
format_args_f_impl(parse_macro_input!(input as IfmtInput))
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
@@ -27,68 +26,25 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
||||
}
|
||||
|
||||
/// The rsx! macro makes it easy for developers to write jsx-style markup in their components.
|
||||
///
|
||||
/// ## Complete Reference Guide:
|
||||
/// ```ignore
|
||||
#[doc = include_str!("../../../examples/rsx_usage.rs")]
|
||||
/// ```
|
||||
#[proc_macro]
|
||||
pub fn rsx(s: TokenStream) -> TokenStream {
|
||||
#[cfg(feature = "hot-reload")]
|
||||
let rsx_text = s.to_string();
|
||||
match syn::parse::<rsx::CallBody>(s) {
|
||||
Err(err) => err.to_compile_error().into(),
|
||||
Ok(body) => {
|
||||
#[cfg(feature = "hot-reload")]
|
||||
{
|
||||
use dioxus_rsx_interpreter::captuered_context::CapturedContextBuilder;
|
||||
|
||||
match CapturedContextBuilder::from_call_body(body) {
|
||||
Ok(captured) => {
|
||||
let lazy = quote::quote! {
|
||||
LazyNodes::new(move |__cx|{
|
||||
let code_location = get_line_num!();
|
||||
let captured = #captured;
|
||||
let text = #rsx_text;
|
||||
|
||||
resolve_scope(code_location, text, captured, __cx)
|
||||
})
|
||||
};
|
||||
if let Some(cx) = captured.custom_context {
|
||||
quote::quote! {
|
||||
#cx.render(#lazy)
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
lazy.into()
|
||||
}
|
||||
}
|
||||
Err(err) => err.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "hot-reload"))]
|
||||
body.to_token_stream().into()
|
||||
}
|
||||
Ok(body) => body.to_token_stream().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The render! macro makes it easy for developers to write jsx-style markup in their components.
|
||||
///
|
||||
/// The render macro automatically renders rsx - making it unhygenic.
|
||||
///
|
||||
/// ## Complete Reference Guide:
|
||||
/// ```ignore
|
||||
#[doc = include_str!("../../../examples/rsx_usage.rs")]
|
||||
/// ```
|
||||
#[proc_macro]
|
||||
pub fn render(s: TokenStream) -> TokenStream {
|
||||
match syn::parse::<rsx::CallBody>(s) {
|
||||
Err(err) => err.to_compile_error().into(),
|
||||
Ok(body) => quote::quote! {
|
||||
cx.render(#body)
|
||||
Ok(mut body) => {
|
||||
body.inline_cx = true;
|
||||
body.into_token_stream().into()
|
||||
}
|
||||
.into_token_stream()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ mod field_info {
|
||||
// children field is automatically defaulted to None
|
||||
if name == "children" {
|
||||
builder_attr.default =
|
||||
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
||||
Some(syn::parse(quote!(::dioxus::core::VNode::empty()).into()).unwrap());
|
||||
}
|
||||
|
||||
// auto detect optional
|
||||
@@ -781,7 +781,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
let arg_type = if field.builder_attr.strip_option {
|
||||
field.type_from_inside_option(false).ok_or_else(|| {
|
||||
Error::new_spanned(
|
||||
&field_type,
|
||||
field_type,
|
||||
"can't `strip_option` - field is not `Option<...>`",
|
||||
)
|
||||
})?
|
||||
|
||||
@@ -18,34 +18,31 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
|
||||
|
||||
# faster hashmaps
|
||||
fxhash = "0.2"
|
||||
rustc-hash = "1.1.0"
|
||||
|
||||
# Used in diffing
|
||||
longest-increasing-subsequence = "0.1.0"
|
||||
|
||||
# internall used
|
||||
log = { version = "0.4" }
|
||||
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
|
||||
smallvec = "1.6"
|
||||
|
||||
slab = "0.4"
|
||||
|
||||
futures-channel = "0.3.21"
|
||||
|
||||
# used for noderefs
|
||||
once_cell = "1.8"
|
||||
|
||||
indexmap = "1.7"
|
||||
|
||||
# Serialize the Edits for use in Webview/Liveview instances
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
anyhow = "1.0.66"
|
||||
|
||||
# todo: I want to get rid of this
|
||||
backtrace = "0.3"
|
||||
smallbox = "0.8.1"
|
||||
log = "0.4.17"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "*", features = ["full"] }
|
||||
dioxus = { path = "../dioxus" }
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
serialize = ["serde"]
|
||||
debug_vdom = []
|
||||
+64
-46
@@ -1,52 +1,84 @@
|
||||
# Dioxus-core
|
||||
# dioxus-core
|
||||
|
||||
This is the core crate for the Dioxus Virtual DOM. This README will focus on the technical design and layout of this Virtual DOM implementation. If you want to read more about using Dioxus, then check out the Dioxus crate, documentation, and website.
|
||||
dioxus-core is a fast and featureful VirtualDom implementation written in and for Rust.
|
||||
|
||||
To build new apps with Dioxus or to extend the ecosystem with new hooks or components, use the higher-level `dioxus` crate with the appropriate feature flags.
|
||||
# Features
|
||||
|
||||
- Functions as components
|
||||
- Hooks for local state
|
||||
- Task pool for spawning futures
|
||||
- Template-based architecture
|
||||
- Asynchronous components
|
||||
- Suspense boundaries
|
||||
- Error boundaries through the `anyhow` crate
|
||||
- Customizable memoization
|
||||
|
||||
If just starting out, check out the Guides first.
|
||||
|
||||
# General Theory
|
||||
|
||||
The dioxus-core `VirtualDom` object is built around the concept of a `Template`. Templates describe a layout tree known at compile time with dynamic parts filled at runtime.
|
||||
|
||||
Each component in the VirtualDom works as a dedicated render loop where re-renders are triggered by events external to the VirtualDom, or from the components themselves.
|
||||
|
||||
When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result<VNode>`. Between two renders, Dioxus compares the inner `VNode` object, and calculates the differences of the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and new layout, Dioxus will write modifications to the `Mutations` object.
|
||||
|
||||
Dioxus expects the target renderer to save its nodes in a list. Each element is given a numerical ID which can be used to directly index into that list for O(1) lookups.
|
||||
|
||||
# Usage
|
||||
|
||||
All Dioxus apps start as just a function that takes the [`Scope`] object and returns an [`Element`].
|
||||
|
||||
The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler syntax of Rust into the logic required to build Templates.
|
||||
|
||||
First, start with your app:
|
||||
|
||||
```rust, ignore
|
||||
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, ignore
|
||||
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, ignore
|
||||
loop {
|
||||
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.
|
||||
|
||||
@@ -114,6 +114,11 @@ Some essential reading:
|
||||
- https://web.dev/rail/
|
||||
- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react
|
||||
|
||||
# Templates
|
||||
|
||||
If everything is a template, then we'll have the idea that the only children can b Templates
|
||||
|
||||
|
||||
# What's going on?
|
||||
|
||||
Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::{
|
||||
innerlude::Scoped,
|
||||
nodes::{ComponentReturn, RenderReturn},
|
||||
scopes::{Scope, ScopeState},
|
||||
Element,
|
||||
};
|
||||
|
||||
/// A trait that essentially allows VComponentProps to be used generically
|
||||
///
|
||||
/// # 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;
|
||||
}
|
||||
|
||||
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,
|
||||
_marker: PhantomData<A>,
|
||||
}
|
||||
|
||||
impl<'a, P, A, F> VProps<'a, P, A, F>
|
||||
where
|
||||
F: ComponentReturn<'a, A>,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
render_fn: fn(Scope<'a, P>) -> F,
|
||||
memo: unsafe fn(&P, &P) -> bool,
|
||||
props: P,
|
||||
) -> Self {
|
||||
Self {
|
||||
render_fn,
|
||||
memo,
|
||||
props,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'a, P, A, F> AnyProps<'a> for VProps<'a, P, A, F>
|
||||
where
|
||||
F: ComponentReturn<'a, A>,
|
||||
{
|
||||
fn props_ptr(&self) -> *const () {
|
||||
&self.props as *const _ as *const ()
|
||||
}
|
||||
|
||||
// Safety:
|
||||
// this will downcast the other ptr as our swallowed type!
|
||||
// you *must* make this check *before* calling this method
|
||||
// if your functions are not the same, then you will downcast a pointer into a different type (UB)
|
||||
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool {
|
||||
let real_other: &P = &*(other.props_ptr() as *const _ as *const P);
|
||||
let real_us: &P = &*(self.props_ptr() as *const _ as *const P);
|
||||
(self.memo)(real_us, real_other)
|
||||
}
|
||||
|
||||
fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> {
|
||||
let scope: &mut Scoped<P> = cx.bump().alloc(Scoped {
|
||||
props: &self.props,
|
||||
scope: cx,
|
||||
});
|
||||
|
||||
// Call the render function directly
|
||||
(self.render_fn)(scope).into_return(cx)
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
use std::fmt::Formatter;
|
||||
|
||||
/// Possible values for an attribute
|
||||
// trying to keep values at 3 bytes
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(untagged))]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum AttributeValue<'a> {
|
||||
Text(&'a str),
|
||||
Float32(f32),
|
||||
Float64(f64),
|
||||
Int32(i32),
|
||||
Int64(i64),
|
||||
Uint32(u32),
|
||||
Uint64(u64),
|
||||
Bool(bool),
|
||||
|
||||
Vec3Float(f32, f32, f32),
|
||||
Vec3Int(i32, i32, i32),
|
||||
Vec3Uint(u32, u32, u32),
|
||||
|
||||
Vec4Float(f32, f32, f32, f32),
|
||||
Vec4Int(i32, i32, i32, i32),
|
||||
Vec4Uint(u32, u32, u32, u32),
|
||||
|
||||
Bytes(&'a [u8]),
|
||||
Any(ArbitraryAttributeValue<'a>),
|
||||
}
|
||||
|
||||
// todo
|
||||
#[allow(missing_docs)]
|
||||
impl<'a> AttributeValue<'a> {
|
||||
pub fn is_truthy(&self) -> bool {
|
||||
match self {
|
||||
AttributeValue::Text(t) => *t == "true",
|
||||
AttributeValue::Bool(t) => *t,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_falsy(&self) -> bool {
|
||||
match self {
|
||||
AttributeValue::Text(t) => *t == "false",
|
||||
AttributeValue::Bool(t) => !(*t),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Display for AttributeValue<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AttributeValue::Text(a) => write!(f, "{}", a),
|
||||
AttributeValue::Float32(a) => write!(f, "{}", a),
|
||||
AttributeValue::Float64(a) => write!(f, "{}", a),
|
||||
AttributeValue::Int32(a) => write!(f, "{}", a),
|
||||
AttributeValue::Int64(a) => write!(f, "{}", a),
|
||||
AttributeValue::Uint32(a) => write!(f, "{}", a),
|
||||
AttributeValue::Uint64(a) => write!(f, "{}", a),
|
||||
AttributeValue::Bool(a) => write!(f, "{}", a),
|
||||
AttributeValue::Vec3Float(_, _, _) => todo!(),
|
||||
AttributeValue::Vec3Int(_, _, _) => todo!(),
|
||||
AttributeValue::Vec3Uint(_, _, _) => todo!(),
|
||||
AttributeValue::Vec4Float(_, _, _, _) => todo!(),
|
||||
AttributeValue::Vec4Int(_, _, _, _) => todo!(),
|
||||
AttributeValue::Vec4Uint(_, _, _, _) => todo!(),
|
||||
AttributeValue::Bytes(a) => write!(f, "{:?}", a),
|
||||
AttributeValue::Any(_) => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ArbitraryAttributeValue<'a> {
|
||||
pub value: &'a dyn std::any::Any,
|
||||
pub cmp: fn(&'a dyn std::any::Any, &'a dyn std::any::Any) -> bool,
|
||||
}
|
||||
|
||||
impl PartialEq for ArbitraryAttributeValue<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
(self.cmp)(self.value, other.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ArbitraryAttributeValue<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ArbitraryAttributeValue").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
impl<'a> serde::Serialize for ArbitraryAttributeValue<'a> {
|
||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
panic!("ArbitraryAttributeValue should not be serialized")
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "serialize")]
|
||||
impl<'de, 'a> serde::Deserialize<'de> for &'a ArbitraryAttributeValue<'a> {
|
||||
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
panic!("ArbitraryAttributeValue is not deserializable!")
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "serialize")]
|
||||
impl<'de, 'a> serde::Deserialize<'de> for ArbitraryAttributeValue<'a> {
|
||||
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
panic!("ArbitraryAttributeValue is not deserializable!")
|
||||
}
|
||||
}
|
||||
|
||||
// todo
|
||||
#[allow(missing_docs)]
|
||||
impl<'a> AttributeValue<'a> {
|
||||
pub fn as_text(&self) -> Option<&'a str> {
|
||||
match self {
|
||||
AttributeValue::Text(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_float32(&self) -> Option<f32> {
|
||||
match self {
|
||||
AttributeValue::Float32(f) => Some(*f),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_float64(&self) -> Option<f64> {
|
||||
match self {
|
||||
AttributeValue::Float64(f) => Some(*f),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_int32(&self) -> Option<i32> {
|
||||
match self {
|
||||
AttributeValue::Int32(i) => Some(*i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_int64(&self) -> Option<i64> {
|
||||
match self {
|
||||
AttributeValue::Int64(i) => Some(*i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_uint32(&self) -> Option<u32> {
|
||||
match self {
|
||||
AttributeValue::Uint32(i) => Some(*i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_uint64(&self) -> Option<u64> {
|
||||
match self {
|
||||
AttributeValue::Uint64(i) => Some(*i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
match self {
|
||||
AttributeValue::Bool(b) => Some(*b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vec3_float(&self) -> Option<(f32, f32, f32)> {
|
||||
match self {
|
||||
AttributeValue::Vec3Float(x, y, z) => Some((*x, *y, *z)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vec3_int(&self) -> Option<(i32, i32, i32)> {
|
||||
match self {
|
||||
AttributeValue::Vec3Int(x, y, z) => Some((*x, *y, *z)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vec3_uint(&self) -> Option<(u32, u32, u32)> {
|
||||
match self {
|
||||
AttributeValue::Vec3Uint(x, y, z) => Some((*x, *y, *z)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vec4_float(&self) -> Option<(f32, f32, f32, f32)> {
|
||||
match self {
|
||||
AttributeValue::Vec4Float(x, y, z, w) => Some((*x, *y, *z, *w)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vec4_int(&self) -> Option<(i32, i32, i32, i32)> {
|
||||
match self {
|
||||
AttributeValue::Vec4Int(x, y, z, w) => Some((*x, *y, *z, *w)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vec4_uint(&self) -> Option<(u32, u32, u32, u32)> {
|
||||
match self {
|
||||
AttributeValue::Vec4Uint(x, y, z, w) => Some((*x, *y, *z, *w)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> Option<&[u8]> {
|
||||
match self {
|
||||
AttributeValue::Bytes(b) => Some(b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_any(&self) -> Option<&'a ArbitraryAttributeValue> {
|
||||
match self {
|
||||
AttributeValue::Any(a) => Some(a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
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(crate) struct ElementRef {
|
||||
// the pathway of the real element inside the template
|
||||
pub path: ElementPath,
|
||||
|
||||
// The actual template
|
||||
pub template: *const VNode<'static>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ElementPath {
|
||||
Deep(&'static [u8]),
|
||||
Root(usize),
|
||||
}
|
||||
|
||||
impl ElementRef {
|
||||
pub(crate) fn null() -> Self {
|
||||
Self {
|
||||
template: std::ptr::null_mut(),
|
||||
path: ElementPath::Root(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualDom {
|
||||
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
|
||||
self.next_reference(template, ElementPath::Deep(path))
|
||||
}
|
||||
|
||||
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
|
||||
self.next_reference(template, ElementPath::Root(path))
|
||||
}
|
||||
|
||||
fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId {
|
||||
let entry = self.elements.vacant_entry();
|
||||
let id = entry.key();
|
||||
|
||||
entry.insert(ElementRef {
|
||||
template: template as *const _ as *mut _,
|
||||
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!(
|
||||
"Cannot reclaim the root element - {:#?}",
|
||||
std::backtrace::Backtrace::force_capture()
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
self.ensure_drop_safety(id);
|
||||
|
||||
if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
|
||||
if let RenderReturn::Sync(Ok(node)) = unsafe { root.extend_lifetime_ref() } {
|
||||
self.drop_scope_inner(node)
|
||||
}
|
||||
}
|
||||
if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } {
|
||||
if let RenderReturn::Sync(Ok(node)) = unsafe { root.extend_lifetime_ref() } {
|
||||
self.drop_scope_inner(node)
|
||||
}
|
||||
}
|
||||
|
||||
self.scopes[id.0].props.take();
|
||||
|
||||
let scope = &mut self.scopes[id.0];
|
||||
|
||||
// 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(..) {
|
||||
drop(unsafe { BumpBox::from_raw(hook) });
|
||||
}
|
||||
}
|
||||
|
||||
fn drop_scope_inner(&mut self, node: &VNode) {
|
||||
node.clear_listeners();
|
||||
node.dynamic_nodes.iter().for_each(|node| match node {
|
||||
DynamicNode::Component(c) => {
|
||||
if let Some(f) = c.scope.get() {
|
||||
self.drop_scope(f);
|
||||
}
|
||||
c.props.take();
|
||||
}
|
||||
DynamicNode::Fragment(nodes) => {
|
||||
nodes.iter().for_each(|node| self.drop_scope_inner(node))
|
||||
}
|
||||
DynamicNode::Placeholder(t) => {
|
||||
if let Some(id) = t.id.get() {
|
||||
self.try_reclaim(id);
|
||||
}
|
||||
}
|
||||
DynamicNode::Text(t) => {
|
||||
if let Some(id) = t.id.get() {
|
||||
self.try_reclaim(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for root in node.root_ids {
|
||||
if let Some(id) = root.get() {
|
||||
if id.0 != 0 {
|
||||
self.try_reclaim(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Descend through the tree, removing any borrowed props and listeners
|
||||
pub(crate) fn ensure_drop_safety(&self, scope: ScopeId) {
|
||||
let scope = &self.scopes[scope.0];
|
||||
|
||||
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
|
||||
// run the hooks (which hold an &mut Reference)
|
||||
// recursively call ensure_drop_safety on all children
|
||||
let mut props = scope.borrowed_props.borrow_mut();
|
||||
props.drain(..).for_each(|comp| {
|
||||
let comp = unsafe { &*comp };
|
||||
if let Some(scope_id) = comp.scope.get() {
|
||||
self.ensure_drop_safety(scope_id);
|
||||
}
|
||||
drop(comp.props.take());
|
||||
});
|
||||
|
||||
// Now that all the references are gone, we can safely drop our own references in our listeners.
|
||||
let mut listeners = scope.listeners.borrow_mut();
|
||||
listeners.drain(..).for_each(|listener| {
|
||||
let listener = unsafe { &*listener };
|
||||
if let AttributeValue::Listener(l) = &listener.value {
|
||||
_ = l.take();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use crate::nodes::RenderReturn;
|
||||
use bumpalo::Bump;
|
||||
use std::cell::Cell;
|
||||
|
||||
pub(crate) struct BumpFrame {
|
||||
pub bump: Bump,
|
||||
pub node: Cell<*const RenderReturn<'static>>,
|
||||
}
|
||||
|
||||
impl BumpFrame {
|
||||
pub(crate) fn new(capacity: usize) -> Self {
|
||||
let bump = Bump::with_capacity(capacity);
|
||||
Self {
|
||||
bump,
|
||||
node: Cell::new(std::ptr::null()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new lifetime out of thin air
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
use crate::any_props::AnyProps;
|
||||
use crate::innerlude::{VComponent, VPlaceholder, 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, RenderReturn, ScopeId, SuspenseContext};
|
||||
use std::cell::Cell;
|
||||
use std::iter::{Enumerate, Peekable};
|
||||
use std::rc::Rc;
|
||||
use std::slice;
|
||||
use TemplateNode::*;
|
||||
|
||||
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.
|
||||
pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize {
|
||||
self.scope_stack.push(scope);
|
||||
let out = self.create(template);
|
||||
self.scope_stack.pop();
|
||||
out
|
||||
}
|
||||
|
||||
/// Create this template and write its mutations
|
||||
pub(crate) fn create(&mut self, node: &'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(&node.template.name) {
|
||||
self.register_template(node);
|
||||
}
|
||||
|
||||
// we know that this will generate at least one mutation per node
|
||||
self.mutations.edits.reserve(node.template.roots.len());
|
||||
|
||||
// Walk the roots, creating nodes and assigning IDs
|
||||
// todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS)
|
||||
let mut attrs = node.template.attr_paths.iter().enumerate().peekable();
|
||||
let mut nodes = node.template.node_paths.iter().enumerate().peekable();
|
||||
|
||||
node.template
|
||||
.roots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, root)| match root {
|
||||
DynamicText { id } | Dynamic { id } => {
|
||||
nodes.next().unwrap();
|
||||
self.write_dynamic_root(node, *id)
|
||||
}
|
||||
Element { .. } => self.write_element_root(node, idx, &mut attrs, &mut nodes),
|
||||
Text { .. } => self.write_static_text_root(node, idx),
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn write_static_text_root(&mut self, node: &VNode, idx: usize) -> usize {
|
||||
// Simply just load the template root, no modifications needed
|
||||
self.load_template_root(node, idx);
|
||||
|
||||
// Text producs just one node on the stack
|
||||
1
|
||||
}
|
||||
|
||||
fn write_dynamic_root(&mut self, template: &'b VNode<'b>, idx: usize) -> usize {
|
||||
use DynamicNode::*;
|
||||
match &template.dynamic_nodes[idx] {
|
||||
node @ Fragment(_) => self.create_dynamic_node(template, node, idx),
|
||||
node @ Component { .. } => self.create_dynamic_node(template, node, idx),
|
||||
Placeholder(VPlaceholder { id }) => {
|
||||
let id = self.set_slot(template, id, idx);
|
||||
self.mutations.push(CreatePlaceholder { id });
|
||||
1
|
||||
}
|
||||
Text(VText { id, value }) => {
|
||||
let id = self.set_slot(template, id, idx);
|
||||
self.create_static_text(value, id);
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_static_text(&mut self, value: &str, id: ElementId) {
|
||||
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||
let unbounded_text: &str = unsafe { std::mem::transmute(value) };
|
||||
self.mutations.push(CreateTextNode {
|
||||
value: unbounded_text,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
/// We write all the descndent data for this element
|
||||
///
|
||||
/// Elements can contain other nodes - and those nodes can be dynamic or static
|
||||
///
|
||||
/// We want to make sure we write these nodes while on top of the root
|
||||
fn write_element_root(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
root_idx: usize,
|
||||
dynamic_attrs: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
|
||||
dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
|
||||
) -> usize {
|
||||
// Load the template root and get the ID for the node on the stack
|
||||
let root_on_stack = self.load_template_root(template, root_idx);
|
||||
|
||||
// Write all the attributes below this root
|
||||
self.write_attrs_on_root(dynamic_attrs, root_idx, root_on_stack, template);
|
||||
|
||||
// Load in all of the placeholder or dynamic content under this root too
|
||||
self.load_placeholders(dynamic_nodes, root_idx, template);
|
||||
|
||||
1
|
||||
}
|
||||
|
||||
/// Load all of the placeholder nodes for descendents of this root node
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// rsx! {
|
||||
/// div {
|
||||
/// // This is a placeholder
|
||||
/// some_value,
|
||||
///
|
||||
/// // Load this too
|
||||
/// "{some_text}"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn load_placeholders(
|
||||
&mut self,
|
||||
dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
|
||||
root_idx: usize,
|
||||
template: &'b VNode<'b>,
|
||||
) {
|
||||
let (start, end) = match collect_dyn_node_range(dynamic_nodes, root_idx) {
|
||||
Some((a, b)) => (a, b),
|
||||
None => return,
|
||||
};
|
||||
|
||||
for idx in (start..=end).rev() {
|
||||
let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx);
|
||||
if m > 0 {
|
||||
// The path is one shorter because the top node is the root
|
||||
let path = &template.template.node_paths[idx][1..];
|
||||
self.mutations.push(ReplacePlaceholder { m, path });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_attrs_on_root(
|
||||
&mut self,
|
||||
attrs: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
|
||||
root_idx: usize,
|
||||
root: ElementId,
|
||||
node: &VNode,
|
||||
) {
|
||||
while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p[0] == root_idx as u8) {
|
||||
let id = self.assign_static_node_as_dynamic(path, root, node, attr_id);
|
||||
|
||||
loop {
|
||||
self.write_attribute(&node.dynamic_attrs[attr_id], id);
|
||||
|
||||
// Only push the dynamic attributes forward if they match the current path (same element)
|
||||
match attrs.next_if(|(_, p)| *p == path) {
|
||||
Some((next_attr_id, _)) => attr_id = next_attr_id,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_attribute(&mut self, attribute: &crate::Attribute, id: ElementId) {
|
||||
// Make sure we set the attribute's associated id
|
||||
attribute.mounted_element.set(id);
|
||||
|
||||
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||
let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) };
|
||||
|
||||
match &attribute.value {
|
||||
AttributeValue::Text(value) => {
|
||||
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||
let unbounded_value: &str = 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: unbounded_name,
|
||||
value: *value,
|
||||
id,
|
||||
}),
|
||||
AttributeValue::Listener(_) => {
|
||||
self.mutations.push(NewEventListener {
|
||||
// all listeners start with "on"
|
||||
name: &unbounded_name[2..],
|
||||
id,
|
||||
})
|
||||
}
|
||||
AttributeValue::Float(_) => todo!(),
|
||||
AttributeValue::Int(_) => todo!(),
|
||||
AttributeValue::Any(_) => todo!(),
|
||||
AttributeValue::None => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
|
||||
// Get an ID for this root since it's a real root
|
||||
let this_id = self.next_root(template, root_idx);
|
||||
template.root_ids[root_idx].set(Some(this_id));
|
||||
|
||||
self.mutations.push(LoadTemplate {
|
||||
name: template.template.name,
|
||||
index: root_idx,
|
||||
id: this_id,
|
||||
});
|
||||
|
||||
this_id
|
||||
}
|
||||
|
||||
/// We have some dynamic attributes attached to a some node
|
||||
///
|
||||
/// That node needs to be loaded at runtime, so we need to give it an ID
|
||||
///
|
||||
/// If the node in question is on the stack, we just return that ID
|
||||
///
|
||||
/// If the node is not on the stack, we create a new ID for it and assign it
|
||||
fn assign_static_node_as_dynamic(
|
||||
&mut self,
|
||||
path: &'static [u8],
|
||||
this_id: ElementId,
|
||||
template: &VNode,
|
||||
attr_id: usize,
|
||||
) -> ElementId {
|
||||
if path.len() == 1 {
|
||||
return this_id;
|
||||
}
|
||||
|
||||
// if attribute is on a root node, then we've already created the element
|
||||
// Else, it's deep in the template and we should create a new id for it
|
||||
let id = self.next_element(template, template.template.attr_paths[attr_id]);
|
||||
|
||||
self.mutations.push(Mutation::AssignId {
|
||||
path: &path[1..],
|
||||
id,
|
||||
});
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Insert a new template into the VirtualDom's template registry
|
||||
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.name, template.template);
|
||||
|
||||
// If it's all dynamic nodes, then we don't need to register it
|
||||
if !template.template.is_completely_dynamic() {
|
||||
self.mutations.templates.push(template.template);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_dynamic_node(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
node: &'b DynamicNode<'b>,
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
use DynamicNode::*;
|
||||
match node {
|
||||
Text(text) => self.create_dynamic_text(template, text, idx),
|
||||
Fragment(frag) => self.create_fragment(frag),
|
||||
Placeholder(frag) => self.create_placeholder(frag, template, idx),
|
||||
Component(component) => self.create_component_node(template, component, idx),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_dynamic_text(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
text: &'b VText<'b>,
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
// Allocate a dynamic element reference for this text node
|
||||
let new_id = self.next_element(template, template.template.node_paths[idx]);
|
||||
|
||||
// Make sure the text node is assigned to the correct element
|
||||
text.id.set(Some(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,
|
||||
});
|
||||
|
||||
// Since we're hydrating an existing node, we don't create any new nodes
|
||||
0
|
||||
}
|
||||
|
||||
pub(crate) fn create_placeholder(
|
||||
&mut self,
|
||||
placeholder: &VPlaceholder,
|
||||
template: &'b VNode<'b>,
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
// 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
|
||||
placeholder.id.set(Some(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
|
||||
}
|
||||
|
||||
pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
|
||||
nodes.iter().map(|child| self.create(child)).sum()
|
||||
}
|
||||
|
||||
pub(super) fn create_component_node(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
component: &'b VComponent<'b>,
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
let scope = match component.props.take() {
|
||||
Some(props) => {
|
||||
let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
|
||||
let scope = self.new_scope(unbounded_props, component.name);
|
||||
scope.id
|
||||
}
|
||||
|
||||
// Component is coming back, it probably still exists, right?
|
||||
None => component.scope.get().unwrap(),
|
||||
};
|
||||
|
||||
component.scope.set(Some(scope));
|
||||
|
||||
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
||||
|
||||
use RenderReturn::*;
|
||||
|
||||
match return_nodes {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
fn mount_component(
|
||||
&mut self,
|
||||
scope: ScopeId,
|
||||
parent: &'b VNode<'b>,
|
||||
new: &'b VNode<'b>,
|
||||
idx: usize,
|
||||
) -> 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.edits.len();
|
||||
|
||||
// Create the component's root element
|
||||
let created = self.create_scope(scope, new);
|
||||
|
||||
// If there are no suspense leaves below us, then just don't bother checking anything suspense related
|
||||
if self.collected_leaves.is_empty() {
|
||||
return created;
|
||||
}
|
||||
|
||||
// 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::<Rc<SuspenseContext>>() {
|
||||
Some(boundary) => boundary,
|
||||
_ => return created,
|
||||
};
|
||||
|
||||
// Since this is a boundary, use its placeholder within the template as the placeholder for the suspense tree
|
||||
let new_id = self.next_element(new, parent.template.node_paths[idx]);
|
||||
|
||||
// Now connect everything to the boundary
|
||||
self.scopes[scope.0].placeholder.set(Some(new_id));
|
||||
|
||||
// This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary
|
||||
// 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.edits.split_off(mutations_to_this_point),
|
||||
)
|
||||
};
|
||||
boundary.mutations.borrow_mut().edits.extend(split_off);
|
||||
boundary.created_on_stack.set(created);
|
||||
boundary
|
||||
.waiting_on
|
||||
.borrow_mut()
|
||||
.extend(self.collected_leaves.drain(..));
|
||||
|
||||
// Now assign the placeholder in the DOM
|
||||
self.mutations.push(AssignId {
|
||||
id: new_id,
|
||||
path: &parent.template.node_paths[idx][1..],
|
||||
});
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// Take the rendered nodes from a component and handle them if they were async
|
||||
///
|
||||
/// IE simply assign an ID to the placeholder
|
||||
fn mount_component_placeholder(
|
||||
&mut self,
|
||||
template: &VNode,
|
||||
idx: usize,
|
||||
scope: ScopeId,
|
||||
) -> usize {
|
||||
let new_id = self.next_element(template, template.template.node_paths[idx]);
|
||||
|
||||
// Set the placeholder of the scope
|
||||
self.scopes[scope.0].placeholder.set(Some(new_id));
|
||||
|
||||
// Since the placeholder is already in the DOM, we don't create any new nodes
|
||||
self.mutations.push(AssignId {
|
||||
id: new_id,
|
||||
path: &template.template.node_paths[idx][1..],
|
||||
});
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
fn set_slot(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
slot: &'b Cell<Option<ElementId>>,
|
||||
id: usize,
|
||||
) -> ElementId {
|
||||
let id = self.next_element(template, template.template.node_paths[id]);
|
||||
slot.set(Some(id));
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_dyn_node_range(
|
||||
dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&[u8]>>>,
|
||||
root_idx: usize,
|
||||
) -> Option<(usize, usize)> {
|
||||
let start = match dynamic_nodes.peek() {
|
||||
Some((idx, p)) if p[0] == root_idx as u8 => *idx,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let mut end = start;
|
||||
|
||||
while let Some((idx, p)) = dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) {
|
||||
if p.len() == 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
end = idx;
|
||||
}
|
||||
|
||||
Some((start, end))
|
||||
}
|
||||
+621
-767
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,21 @@
|
||||
use std::hash::Hash;
|
||||
|
||||
use crate::ScopeId;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialOrd, Ord)]
|
||||
pub struct DirtyScope {
|
||||
pub height: u32,
|
||||
pub id: ScopeId,
|
||||
}
|
||||
|
||||
impl PartialEq for DirtyScope {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for DirtyScope {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use crate::ScopeId;
|
||||
|
||||
/// A boundary that will capture any errors from child components
|
||||
#[allow(dead_code)]
|
||||
pub struct ErrorBoundary {
|
||||
error: RefCell<Option<(anyhow::Error, ScopeId)>>,
|
||||
id: ScopeId,
|
||||
}
|
||||
|
||||
impl ErrorBoundary {
|
||||
pub fn new(id: ScopeId) -> Self {
|
||||
Self {
|
||||
error: RefCell::new(None),
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
-156
@@ -1,182 +1,165 @@
|
||||
//! Internal and external event system
|
||||
//!
|
||||
//!
|
||||
//! This is all kinda WIP, but the bones are there.
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::{ElementId, ScopeId};
|
||||
use std::{any::Any, cell::Cell, fmt::Debug, rc::Rc, sync::Arc};
|
||||
|
||||
pub(crate) struct BubbleState {
|
||||
pub canceled: 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> {
|
||||
/// The data associated with this event
|
||||
pub data: Rc<T>,
|
||||
pub(crate) propogates: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl BubbleState {
|
||||
pub fn new() -> Self {
|
||||
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 Event<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
canceled: Cell::new(false),
|
||||
propogates: self.propogates.clone(),
|
||||
data: self.data.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// User Events are events that are shuttled from the renderer into the [`VirtualDom`] through the scheduler channel.
|
||||
impl<T> std::ops::Deref for Event<T> {
|
||||
type Target = Rc<T>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
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.propogates)
|
||||
.field("data", &self.data)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
|
||||
///
|
||||
/// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree
|
||||
/// where each listener is checked and fired if the event name matches.
|
||||
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
|
||||
///
|
||||
/// It is the expectation that the event name matches the corresponding event listener, otherwise Dioxus will panic in
|
||||
/// attempting to downcast the event data.
|
||||
///
|
||||
/// Because Event Data is sent across threads, it must be `Send + Sync`. We are hoping to lift the `Sync` restriction but
|
||||
/// `Send` will not be lifted. The entire `UserEvent` must also be `Send + Sync` due to its use in the scheduler channel.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn App(cx: Scope) -> Element {
|
||||
/// render!(div {
|
||||
/// onclick: move |_| println!("Clicked!")
|
||||
/// rsx!{
|
||||
/// MyComponent { onclick: move |evt| log::info!("clicked") }
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Props)]
|
||||
/// struct MyProps<'a> {
|
||||
/// onclick: EventHandler<'a, MouseEvent>,
|
||||
/// }
|
||||
///
|
||||
/// fn MyComponent(cx: Scope<'a, MyProps<'a>>) -> Element {
|
||||
/// cx.render(rsx!{
|
||||
/// button {
|
||||
/// onclick: move |evt| cx.props.onclick.call(evt),
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// let mut dom = VirtualDom::new(App);
|
||||
/// let mut scheduler = dom.get_scheduler_channel();
|
||||
/// scheduler.unbounded_send(SchedulerMsg::UiEvent(
|
||||
/// UserEvent {
|
||||
/// scope_id: None,
|
||||
/// priority: EventPriority::Medium,
|
||||
/// name: "click",
|
||||
/// element: Some(ElementId(0)),
|
||||
/// data: Arc::new(ClickEvent { .. })
|
||||
/// }
|
||||
/// )).unwrap();
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserEvent {
|
||||
/// The originator of the event trigger if available
|
||||
pub scope_id: Option<ScopeId>,
|
||||
|
||||
/// The priority of the event to be scheduled around ongoing work
|
||||
pub priority: EventPriority,
|
||||
|
||||
/// The optional real node associated with the trigger
|
||||
pub element: Option<ElementId>,
|
||||
|
||||
/// The event type IE "onclick" or "onmouseover"
|
||||
pub name: &'static str,
|
||||
|
||||
/// If the event is bubbles up through the vdom
|
||||
pub bubbles: bool,
|
||||
|
||||
/// The event data to be passed onto the event handler
|
||||
pub data: Arc<dyn Any + Send + Sync>,
|
||||
pub struct EventHandler<'bump, T = ()> {
|
||||
pub(super) callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// The internal Dioxus type that carries any event data to the relevant handler.
|
||||
|
||||
pub struct AnyEvent {
|
||||
pub(crate) bubble_state: Rc<BubbleState>,
|
||||
pub(crate) data: Arc<dyn Any + Send + Sync>,
|
||||
}
|
||||
|
||||
impl AnyEvent {
|
||||
/// Convert this [`AnyEvent`] into a specific [`UiEvent`] with [`EventData`].
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let evt: FormEvent = evvt.downcast().unwrap();
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn downcast<T: Send + Sync + 'static>(self) -> Option<UiEvent<T>> {
|
||||
let AnyEvent { data, bubble_state } = self;
|
||||
|
||||
data.downcast::<T>()
|
||||
.ok()
|
||||
.map(|data| UiEvent { data, bubble_state })
|
||||
impl<T> Default for EventHandler<'_, T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
callback: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`UiEvent`] is a type that wraps various [`EventData`].
|
||||
///
|
||||
/// You should prefer to use the name of the event directly, rather than
|
||||
/// the [`UiEvent`]<T> generic type.
|
||||
///
|
||||
/// For the HTML crate, this would include `MouseEvent`, `FormEvent` etc.
|
||||
pub struct UiEvent<T> {
|
||||
/// The internal data of the event
|
||||
/// This is wrapped in an Arc so that it can be sent across threads
|
||||
pub data: Arc<T>,
|
||||
type ExternalListenerCallback<'bump, T> = bumpalo::boxed::Box<'bump, dyn FnMut(T) + 'bump>;
|
||||
|
||||
#[allow(unused)]
|
||||
bubble_state: Rc<BubbleState>,
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> std::fmt::Debug for UiEvent<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UiEvent").field("data", &self.data).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for UiEvent<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.data.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UiEvent<T> {
|
||||
/// Prevent this event from bubbling up the tree.
|
||||
pub fn cancel_bubble(&self) {
|
||||
self.bubble_state.canceled.set(true);
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use crate::innerlude::*;
|
||||
|
||||
/// Create inline fragments using Component syntax.
|
||||
///
|
||||
/// ## Details
|
||||
///
|
||||
/// Fragments capture a series of children without rendering extra nodes.
|
||||
///
|
||||
/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and
|
||||
/// a key is needed to identify each item.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// rsx!{
|
||||
/// Fragment { key: "abc" }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
|
||||
/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments.
|
||||
///
|
||||
/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
|
||||
///
|
||||
/// 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().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
Ok(VNode {
|
||||
key: children.key,
|
||||
parent: children.parent,
|
||||
template: children.template,
|
||||
root_ids: children.root_ids,
|
||||
dynamic_nodes: children.dynamic_nodes,
|
||||
dynamic_attrs: children.dynamic_attrs,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct FragmentProps<'a>(Element<'a>);
|
||||
pub struct FragmentBuilder<'a, const BUILT: bool>(Element<'a>);
|
||||
impl<'a> FragmentBuilder<'a, false> {
|
||||
pub fn children(self, children: Element<'a>) -> FragmentBuilder<'a, true> {
|
||||
FragmentBuilder(children)
|
||||
}
|
||||
}
|
||||
impl<'a, const A: bool> FragmentBuilder<'a, A> {
|
||||
pub fn build(self) -> FragmentProps<'a> {
|
||||
FragmentProps(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Access the children elements passed into the component
|
||||
///
|
||||
/// This enables patterns where a component is passed children from its parent.
|
||||
///
|
||||
/// ## Details
|
||||
///
|
||||
/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions
|
||||
/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure
|
||||
/// on the props that takes Context.
|
||||
///
|
||||
/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other
|
||||
/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's
|
||||
/// props are valid for the static lifetime.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn App(cx: Scope) -> Element {
|
||||
/// cx.render(rsx!{
|
||||
/// CustomCard {
|
||||
/// h1 {}
|
||||
/// p {}
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// #[derive(PartialEq, Props)]
|
||||
/// struct CardProps {
|
||||
/// children: Element
|
||||
/// }
|
||||
///
|
||||
/// fn CustomCard(cx: Scope<CardProps>) -> Element {
|
||||
/// cx.render(rsx!{
|
||||
/// div {
|
||||
/// h1 {"Title card"}
|
||||
/// {cx.props.children}
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
impl<'a> Properties for FragmentProps<'a> {
|
||||
type Builder = FragmentBuilder<'a, false>;
|
||||
const IS_STATIC: bool = false;
|
||||
fn builder() -> Self::Builder {
|
||||
FragmentBuilder(VNode::empty())
|
||||
}
|
||||
unsafe fn memoize(&self, _other: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
+22
-273
@@ -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.
|
||||
@@ -11,27 +11,25 @@
|
||||
//! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
|
||||
//! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
|
||||
|
||||
use crate::innerlude::{NodeFactory, VNode};
|
||||
use std::mem;
|
||||
use smallbox::{smallbox, space::S16, SmallBox};
|
||||
|
||||
use crate::{innerlude::VNode, ScopeState};
|
||||
|
||||
/// 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
|
||||
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
|
||||
/// ```
|
||||
pub struct LazyNodes<'a, 'b> {
|
||||
inner: StackNodeStorage<'a, 'b>,
|
||||
}
|
||||
#[cfg(not(miri))]
|
||||
inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
|
||||
|
||||
type StackHeapSize = [usize; 16];
|
||||
|
||||
enum StackNodeStorage<'a, 'b> {
|
||||
Stack(LazyStack),
|
||||
Heap(Box<dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b>),
|
||||
#[cfg(miri)]
|
||||
inner: Box<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> LazyNodes<'a, 'b> {
|
||||
@@ -40,118 +38,22 @@ 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>>| {
|
||||
fac.map(
|
||||
slot.take()
|
||||
.expect("LazyNodes closure to be called only once"),
|
||||
)
|
||||
};
|
||||
|
||||
// miri does not know how to work with mucking directly into bytes
|
||||
// just use a heap allocated type when miri is running
|
||||
if cfg!(miri) {
|
||||
Self {
|
||||
inner: StackNodeStorage::Heap(Box::new(val)),
|
||||
}
|
||||
} else {
|
||||
unsafe { LazyNodes::new_inner(val) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
{
|
||||
// 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>>| {
|
||||
fac.map(
|
||||
slot.take()
|
||||
.expect("LazyNodes closure to be called only once"),
|
||||
)
|
||||
})),
|
||||
}
|
||||
}
|
||||
#[cfg(not(miri))]
|
||||
inner: smallbox!(move |f| {
|
||||
let val = slot.take().expect("cannot call LazyNodes twice");
|
||||
val(f)
|
||||
}),
|
||||
|
||||
unsafe fn new_inner<F>(val: F) -> Self
|
||||
where
|
||||
F: FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b,
|
||||
{
|
||||
let mut ptr: *const _ = &val as &dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>>;
|
||||
|
||||
assert_eq!(
|
||||
ptr as *const u8, &val as *const _ as *const u8,
|
||||
"MISUSE: Closure returned different pointer"
|
||||
);
|
||||
assert_eq!(
|
||||
std::mem::size_of_val(&*ptr),
|
||||
std::mem::size_of::<F>(),
|
||||
"MISUSE: Closure returned a subset pointer"
|
||||
);
|
||||
|
||||
let words = ptr_as_slice(&mut ptr);
|
||||
assert!(
|
||||
words[0] == &val as *const _ as usize,
|
||||
"BUG: Pointer layout is not (data_ptr, info...)"
|
||||
);
|
||||
|
||||
// - Ensure that Self is aligned same as data requires
|
||||
assert!(
|
||||
std::mem::align_of::<F>() <= std::mem::align_of::<Self>(),
|
||||
"TODO: Enforce alignment >{} (requires {})",
|
||||
std::mem::align_of::<Self>(),
|
||||
std::mem::align_of::<F>()
|
||||
);
|
||||
|
||||
let info = &words[1..];
|
||||
let data = words[0] as *mut ();
|
||||
let size = mem::size_of::<F>();
|
||||
|
||||
let stored_size = info.len() * mem::size_of::<usize>() + size;
|
||||
let max_size = mem::size_of::<StackHeapSize>();
|
||||
|
||||
if stored_size > max_size {
|
||||
Self {
|
||||
inner: StackNodeStorage::Heap(Box::new(val)),
|
||||
}
|
||||
} else {
|
||||
let mut buf: StackHeapSize = StackHeapSize::default();
|
||||
|
||||
assert!(info.len() + round_to_words(size) <= buf.as_ref().len());
|
||||
|
||||
// Place pointer information at the end of the region
|
||||
// - Allows the data to be at the start for alignment purposes
|
||||
{
|
||||
let info_ofs = buf.as_ref().len() - info.len();
|
||||
let info_dst = &mut buf.as_mut()[info_ofs..];
|
||||
for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) {
|
||||
*d = *v;
|
||||
}
|
||||
}
|
||||
|
||||
let src_ptr = data as *const u8;
|
||||
let dataptr = buf.as_mut_ptr().cast::<u8>();
|
||||
|
||||
for i in 0..size {
|
||||
*dataptr.add(i) = *src_ptr.add(i);
|
||||
}
|
||||
|
||||
std::mem::forget(val);
|
||||
|
||||
Self {
|
||||
inner: StackNodeStorage::Stack(LazyStack {
|
||||
_align: [],
|
||||
buf,
|
||||
dropped: false,
|
||||
}),
|
||||
}
|
||||
#[cfg(miri)]
|
||||
inner: Box::new(move |f| {
|
||||
let val = slot.take().expect("cannot call LazyNodes twice");
|
||||
val(f)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,163 +62,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> {
|
||||
match self.inner {
|
||||
StackNodeStorage::Heap(mut lazy) => {
|
||||
lazy(Some(f)).expect("Closure should not be called twice")
|
||||
}
|
||||
StackNodeStorage::Stack(mut stack) => stack.call(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LazyStack {
|
||||
_align: [u64; 0],
|
||||
buf: StackHeapSize,
|
||||
dropped: bool,
|
||||
}
|
||||
|
||||
impl LazyStack {
|
||||
fn call<'a>(&mut self, f: NodeFactory<'a>) -> 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::<usize>()
|
||||
- 1;
|
||||
|
||||
let info_ofs = data.len() - info_size;
|
||||
|
||||
let g: *mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> =
|
||||
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
|
||||
|
||||
self.dropped = true;
|
||||
|
||||
let clos = unsafe { &mut *g };
|
||||
clos(Some(f)).unwrap()
|
||||
}
|
||||
}
|
||||
impl Drop for LazyStack {
|
||||
fn drop(&mut self) {
|
||||
if !self.dropped {
|
||||
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_ofs = data.len() - info_size;
|
||||
|
||||
let g: *mut dyn FnMut(Option<NodeFactory<'_>>) -> Option<VNode<'_>> =
|
||||
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
|
||||
|
||||
self.dropped = true;
|
||||
|
||||
let clos = unsafe { &mut *g };
|
||||
clos(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtain mutable access to a pointer's words
|
||||
fn ptr_as_slice<T>(ptr: &mut T) -> &mut [usize] {
|
||||
assert!(mem::size_of::<T>() % mem::size_of::<usize>() == 0);
|
||||
let words = mem::size_of::<T>() / mem::size_of::<usize>();
|
||||
// SAFE: Points to valid memory (a raw pointer)
|
||||
unsafe { core::slice::from_raw_parts_mut(ptr as *mut _ as *mut usize, words) }
|
||||
}
|
||||
|
||||
/// Re-construct a fat pointer
|
||||
unsafe fn make_fat_ptr<T: ?Sized>(data_ptr: usize, meta_vals: &[usize]) -> *mut T {
|
||||
let mut rv = mem::MaybeUninit::<*mut T>::uninit();
|
||||
{
|
||||
let s = ptr_as_slice(&mut rv);
|
||||
s[0] = data_ptr;
|
||||
s[1..].copy_from_slice(meta_vals);
|
||||
}
|
||||
let rv = rv.assume_init();
|
||||
assert_eq!(rv as *const (), data_ptr as *const ());
|
||||
rv
|
||||
}
|
||||
|
||||
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);
|
||||
pub fn call(mut self, f: &'a ScopeState) -> VNode<'a> {
|
||||
(self.inner)(f)
|
||||
}
|
||||
}
|
||||
+34
-59
@@ -1,33 +1,43 @@
|
||||
#![allow(non_snake_case)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub(crate) mod arbitrary_value;
|
||||
pub(crate) mod diff;
|
||||
pub(crate) mod events;
|
||||
pub(crate) mod lazynodes;
|
||||
pub(crate) mod mutations;
|
||||
pub(crate) mod nodes;
|
||||
pub(crate) mod properties;
|
||||
pub(crate) mod scopes;
|
||||
pub(crate) mod util;
|
||||
pub(crate) mod virtual_dom;
|
||||
mod any_props;
|
||||
mod arena;
|
||||
mod bump_frame;
|
||||
mod create;
|
||||
mod diff;
|
||||
mod dirty_scope;
|
||||
mod error_boundary;
|
||||
mod events;
|
||||
mod fragment;
|
||||
mod lazynodes;
|
||||
mod mutations;
|
||||
mod nodes;
|
||||
mod properties;
|
||||
mod scheduler;
|
||||
mod scope_arena;
|
||||
mod scopes;
|
||||
mod virtual_dom;
|
||||
|
||||
pub(crate) mod innerlude {
|
||||
pub use crate::arbitrary_value::*;
|
||||
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::*;
|
||||
pub use crate::scopes::*;
|
||||
pub use crate::util::*;
|
||||
pub use crate::virtual_dom::*;
|
||||
|
||||
/// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`].
|
||||
/// An [`Element`] is a possibly-errored [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`].
|
||||
///
|
||||
/// Any [`None`] [`Element`] will automatically be coerced into a placeholder [`VNode`] with the [`VNode::Placeholder`] variant.
|
||||
pub type Element<'a> = Option<VNode<'a>>;
|
||||
/// An Errored [`Element`] will propagate the error to the nearest error boundary.
|
||||
pub type Element<'a> = Result<VNode<'a>, anyhow::Error>;
|
||||
|
||||
/// A [`Component`] is a function that takes a [`Scope`] and returns an [`Element`].
|
||||
///
|
||||
@@ -57,16 +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::{
|
||||
AnyEvent, Attribute, AttributeValue, Component, DioxusElement, DomEdit, Element, ElementId,
|
||||
ElementIdIterator, EventHandler, EventPriority, IntoVNode, LazyNodes, Listener, Mutations,
|
||||
NodeFactory, Properties, SchedulerMsg, Scope, ScopeId, ScopeState, TaskId, UiEvent, UserEvent,
|
||||
VComponent, VElement, VFragment, VNode, VPlaceholder, 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
|
||||
@@ -74,8 +81,9 @@ pub use crate::innerlude::{
|
||||
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
||||
pub mod prelude {
|
||||
pub use crate::innerlude::{
|
||||
fc_to_builder, Attributes, Component, DioxusElement, Element, EventHandler, Fragment,
|
||||
LazyNodes, NodeFactory, Properties, Scope, ScopeId, ScopeState, VNode, VirtualDom,
|
||||
fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties,
|
||||
Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode,
|
||||
VNode, VirtualDom,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,37 +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;
|
||||
}
|
||||
|
||||
/// Functions that wrap unsafe functionality to prevent us from misusing it at the callsite
|
||||
pub(crate) mod unsafe_utils {
|
||||
use crate::VNode;
|
||||
|
||||
pub(crate) unsafe fn extend_vnode<'a, 'b>(node: &'a VNode<'a>) -> &'b VNode<'b> {
|
||||
std::mem::transmute(node)
|
||||
}
|
||||
}
|
||||
|
||||
#[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();
|
||||
)*}
|
||||
}
|
||||
+195
-288
@@ -1,350 +1,257 @@
|
||||
//! Instructions returned by the VirtualDOM on how to modify the Real DOM.
|
||||
//!
|
||||
//! This module contains an internal API to generate these instructions.
|
||||
//!
|
||||
//! Beware that changing code in this module will break compatibility with
|
||||
//! interpreters for these types of DomEdits.
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::innerlude::*;
|
||||
use std::{any::Any, fmt::Debug};
|
||||
use crate::{arena::ElementId, ScopeId, Template};
|
||||
|
||||
/// ## Mutations
|
||||
/// A container for all the relevant steps to modify the Real DOM
|
||||
///
|
||||
/// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also
|
||||
/// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has
|
||||
/// applied the edits.
|
||||
/// 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.
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
|
||||
#[derive(Debug, Default)]
|
||||
#[must_use = "not handling edits can lead to visual inconsistencies in UI"]
|
||||
pub struct Mutations<'a> {
|
||||
/// The list of edits that need to be applied for the RealDOM to match the VirtualDOM.
|
||||
pub edits: Vec<DomEdit<'a>>,
|
||||
/// The ID of the subtree that these edits are targetting
|
||||
pub subtree: usize,
|
||||
|
||||
/// The list of Scopes that were diffed, created, and removed during the Diff process.
|
||||
pub dirty_scopes: FxHashSet<ScopeId>,
|
||||
|
||||
/// The list of nodes to connect to the RealDOM.
|
||||
pub refs: Vec<NodeRefMutation<'a>>,
|
||||
/// 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 Debug for Mutations<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Mutations")
|
||||
.field("edits", &self.edits)
|
||||
.field("noderefs", &self.refs)
|
||||
.finish()
|
||||
impl<'a> Mutations<'a> {
|
||||
/// 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.edits.iter_mut() {
|
||||
if let Mutation::LoadTemplate { name, .. } = edit {
|
||||
*name = "template"
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Push a new mutation into the dom_edits list
|
||||
pub(crate) fn push(&mut self, mutation: Mutation<'static>) {
|
||||
self.edits.push(mutation)
|
||||
}
|
||||
}
|
||||
|
||||
/// A `DomEdit` represents a serialized form of the VirtualDom's trait-based API. This allows streaming edits across the
|
||||
/// network or through FFI boundaries.
|
||||
#[derive(Debug, PartialEq)]
|
||||
/// 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),
|
||||
serde(tag = "type")
|
||||
)]
|
||||
pub enum DomEdit<'bump> {
|
||||
/// Push the given root node onto our stack.
|
||||
PushRoot {
|
||||
/// The ID of the root node to push.
|
||||
root: u64,
|
||||
},
|
||||
|
||||
/// Pop the topmost node from our stack and append them to the node
|
||||
/// at the top of the stack.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Mutation<'a> {
|
||||
/// Add these m children to the target element
|
||||
AppendChildren {
|
||||
/// How many nodes should be popped from the stack.
|
||||
/// The node remaining on the stack will be the target for the append.
|
||||
many: u32,
|
||||
/// The ID of the element being mounted to
|
||||
id: ElementId,
|
||||
|
||||
/// The number of nodes on the stack
|
||||
m: usize,
|
||||
},
|
||||
|
||||
/// Replace a given (single) node with a handful of nodes currently on the stack.
|
||||
ReplaceWith {
|
||||
/// The ID of the node to be replaced.
|
||||
root: u64,
|
||||
/// 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],
|
||||
|
||||
/// How many nodes should be popped from the stack to replace the target node.
|
||||
m: u32,
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// 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 {
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// Insert a number of nodes after a given node.
|
||||
InsertAfter {
|
||||
/// The ID of the node to insert after.
|
||||
root: u64,
|
||||
id: ElementId,
|
||||
|
||||
/// How many nodes should be popped from the stack to insert after the target node.
|
||||
n: u32,
|
||||
/// The ids of the nodes to insert after the target node.
|
||||
m: usize,
|
||||
},
|
||||
|
||||
/// Insert a number of nodes before a given node.
|
||||
InsertBefore {
|
||||
/// The ID of the node to insert before.
|
||||
root: u64,
|
||||
id: ElementId,
|
||||
|
||||
/// How many nodes should be popped from the stack to insert before the target node.
|
||||
n: u32,
|
||||
/// The ids of the nodes to insert before the target node.
|
||||
m: usize,
|
||||
},
|
||||
|
||||
/// Remove a particular node from the DOM
|
||||
Remove {
|
||||
/// The ID of the node to remove.
|
||||
root: u64,
|
||||
/// 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,
|
||||
|
||||
/// The (optional) namespace of the attribute.
|
||||
/// For instance, "style" is in the "style" namespace.
|
||||
ns: Option<&'a str>,
|
||||
},
|
||||
|
||||
/// Create a new purely-text node
|
||||
CreateTextNode {
|
||||
/// The ID the new node should have.
|
||||
root: u64,
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// Set the textcontent of a node.
|
||||
SetText {
|
||||
/// The textcontent of the node
|
||||
text: &'bump str,
|
||||
},
|
||||
value: &'a str,
|
||||
|
||||
/// Create a new purely-element node
|
||||
CreateElement {
|
||||
/// The ID the new node should have.
|
||||
root: u64,
|
||||
|
||||
/// The tagname of the node
|
||||
tag: &'bump str,
|
||||
},
|
||||
|
||||
/// Create a new purely-comment node with a given namespace
|
||||
CreateElementNs {
|
||||
/// The ID the new node should have.
|
||||
root: u64,
|
||||
|
||||
/// The namespace of the node
|
||||
tag: &'bump str,
|
||||
|
||||
/// The namespace of the node (like `SVG`)
|
||||
ns: &'static str,
|
||||
},
|
||||
|
||||
/// Create a new placeholder node.
|
||||
/// In most implementations, this will either be a hidden div or a comment node.
|
||||
CreatePlaceholder {
|
||||
/// The ID the new node should have.
|
||||
root: u64,
|
||||
/// 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: &'static str,
|
||||
name: &'a str,
|
||||
|
||||
/// The ID of the node to attach the listener to.
|
||||
scope: ScopeId,
|
||||
|
||||
/// The ID of the node to attach the listener to.
|
||||
root: u64,
|
||||
id: ElementId,
|
||||
},
|
||||
|
||||
/// Remove an existing Event Listener.
|
||||
RemoveEventListener {
|
||||
/// The ID of the node to remove.
|
||||
root: u64,
|
||||
|
||||
/// The name of the event to remove.
|
||||
event: &'static str,
|
||||
},
|
||||
name: &'a str,
|
||||
|
||||
/// Set the textcontent of a node.
|
||||
SetText {
|
||||
/// The ID of the node to set the textcontent of.
|
||||
root: u64,
|
||||
|
||||
/// The textcontent of the node
|
||||
text: &'bump str,
|
||||
},
|
||||
|
||||
/// Set the value of a node's attribute.
|
||||
SetAttribute {
|
||||
/// The ID of the node to set the attribute of.
|
||||
root: u64,
|
||||
|
||||
/// The name of the attribute to set.
|
||||
field: &'static str,
|
||||
|
||||
/// The value of the attribute.
|
||||
value: AttributeValue<'bump>,
|
||||
|
||||
// value: &'bump str,
|
||||
/// The (optional) namespace of the attribute.
|
||||
/// For instance, "style" is in the "style" namespace.
|
||||
ns: Option<&'bump str>,
|
||||
},
|
||||
|
||||
/// Remove an attribute from a node.
|
||||
RemoveAttribute {
|
||||
/// The ID of the node to remove.
|
||||
root: u64,
|
||||
|
||||
/// The name of the attribute to remove.
|
||||
name: &'static str,
|
||||
|
||||
/// The namespace of the attribute.
|
||||
ns: Option<&'bump str>,
|
||||
id: ElementId,
|
||||
},
|
||||
|
||||
/// Manually pop a root node from the stack.
|
||||
PopRoot {},
|
||||
}
|
||||
|
||||
use fxhash::FxHashSet;
|
||||
use DomEdit::*;
|
||||
|
||||
impl<'a> Mutations<'a> {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
edits: Vec::new(),
|
||||
refs: Vec::new(),
|
||||
dirty_scopes: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
pub(crate) fn push_root(&mut self, root: ElementId) {
|
||||
let id = root.as_u64();
|
||||
self.edits.push(PushRoot { root: id });
|
||||
}
|
||||
|
||||
// Navigation
|
||||
pub(crate) fn pop_root(&mut self) {
|
||||
self.edits.push(PopRoot {});
|
||||
}
|
||||
|
||||
pub(crate) fn replace_with(&mut self, root: ElementId, m: u32) {
|
||||
let root = root.as_u64();
|
||||
self.edits.push(ReplaceWith { m, root });
|
||||
}
|
||||
|
||||
pub(crate) fn insert_after(&mut self, root: ElementId, n: u32) {
|
||||
let root = root.as_u64();
|
||||
self.edits.push(InsertAfter { n, root });
|
||||
}
|
||||
|
||||
pub(crate) fn insert_before(&mut self, root: ElementId, n: u32) {
|
||||
let root = root.as_u64();
|
||||
self.edits.push(InsertBefore { n, root });
|
||||
}
|
||||
|
||||
pub(crate) fn append_children(&mut self, n: u32) {
|
||||
self.edits.push(AppendChildren { many: n });
|
||||
}
|
||||
|
||||
// Remove Nodes from the dom
|
||||
pub(crate) fn remove(&mut self, id: u64) {
|
||||
self.edits.push(Remove { root: id });
|
||||
}
|
||||
|
||||
// Create
|
||||
pub(crate) fn create_text_node(&mut self, text: &'a str, id: ElementId) {
|
||||
let id = id.as_u64();
|
||||
self.edits.push(CreateTextNode { text, root: id });
|
||||
}
|
||||
|
||||
pub(crate) fn create_element(
|
||||
&mut self,
|
||||
tag: &'static str,
|
||||
ns: Option<&'static str>,
|
||||
/// Remove a particular node from the DOM
|
||||
Remove {
|
||||
/// The ID of the node to remove.
|
||||
id: ElementId,
|
||||
) {
|
||||
let id = id.as_u64();
|
||||
match ns {
|
||||
Some(ns) => self.edits.push(CreateElementNs { root: id, ns, tag }),
|
||||
None => self.edits.push(CreateElement { root: id, tag }),
|
||||
}
|
||||
}
|
||||
// placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
|
||||
pub(crate) fn create_placeholder(&mut self, id: ElementId) {
|
||||
let id = id.as_u64();
|
||||
self.edits.push(CreatePlaceholder { root: id });
|
||||
}
|
||||
},
|
||||
|
||||
// events
|
||||
pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) {
|
||||
let Listener {
|
||||
event,
|
||||
mounted_node,
|
||||
..
|
||||
} = listener;
|
||||
|
||||
let element_id = mounted_node.get().unwrap().as_u64();
|
||||
|
||||
self.edits.push(NewEventListener {
|
||||
scope,
|
||||
event_name: event,
|
||||
root: element_id,
|
||||
});
|
||||
}
|
||||
pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: u64) {
|
||||
self.edits.push(RemoveEventListener { event, root });
|
||||
}
|
||||
|
||||
// modify
|
||||
pub(crate) fn set_text(&mut self, text: &'a str, root: u64) {
|
||||
self.edits.push(SetText { text, root });
|
||||
}
|
||||
|
||||
pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute<'a>, root: u64) {
|
||||
let Attribute {
|
||||
name,
|
||||
value,
|
||||
namespace,
|
||||
..
|
||||
} = attribute;
|
||||
|
||||
self.edits.push(SetAttribute {
|
||||
field: name,
|
||||
value: value.clone(),
|
||||
ns: *namespace,
|
||||
root,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) {
|
||||
let Attribute {
|
||||
name, namespace, ..
|
||||
} = attribute;
|
||||
|
||||
self.edits.push(RemoveAttribute {
|
||||
name,
|
||||
ns: *namespace,
|
||||
root,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) {
|
||||
self.dirty_scopes.insert(scope);
|
||||
}
|
||||
}
|
||||
|
||||
// refs are only assigned once
|
||||
pub struct NodeRefMutation<'a> {
|
||||
pub element: &'a mut Option<once_cell::sync::OnceCell<Box<dyn Any>>>,
|
||||
pub element_id: ElementId,
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Debug for NodeRefMutation<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("NodeRefMutation")
|
||||
.field("element_id", &self.element_id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> NodeRefMutation<'a> {
|
||||
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
|
||||
self.element
|
||||
.as_ref()
|
||||
.and_then(|f| f.get())
|
||||
.and_then(|f| f.downcast_ref::<T>())
|
||||
}
|
||||
pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
|
||||
self.element
|
||||
.as_mut()
|
||||
.and_then(|f| f.get_mut())
|
||||
.and_then(|f| f.downcast_mut::<T>())
|
||||
}
|
||||
/// Push the given root node onto our stack.
|
||||
PushRoot {
|
||||
/// The ID of the root node to push.
|
||||
id: ElementId,
|
||||
},
|
||||
}
|
||||
+543
-796
File diff suppressed because it is too large.
Load diff
@@ -1,100 +1,5 @@
|
||||
use crate::innerlude::*;
|
||||
|
||||
pub struct FragmentProps<'a>(Element<'a>);
|
||||
pub struct FragmentBuilder<'a, const BUILT: bool>(Element<'a>);
|
||||
impl<'a> FragmentBuilder<'a, false> {
|
||||
pub fn children(self, children: Element<'a>) -> FragmentBuilder<'a, true> {
|
||||
FragmentBuilder(children)
|
||||
}
|
||||
}
|
||||
impl<'a, const A: bool> FragmentBuilder<'a, A> {
|
||||
pub fn build(self) -> FragmentProps<'a> {
|
||||
FragmentProps(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Access the children elements passed into the component
|
||||
///
|
||||
/// This enables patterns where a component is passed children from its parent.
|
||||
///
|
||||
/// ## Details
|
||||
///
|
||||
/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions
|
||||
/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure
|
||||
/// on the props that takes Context.
|
||||
///
|
||||
/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other
|
||||
/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's
|
||||
/// props are valid for the static lifetime.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn App(cx: Scope) -> Element {
|
||||
/// cx.render(rsx!{
|
||||
/// CustomCard {
|
||||
/// h1 {}
|
||||
/// p {}
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// #[derive(PartialEq, Props)]
|
||||
/// struct CardProps {
|
||||
/// children: Element
|
||||
/// }
|
||||
///
|
||||
/// fn CustomCard(cx: Scope<CardProps>) -> Element {
|
||||
/// cx.render(rsx!{
|
||||
/// div {
|
||||
/// h1 {"Title card"}
|
||||
/// {cx.props.children}
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
impl<'a> Properties for FragmentProps<'a> {
|
||||
type Builder = FragmentBuilder<'a, false>;
|
||||
const IS_STATIC: bool = false;
|
||||
fn builder() -> Self::Builder {
|
||||
FragmentBuilder(None)
|
||||
}
|
||||
unsafe fn memoize(&self, _other: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Create inline fragments using Component syntax.
|
||||
///
|
||||
/// ## Details
|
||||
///
|
||||
/// Fragments capture a series of children without rendering extra nodes.
|
||||
///
|
||||
/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and
|
||||
/// a key is needed to identify each item.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// rsx!{
|
||||
/// Fragment { key: "abc" }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
|
||||
/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments.
|
||||
///
|
||||
/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
|
||||
///
|
||||
/// 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 i = cx.props.0.as_ref().map(|f| f.decouple());
|
||||
cx.render(LazyNodes::new(|f| f.fragment_from_iter(i)))
|
||||
}
|
||||
|
||||
/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus
|
||||
/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the
|
||||
/// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe.
|
||||
@@ -165,6 +70,6 @@ impl EmptyBuilder {
|
||||
|
||||
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
|
||||
/// to initialize a component's props.
|
||||
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element) -> T::Builder {
|
||||
pub fn fc_to_builder<'a, A, T: Properties + 'a>(_: fn(Scope<'a, T>) -> A) -> T::Builder {
|
||||
T::builder()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::ScopeId;
|
||||
use slab::Slab;
|
||||
|
||||
mod suspense;
|
||||
mod task;
|
||||
mod wait;
|
||||
mod waker;
|
||||
|
||||
pub use suspense::*;
|
||||
pub use task::*;
|
||||
pub use waker::RcWake;
|
||||
|
||||
/// 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(crate) enum SchedulerMsg {
|
||||
/// Immediate updates from Components that mark them as dirty
|
||||
Immediate(ScopeId),
|
||||
|
||||
/// A task has woken and needs to be progressed
|
||||
TaskNotified(TaskId),
|
||||
|
||||
/// A task has woken and needs to be progressed
|
||||
SuspenseNotified(SuspenseId),
|
||||
}
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub(crate) struct Scheduler {
|
||||
pub sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
|
||||
/// Tasks created with cx.spawn
|
||||
pub tasks: RefCell<Slab<Rc<LocalTask>>>,
|
||||
|
||||
/// Async components
|
||||
pub leaves: RefCell<Slab<Rc<SuspenseLeaf>>>,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
pub fn new(sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>) -> Rc<Self> {
|
||||
Rc::new(Scheduler {
|
||||
sender,
|
||||
tasks: RefCell::new(Slab::new()),
|
||||
leaves: RefCell::new(Slab::new()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use super::{waker::RcWake, SchedulerMsg};
|
||||
use crate::ElementId;
|
||||
use crate::{innerlude::Mutations, Element, ScopeId};
|
||||
use std::future::Future;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashSet,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// An ID representing an ongoing suspended component
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub(crate) struct SuspenseId(pub usize);
|
||||
|
||||
/// 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 SuspenseContext {
|
||||
/// Create a new boundary for suspense
|
||||
pub fn new(id: ScopeId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
waiting_on: Default::default(),
|
||||
mutations: RefCell::new(Mutations::default()),
|
||||
placeholder: Cell::new(None),
|
||||
created_on_stack: Cell::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SuspenseLeaf {
|
||||
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 {
|
||||
fn wake_by_ref(arc_self: &Rc<Self>) {
|
||||
arc_self.notified.set(true);
|
||||
_ = arc_self
|
||||
.tx
|
||||
.unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use super::{waker::RcWake, Scheduler, SchedulerMsg};
|
||||
use crate::ScopeId;
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
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);
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
/// Start a new future on the same thread as the rest of the VirtualDom.
|
||||
///
|
||||
/// This future will not contribute to suspense resolving, so you should primarily use this for reacting to changes
|
||||
/// and long running tasks.
|
||||
///
|
||||
/// Whenever the component that owns this future is dropped, the future will be dropped as well.
|
||||
///
|
||||
/// Spawning a future onto the root scope will cause it to be dropped when the root component is dropped - which
|
||||
/// will only occur when the VirtuaalDom itself has been dropped.
|
||||
pub fn spawn(&self, scope: ScopeId, task: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
let mut tasks = self.tasks.borrow_mut();
|
||||
let entry = tasks.vacant_entry();
|
||||
let task_id = TaskId(entry.key());
|
||||
|
||||
entry.insert(Rc::new(LocalTask {
|
||||
id: task_id,
|
||||
tx: self.sender.clone(),
|
||||
task: RefCell::new(Box::pin(task)),
|
||||
scope,
|
||||
}));
|
||||
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
||||
.expect("Scheduler should exist");
|
||||
|
||||
task_id
|
||||
}
|
||||
|
||||
/// Drop the future with the given TaskId
|
||||
///
|
||||
/// This does nto abort the task, so you'll want to wrap it in an aborthandle if that's important to you
|
||||
pub fn remove(&self, id: TaskId) {
|
||||
self.tasks.borrow_mut().remove(id.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl RcWake for LocalTask {
|
||||
fn wake_by_ref(arc_self: &Rc<Self>) {
|
||||
_ = arc_self
|
||||
.tx
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use futures_util::FutureExt;
|
||||
use std::{
|
||||
rc::Rc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
innerlude::{Mutation, Mutations, SuspenseContext},
|
||||
nodes::RenderReturn,
|
||||
ScopeId, TaskId, VNode, VirtualDom,
|
||||
};
|
||||
|
||||
use super::{waker::RcWake, SuspenseId};
|
||||
|
||||
impl VirtualDom {
|
||||
/// Handle notifications by tasks inside the scheduler
|
||||
///
|
||||
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
|
||||
/// queue
|
||||
pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) {
|
||||
let mut tasks = self.scheduler.tasks.borrow_mut();
|
||||
|
||||
let task = match tasks.get(id.0) {
|
||||
Some(task) => task,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let waker = task.waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
// If the task completes...
|
||||
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);
|
||||
|
||||
// Remove it from the scheduler
|
||||
tasks.remove(id.0);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
|
||||
self.scopes[id.0]
|
||||
.consume_context::<Rc<SuspenseContext>>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
|
||||
let leaf = self
|
||||
.scheduler
|
||||
.leaves
|
||||
.borrow_mut()
|
||||
.get(id.0)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
let scope_id = leaf.scope_id;
|
||||
|
||||
// todo: cache the waker
|
||||
let waker = leaf.waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
// Safety: the future is always pinned to the bump arena
|
||||
let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) };
|
||||
let as_pinned_mut = &mut pinned;
|
||||
|
||||
// the component finished rendering and gave us nodes
|
||||
// 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) {
|
||||
// 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);
|
||||
|
||||
let scope = &mut self.scopes[scope_id.0];
|
||||
let arena = scope.current_frame();
|
||||
|
||||
let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes));
|
||||
arena.node.set(ret);
|
||||
|
||||
fiber.waiting_on.borrow_mut().remove(&id);
|
||||
|
||||
if let RenderReturn::Sync(Ok(template)) = ret {
|
||||
let mutations_ref = &mut fiber.mutations.borrow_mut();
|
||||
let mutations = &mut **mutations_ref;
|
||||
let template: &VNode = unsafe { std::mem::transmute(template) };
|
||||
let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) };
|
||||
|
||||
std::mem::swap(&mut self.mutations, mutations);
|
||||
|
||||
let place_holder_id = scope.placeholder.get().unwrap();
|
||||
self.scope_stack.push(scope_id);
|
||||
let created = self.create(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() {
|
||||
self.finished_fibers.push(fiber.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use std::task::{RawWaker, RawWakerVTable, Waker};
|
||||
use std::{mem, rc::Rc};
|
||||
|
||||
pub trait RcWake: Sized {
|
||||
/// Create a waker from this self-wakening object
|
||||
fn waker(self: &Rc<Self>) -> Waker {
|
||||
unsafe fn rc_vtable<T: RcWake>() -> &'static RawWakerVTable {
|
||||
&RawWakerVTable::new(
|
||||
|data| {
|
||||
let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
|
||||
let _rc_clone: mem::ManuallyDrop<_> = arc.clone();
|
||||
RawWaker::new(data, rc_vtable::<T>())
|
||||
},
|
||||
|data| Rc::from_raw(data.cast::<T>()).wake(),
|
||||
|data| {
|
||||
let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
|
||||
RcWake::wake_by_ref(&arc);
|
||||
},
|
||||
|data| drop(Rc::<T>::from_raw(data.cast::<T>())),
|
||||
)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
Waker::from_raw(RawWaker::new(
|
||||
Rc::into_raw(self.clone()).cast(),
|
||||
rc_vtable::<Self>(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn wake_by_ref(arc_self: &Rc<Self>);
|
||||
|
||||
fn wake(self: Rc<Self>) {
|
||||
Self::wake_by_ref(&self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
use crate::{
|
||||
any_props::AnyProps,
|
||||
bump_frame::BumpFrame,
|
||||
innerlude::DirtyScope,
|
||||
innerlude::{SuspenseId, SuspenseLeaf},
|
||||
nodes::RenderReturn,
|
||||
scheduler::RcWake,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
virtual_dom::VirtualDom,
|
||||
};
|
||||
use bumpalo::Bump;
|
||||
use futures_util::FutureExt;
|
||||
use std::{
|
||||
mem,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
impl VirtualDom {
|
||||
pub(super) fn new_scope(
|
||||
&mut self,
|
||||
props: Box<dyn AnyProps<'static>>,
|
||||
name: &'static str,
|
||||
) -> &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(Box::new(ScopeState {
|
||||
parent,
|
||||
id,
|
||||
height,
|
||||
name,
|
||||
props: Some(props),
|
||||
tasks: self.scheduler.clone(),
|
||||
placeholder: Default::default(),
|
||||
node_arena_1: BumpFrame::new(0),
|
||||
node_arena_2: BumpFrame::new(0),
|
||||
spawned_tasks: Default::default(),
|
||||
render_cnt: Default::default(),
|
||||
hook_arena: Default::default(),
|
||||
hook_list: Default::default(),
|
||||
hook_idx: Default::default(),
|
||||
shared_contexts: Default::default(),
|
||||
borrowed_props: Default::default(),
|
||||
listeners: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
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() as *mut _))
|
||||
}
|
||||
|
||||
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 = self.scopes[scope_id.0].as_mut();
|
||||
|
||||
// if this frame hasn't been intialized yet, we can guess the size of the next frame to be more efficient
|
||||
if scope.previous_frame().bump.allocated_bytes() == 0 {
|
||||
scope.previous_frame_mut().bump =
|
||||
Bump::with_capacity(scope.current_frame().bump.allocated_bytes());
|
||||
} else {
|
||||
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 = scope.props.as_ref().unwrap().as_ref();
|
||||
let props: &dyn AnyProps = mem::transmute(props);
|
||||
props.render(scope).extend_lifetime()
|
||||
};
|
||||
|
||||
// immediately resolve futures that can be resolved
|
||||
if let RenderReturn::Async(task) = &mut new_nodes {
|
||||
let mut leaves = self.scheduler.leaves.borrow_mut();
|
||||
|
||||
let entry = leaves.vacant_entry();
|
||||
let suspense_id = SuspenseId(entry.key());
|
||||
|
||||
let leaf = Rc::new(SuspenseLeaf {
|
||||
scope_id,
|
||||
task: task.as_mut(),
|
||||
id: suspense_id,
|
||||
tx: self.scheduler.sender.clone(),
|
||||
notified: Default::default(),
|
||||
});
|
||||
|
||||
let waker = leaf.waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
// safety: the task is already pinned in the bump arena
|
||||
let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) };
|
||||
|
||||
// Keep polling until either we get a value or the future is not ready
|
||||
loop {
|
||||
match pinned.poll_unpin(&mut cx) {
|
||||
// If nodes are produced, then set it and we can break
|
||||
Poll::Ready(nodes) => {
|
||||
new_nodes = RenderReturn::Sync(nodes);
|
||||
break;
|
||||
}
|
||||
|
||||
// If no nodes are produced but the future woke up immediately, then try polling it again
|
||||
// This circumvents things like yield_now, but is important is important when rendering
|
||||
// components that are just a stream of immediately ready futures
|
||||
_ if leaf.notified.get() => {
|
||||
leaf.notified.set(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If no nodes are produced, then we need to wait for the future to be woken up
|
||||
// Insert the future into fiber leaves and break
|
||||
_ => {
|
||||
entry.insert(leaf);
|
||||
self.collected_leaves.push(suspense_id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
|
||||
// We write on top of the previous frame and then make it the current by pushing the generation forward
|
||||
let frame = scope.previous_frame();
|
||||
|
||||
// set the new head of the bump frame
|
||||
let allocated = &*frame.bump.alloc(new_nodes);
|
||||
frame.node.set(allocated);
|
||||
|
||||
// And move the render generation forward by one
|
||||
scope.render_cnt.set(scope.render_cnt.get() + 1);
|
||||
|
||||
// remove this scope from dirty scopes
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: scope.height,
|
||||
id: scope.id,
|
||||
});
|
||||
|
||||
// rebind the lifetime now that its stored internally
|
||||
unsafe { mem::transmute(allocated) }
|
||||
}
|
||||
}
|
||||
+333
-792
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
This is a WIP module
|
||||
|
||||
Subtrees allow the virtualdom to split up the mutation stream into smaller chunks which can be directed to different parts of the dom.
|
||||
It's core to implementing multiwindow desktop support, portals, and alternative inline renderers like react-three-fiber.
|
||||
|
||||
The primary idea is to give each renderer a linear element tree managed by Dioxus to maximize performance and minimize memory usage.
|
||||
This can't be done if two renderers need to share the same native tree.
|
||||
With subtrees, we have an entirely different slab of elements
|
||||
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use slab::Slab;
|
||||
|
||||
use crate::{ElementPath, ScopeId};
|
||||
|
||||
/// A collection of elements confined to a single scope under a chunk of the tree
|
||||
///
|
||||
/// All elements in this collection are guaranteed to be in the same scope and share the same numbering
|
||||
///
|
||||
/// This unit can be multithreaded
|
||||
/// Whenever multiple subtrees are present, we can perform **parallel diffing**
|
||||
pub struct Subtree {
|
||||
id: usize,
|
||||
namespace: Cow<'static, str>,
|
||||
root: ScopeId,
|
||||
elements: Slab<ElementPath>,
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
use crate::innerlude::{VNode, VirtualDom};
|
||||
|
||||
/// An iterator that only yields "real" [`Element`]s. IE only Elements that are
|
||||
/// not [`VNode::Component`] or [`VNode::Fragment`], .
|
||||
pub struct ElementIdIterator<'a> {
|
||||
vdom: &'a VirtualDom,
|
||||
|
||||
// Heuristically we should never bleed into 5 completely nested fragments/components
|
||||
// Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane
|
||||
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>,
|
||||
}
|
||||
|
||||
impl<'a> ElementIdIterator<'a> {
|
||||
/// Create a new iterator from the given [`VirtualDom`] and [`VNode`]
|
||||
///
|
||||
/// This will allow you to iterate through all the real childrne of the [`VNode`].
|
||||
pub fn new(vdom: &'a VirtualDom, node: &'a VNode<'a>) -> Self {
|
||||
Self {
|
||||
vdom,
|
||||
stack: smallvec::smallvec![(0, node)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ElementIdIterator<'a> {
|
||||
type Item = &'a VNode<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<&'a VNode<'a>> {
|
||||
let mut should_pop = false;
|
||||
let mut returned_node = None;
|
||||
let mut should_push = None;
|
||||
|
||||
while returned_node.is_none() {
|
||||
if let Some((count, node)) = self.stack.last_mut() {
|
||||
match node {
|
||||
// We can only exit our looping when we get "real" nodes
|
||||
VNode::Element(_) | VNode::Text(_) | VNode::Placeholder(_) => {
|
||||
// We've recursed INTO an element/text
|
||||
// We need to recurse *out* of it and move forward to the next
|
||||
// println!("Found element! Returning it!");
|
||||
should_pop = true;
|
||||
returned_node = Some(&**node);
|
||||
}
|
||||
|
||||
// If we get a fragment we push the next child
|
||||
VNode::Fragment(frag) => {
|
||||
let count = *count as usize;
|
||||
if count >= frag.children.len() {
|
||||
should_pop = true;
|
||||
} else {
|
||||
should_push = Some(&frag.children[count]);
|
||||
}
|
||||
}
|
||||
|
||||
// For components, we load their root and push them onto the stack
|
||||
VNode::Component(sc) => {
|
||||
let scope = self.vdom.get_scope(sc.scope.get().unwrap()).unwrap();
|
||||
// Simply swap the current node on the stack with the root of the component
|
||||
*node = scope.root_node();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there's no more items on the stack, we're done!
|
||||
return None;
|
||||
}
|
||||
|
||||
if should_pop {
|
||||
self.stack.pop();
|
||||
if let Some((id, _)) = self.stack.last_mut() {
|
||||
*id += 1;
|
||||
}
|
||||
should_pop = false;
|
||||
}
|
||||
|
||||
if let Some(push) = should_push {
|
||||
self.stack.push((0, push));
|
||||
should_push = None;
|
||||
}
|
||||
}
|
||||
|
||||
returned_node
|
||||
}
|
||||
}
|
||||
+416
-534
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,2 @@
|
||||
struct_variant_width = 100
|
||||
struct_lit_width = 80
|
||||
@@ -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
|
||||
@@ -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(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) },
|
||||
AssignId { path: &[0], id: ElementId(3) },
|
||||
SetAttribute { name: "class", value: "3", id: ElementId(3), ns: None },
|
||||
SetAttribute { name: "id", value: "3", id: ElementId(3), ns: None },
|
||||
ReplaceWith { id: ElementId(1), 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 }
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use dioxus::core::{ElementId, Mutation::*};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn bool_test() {
|
||||
let mut app = VirtualDom::new(|cx| cx.render(rsx!(div { hidden: false })));
|
||||
assert_eq!(
|
||||
app.rebuild().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
SetBoolAttribute { name: "hidden", value: false, id: ElementId(1,) },
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use dioxus::core::{ElementId, Mutation::*};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_borrowed_state() {
|
||||
let mut dom = VirtualDom::new(Parent);
|
||||
|
||||
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,) },
|
||||
HydrateText { path: &[0,], value: "Hello w1!", id: ElementId(4,) },
|
||||
ReplacePlaceholder { path: &[1,], m: 1 },
|
||||
ReplacePlaceholder { path: &[0,], m: 1 },
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
fn Parent(cx: Scope) -> Element {
|
||||
let w1 = cx.use_hook(|| String::from("w1"));
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
Child { name: w1 }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct ChildProps<'a> {
|
||||
name: &'a str,
|
||||
}
|
||||
|
||||
fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
h1 { "it's nested" }
|
||||
Child2 { name: cx.props.name }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct Grandchild<'a> {
|
||||
name: &'a str,
|
||||
}
|
||||
|
||||
fn Child2<'a>(cx: Scope<'a, Grandchild<'a>>) -> Element {
|
||||
cx.render(rsx!(div { "Hello {cx.props.name}!" }))
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//! we should properly bubble up errors from components
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let raw = match cx.generation() % 2 {
|
||||
0 => "123.123",
|
||||
1 => "123.123.123",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let value = raw.parse::<f32>()?;
|
||||
|
||||
cx.render(rsx! {
|
||||
div { "hello {value}" }
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bubbles_error() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let _edits = dom.rebuild().santize();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use dioxus::core::{ElementId, Mutation::*};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn state_shares() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.provide_context(cx.generation() as i32);
|
||||
|
||||
cx.render(rsx!(child_1 {}))
|
||||
}
|
||||
|
||||
fn child_1(cx: Scope) -> Element {
|
||||
cx.render(rsx!(child_2 {}))
|
||||
}
|
||||
|
||||
fn child_2(cx: Scope) -> Element {
|
||||
let value = cx.consume_context::<i32>().unwrap();
|
||||
cx.render(rsx!("Value is {value}"))
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
CreateTextNode { value: "Value is 0", id: ElementId(1,) },
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
_ = dom.render_immediate();
|
||||
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 1);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
_ = dom.render_immediate();
|
||||
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 2);
|
||||
|
||||
dom.mark_dirty(ScopeId(2));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[SetText { value: "Value is 2", id: ElementId(1,) },]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId(2));
|
||||
let edits = dom.render_immediate();
|
||||
assert_eq!(
|
||||
edits.santize().edits,
|
||||
[SetText { value: "Value is 3", id: ElementId(1,) },]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
#![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::core::Mutation::*;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::ElementId;
|
||||
|
||||
#[test]
|
||||
fn test_original_diff() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
div {
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
// add to root
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
AppendChildren { m: 1, id: ElementId(0) }
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
div {
|
||||
"Hello, world!"
|
||||
div {
|
||||
div {
|
||||
Fragment {
|
||||
"hello"
|
||||
"world"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
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]
|
||||
fn create_list() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
cx.render(rsx! {
|
||||
(0..3).map(|f| rsx!( div { "hello" } ))
|
||||
})
|
||||
});
|
||||
|
||||
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]
|
||||
fn create_simple() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
cx.render(rsx! {
|
||||
div {}
|
||||
div {}
|
||||
div {}
|
||||
div {}
|
||||
})
|
||||
});
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
|
||||
// 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() {
|
||||
let mut dom = VirtualDom::new(|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 _edits = dom.rebuild().santize();
|
||||
|
||||
// todo: test this
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchors() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
cx.render(rsx! {
|
||||
if true {
|
||||
rsx!( div { "hello" } )
|
||||
}
|
||||
if false {
|
||||
rsx!( div { "goodbye" } )
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// note that the template under "false" doesn't show up since it's not loaded
|
||||
let edits = dom.rebuild().santize();
|
||||
|
||||
// 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, id: ElementId(0) }
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// use dioxus::core::Mutation::*;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn multiroot() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
cx.render(rsx! {
|
||||
div { "Hello a" }
|
||||
div { "Hello b" }
|
||||
div { "Hello c" }
|
||||
})
|
||||
});
|
||||
|
||||
// note: we dont test template edits anymore
|
||||
let _templates = dom.rebuild().santize().templates;
|
||||
|
||||
// assert_eq!(
|
||||
// dom.rebuild().santize().templates,
|
||||
// [
|
||||
// CreateElement { name: "div" },
|
||||
// CreateStaticText { value: "Hello a" },
|
||||
// AppendChildren { m: 1 },
|
||||
// CreateElement { name: "div" },
|
||||
// CreateStaticText { value: "Hello b" },
|
||||
// AppendChildren { m: 1 },
|
||||
// CreateElement { name: "div" },
|
||||
// CreateStaticText { value: "Hello c" },
|
||||
// AppendChildren { m: 1 },
|
||||
// SaveTemplate { name: "template", m: 3 }
|
||||
// ]
|
||||
// )
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//! Do we create fragments properly across complex boundaries?
|
||||
|
||||
use dioxus::core::Mutation::*;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::ElementId;
|
||||
|
||||
#[test]
|
||||
fn empty_fragment_creates_nothing() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx!(()))
|
||||
}
|
||||
|
||||
let mut vdom = VirtualDom::new(app);
|
||||
let edits = vdom.rebuild();
|
||||
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(1) },
|
||||
AppendChildren { id: ElementId(0), m: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_fragments_work() {
|
||||
let mut vdom = VirtualDom::new(|cx| {
|
||||
cx.render(rsx!(
|
||||
div { "hello" }
|
||||
div { "goodbye" }
|
||||
))
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
vdom.rebuild().edits.last().unwrap(),
|
||||
&AppendChildren { id: ElementId(0), m: 2 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragments_nested() {
|
||||
let mut vdom = VirtualDom::new(|cx| {
|
||||
cx.render(rsx!(
|
||||
div { "hello" }
|
||||
div { "goodbye" }
|
||||
rsx! {
|
||||
div { "hello" }
|
||||
div { "goodbye" }
|
||||
rsx! {
|
||||
div { "hello" }
|
||||
div { "goodbye" }
|
||||
rsx! {
|
||||
div { "hello" }
|
||||
div { "goodbye" }
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
vdom.rebuild().edits.last().unwrap(),
|
||||
&AppendChildren { id: ElementId(0), m: 8 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragments_across_components() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
demo_child {}
|
||||
demo_child {}
|
||||
demo_child {}
|
||||
demo_child {}
|
||||
})
|
||||
}
|
||||
|
||||
fn demo_child(cx: Scope) -> Element {
|
||||
let world = "world";
|
||||
cx.render(rsx! {
|
||||
"hellO!"
|
||||
world
|
||||
})
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
VirtualDom::new(app).rebuild().edits.last().unwrap(),
|
||||
&AppendChildren { id: ElementId(0), m: 8 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_fragments() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx!(
|
||||
h1 {"hello"}
|
||||
(0..6).map(|f| rsx!( span { "{f}" }))
|
||||
))
|
||||
}
|
||||
assert_eq!(
|
||||
VirtualDom::new(app).rebuild().edits.last().unwrap(),
|
||||
&AppendChildren { id: ElementId(0), m: 7 }
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use dioxus::core::Mutation::*;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::ElementId;
|
||||
|
||||
// A real-world usecase of templates at peak performance
|
||||
// In react, this would be a lot of node creation.
|
||||
//
|
||||
// In Dioxus, we memoize the rsx! body and simplify it down to a few template loads
|
||||
//
|
||||
// Also note that the IDs increase linearly. This lets us drive a vec on the renderer for O(1) re-indexing
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
(0..3).map(|i| rsx! {
|
||||
div {
|
||||
h1 { "hello world! "}
|
||||
p { "{i}" }
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_renders() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
|
||||
// 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,
|
||||
[
|
||||
// Load the outer div
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
// Load each template one-by-one, rehydrating it
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
HydrateText { path: &[1, 0], value: "0", id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||
HydrateText { path: &[1, 0], value: "1", id: ElementId(5) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||
HydrateText { path: &[1, 0], value: "2", id: ElementId(7) },
|
||||
// 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, id: ElementId(0) }
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use dioxus::core::Mutation::*;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::ElementId;
|
||||
|
||||
/// Should push the text node onto the stack and modify it
|
||||
#[test]
|
||||
fn nested_passthru_creates() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
pass_thru {
|
||||
pass_thru {
|
||||
pass_thru {
|
||||
div { "hi" }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn pass_thru<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
|
||||
cx.render(rsx!(children))
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let edits = dom.rebuild().santize();
|
||||
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/// Should load all the templates and append them
|
||||
///
|
||||
/// Take note on how we don't spit out the template for child_comp since it's entirely dynamic
|
||||
#[test]
|
||||
fn nested_passthru_creates_add() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
child_comp {
|
||||
"1"
|
||||
child_comp {
|
||||
"2"
|
||||
child_comp {
|
||||
"3"
|
||||
div {
|
||||
"hi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn child_comp<'a>(cx: Scope, children: Element<'a>) -> Element {
|
||||
cx.render(rsx! { children })
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
// load 1
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
// load 2
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
// load 3
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
// load div that contains 4
|
||||
LoadTemplate { name: "template", index: 1, id: ElementId(4) },
|
||||
AppendChildren { id: ElementId(0), m: 4 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// note that the template is all dynamic roots - so it doesn't actually get cached as a template
|
||||
#[test]
|
||||
fn dynamic_node_as_root() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let a = 123;
|
||||
let b = 456;
|
||||
cx.render(rsx! { "{a}" "{b}" })
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let edits = dom.rebuild().santize();
|
||||
|
||||
// Since the roots were all dynamic, they should not cause any template muations
|
||||
assert!(edits.templates.is_empty());
|
||||
|
||||
// The root node is text, so we just create it on the spot
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
CreateTextNode { value: "123", id: ElementId(1) },
|
||||
CreateTextNode { value: "456", id: ElementId(2) },
|
||||
AppendChildren { id: ElementId(0), m: 2 }
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use dioxus::core::{ElementId, Mutation::*};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// As we clean up old templates, the ID for the node should cycle
|
||||
#[test]
|
||||
fn cycling_elements() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
cx.render(match cx.generation() % 2 {
|
||||
0 => rsx! { div { "wasd" } },
|
||||
1 => rsx! { div { "abcd" } },
|
||||
_ => unreachable!(),
|
||||
})
|
||||
});
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
// notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use dioxus::core::{ElementId, Mutation::*};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// When returning sets of components, we do a light diff of the contents to preserve some react-like functionality
|
||||
///
|
||||
/// This means that nav_bar should never get re-created and that we should only be swapping out
|
||||
/// different pointers
|
||||
#[test]
|
||||
fn component_swap() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let render_phase = cx.use_hook(|| 0);
|
||||
|
||||
*render_phase += 1;
|
||||
|
||||
cx.render(match *render_phase {
|
||||
0 => rsx! {
|
||||
nav_bar {}
|
||||
dash_board {}
|
||||
},
|
||||
1 => rsx! {
|
||||
nav_bar {}
|
||||
dash_results {}
|
||||
},
|
||||
2 => rsx! {
|
||||
nav_bar {}
|
||||
dash_board {}
|
||||
},
|
||||
3 => rsx! {
|
||||
nav_bar {}
|
||||
dash_results {}
|
||||
},
|
||||
4 => rsx! {
|
||||
nav_bar {}
|
||||
dash_board {}
|
||||
},
|
||||
_ => rsx!("blah"),
|
||||
})
|
||||
}
|
||||
|
||||
fn nav_bar(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
h1 {
|
||||
"NavBar"
|
||||
(0..3).map(|_| rsx!(nav_link {}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn nav_link(cx: Scope) -> Element {
|
||||
cx.render(rsx!( h1 { "nav_link" } ))
|
||||
}
|
||||
|
||||
fn dash_board(cx: Scope) -> Element {
|
||||
cx.render(rsx!( div { "dashboard" } ))
|
||||
}
|
||||
|
||||
fn dash_results(cx: Scope) -> Element {
|
||||
cx.render(rsx!( div { "results" } ))
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let edits = dom.rebuild().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||
ReplacePlaceholder { path: &[1], m: 3 },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||
AppendChildren { m: 2, id: ElementId(0) }
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||
ReplaceWith { id: ElementId(5), m: 1 }
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||
ReplaceWith { id: ElementId(6), m: 1 }
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||
ReplaceWith { id: ElementId(5), m: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use dioxus::core::Mutation::*;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::ElementId;
|
||||
|
||||
#[test]
|
||||
fn text_diff() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let gen = cx.generation();
|
||||
cx.render(rsx!( h1 { "hello {gen}" } ))
|
||||
}
|
||||
|
||||
let mut vdom = VirtualDom::new(app);
|
||||
_ = vdom.rebuild();
|
||||
|
||||
vdom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
vdom.render_immediate().edits,
|
||||
[SetText { value: "hello 1", id: ElementId(2) }]
|
||||
);
|
||||
|
||||
vdom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
vdom.render_immediate().edits,
|
||||
[SetText { value: "hello 2", id: ElementId(2) }]
|
||||
);
|
||||
|
||||
vdom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
vdom.render_immediate().edits,
|
||||
[SetText { value: "hello 3", id: ElementId(2) }]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn element_swap() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let gen = cx.generation();
|
||||
|
||||
match gen % 2 {
|
||||
0 => cx.render(rsx!( h1 { "hello 1" } )),
|
||||
1 => cx.render(rsx!( h2 { "hello 2" } )),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
let mut vdom = VirtualDom::new(app);
|
||||
_ = vdom.rebuild();
|
||||
|
||||
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(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(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(ScopeId(0));
|
||||
assert_eq!(
|
||||
vdom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
//! Diffing Tests
|
||||
//!
|
||||
//! These tests only verify that the diffing algorithm works properly for single components.
|
||||
//!
|
||||
//! It does not validated that component lifecycles work properly. This is done in another test file.
|
||||
|
||||
use dioxus::core::{ElementId, Mutation::*};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// Should result in moves, but not removals or additions
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order = match cx.generation() % 2 {
|
||||
0 => &[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9],
|
||||
1 => &[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(8,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(10,) },
|
||||
AppendChildren { m: 10, id: ElementId(0) },
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().edits,
|
||||
[
|
||||
PushRoot { id: ElementId(7,) },
|
||||
InsertBefore { id: ElementId(5,), m: 1 },
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/// Should result in moves only
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order = match cx.generation() % 2 {
|
||||
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
|
||||
1 => &[/**/ 8, 7, 4, 5, 6 /**/],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().edits,
|
||||
[
|
||||
PushRoot { id: ElementId(5,) },
|
||||
PushRoot { id: ElementId(4,) },
|
||||
InsertBefore { id: ElementId(1,), m: 2 },
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/// Should result in moves only
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_3() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order = match cx.generation() % 2 {
|
||||
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
|
||||
1 => &[/**/ 4, 8, 7, 5, 6 /**/],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().edits,
|
||||
[
|
||||
PushRoot { id: ElementId(5,) },
|
||||
PushRoot { id: ElementId(4,) },
|
||||
InsertBefore { id: ElementId(2,), m: 2 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_4() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order = match cx.generation() % 2 {
|
||||
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
|
||||
1 => &[/**/ 4, 5, 8, 7, 6 /**/],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().edits,
|
||||
[
|
||||
PushRoot { id: ElementId(5,) },
|
||||
PushRoot { id: ElementId(4,) },
|
||||
InsertBefore { id: ElementId(3,), m: 2 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_5() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order = match cx.generation() % 2 {
|
||||
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
|
||||
1 => &[/**/ 4, 5, 6, 8, 7 /**/],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().edits,
|
||||
[
|
||||
PushRoot { id: ElementId(5,) },
|
||||
InsertBefore { id: ElementId(4,), m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_additions() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order: &[_] = match cx.generation() % 2 {
|
||||
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
|
||||
1 => &[/**/ 4, 5, 6, 7, 8, 9, 10 /**/],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
|
||||
InsertAfter { id: ElementId(5), m: 2 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyed_diffing_additions_and_moves_on_ends() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order: &[_] = match cx.generation() % 2 {
|
||||
0 => &[/**/ 4, 5, 6, 7 /**/],
|
||||
1 => &[/**/ 7, 4, 5, 6, 11, 12 /**/],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
// create 11, 12
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||
InsertAfter { id: ElementId(3), m: 2 },
|
||||
// move 7 to the front
|
||||
PushRoot { id: ElementId(4) },
|
||||
InsertBefore { id: ElementId(1), m: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyed_diffing_additions_and_moves_in_middle() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order: &[_] = match cx.generation() % 2 {
|
||||
0 => &[/**/ 1, 2, 3, 4 /**/],
|
||||
1 => &[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
// LIS: 4, 5, 6
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
// create 5, 6
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||
InsertBefore { id: ElementId(3), m: 2 },
|
||||
// create 7, 8
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(8) },
|
||||
InsertBefore { id: ElementId(2), m: 2 },
|
||||
// move 7
|
||||
PushRoot { id: ElementId(4) },
|
||||
InsertBefore { id: ElementId(1), m: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controlled_keyed_diffing_out_of_order() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order: &[_] = match cx.generation() % 2 {
|
||||
0 => &[4, 5, 6, 7],
|
||||
1 => &[0, 5, 9, 6, 4],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
// LIS: 5, 6
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
// remove 7
|
||||
Remove { id: ElementId(4,) },
|
||||
// move 4 to after 6
|
||||
PushRoot { id: ElementId(1) },
|
||||
InsertAfter { id: ElementId(3,), m: 1 },
|
||||
// create 9 and insert before 6
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||
InsertBefore { id: ElementId(3,), m: 1 },
|
||||
// create 0 and insert before 5
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||
InsertBefore { id: ElementId(2,), m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controlled_keyed_diffing_out_of_order_max_test() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order: &[_] = match cx.generation() % 2 {
|
||||
0 => &[0, 1, 2, 3, 4],
|
||||
1 => &[3, 0, 1, 10, 2],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
Remove { id: ElementId(5,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||
InsertBefore { id: ElementId(3,), m: 1 },
|
||||
PushRoot { id: ElementId(4) },
|
||||
InsertBefore { id: ElementId(1,), m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// noticed some weird behavior in the desktop interpreter
|
||||
// just making sure it doesnt happen in the core implementation
|
||||
#[test]
|
||||
fn remove_list() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let order: &[_] = match cx.generation() % 2 {
|
||||
0 => &[9, 8, 7, 6, 5],
|
||||
1 => &[9, 8],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
Remove { id: ElementId(5) },
|
||||
Remove { id: ElementId(4) },
|
||||
Remove { id: ElementId(3) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[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(3) },
|
||||
Remove { id: ElementId(2) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||
ReplaceWith { id: ElementId(1), m: 3 }
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
use dioxus::core::{ElementId, Mutation::*};
|
||||
use dioxus::prelude::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn list_creates_one_by_one() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let gen = cx.generation();
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
(0..gen).map(|i| rsx! {
|
||||
div { "{i}" }
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// load the div and then assign the empty fragment as a placeholder
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
AssignId { path: &[0], id: ElementId(2,) },
|
||||
AppendChildren { id: ElementId(0), m: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
// Rendering the first item should replace the placeholder with an element
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3,) },
|
||||
HydrateText { path: &[0], value: "0", id: ElementId(4,) },
|
||||
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
// Rendering the next item should insert after the previous
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
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(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
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(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(8,) },
|
||||
HydrateText { path: &[0], value: "3", id: ElementId(9,) },
|
||||
InsertAfter { id: ElementId(6,), m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_one_by_one() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let gen = 3 - cx.generation() % 4;
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
(0..gen).map(|i| rsx! {
|
||||
div { "{i}" }
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// load the div and then assign the empty fragment as a placeholder
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
// The container
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
// each list item
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
HydrateText { path: &[0], value: "0", id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(5) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||
HydrateText { path: &[0], value: "2", id: ElementId(7) },
|
||||
// replace the placeholder in the template with the 3 templates on the stack
|
||||
ReplacePlaceholder { m: 3, path: &[0] },
|
||||
// Mount the div
|
||||
AppendChildren { id: ElementId(0), m: 1 }
|
||||
]
|
||||
);
|
||||
|
||||
// Remove div(3)
|
||||
// Rendering the first item should replace the placeholder with an element
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[Remove { id: ElementId(6) }]
|
||||
);
|
||||
|
||||
// Remove div(2)
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[Remove { id: ElementId(4) }]
|
||||
);
|
||||
|
||||
// Remove div(1) and replace with a placeholder
|
||||
// todo: this should just be a remove with no placeholder
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(4) },
|
||||
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(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
HydrateText { path: &[0], value: "0", id: ElementId(3) },
|
||||
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(4), m: 3 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_shrink_multiroot() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
(0..cx.generation()).map(|i| rsx! {
|
||||
div { "{i}" }
|
||||
div { "{i}" }
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
AssignId { path: &[0,], id: ElementId(2,) },
|
||||
AppendChildren { id: ElementId(0), m: 1 }
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
HydrateText { path: &[0], value: "0", id: ElementId(4) },
|
||||
LoadTemplate { name: "template", index: 1, id: ElementId(5) },
|
||||
HydrateText { path: &[0], value: "0", id: ElementId(6) },
|
||||
ReplaceWith { id: ElementId(2), m: 2 }
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
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(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
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 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_one_by_one_multiroot() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
let gen = 3 - cx.generation() % 4;
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
(0..gen).map(|i| rsx! {
|
||||
div { "{i}" }
|
||||
div { "{i}" }
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// load the div and then assign the empty fragment as a placeholder
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
//
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
HydrateText { path: &[0], value: "0", id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 1, id: ElementId(4) },
|
||||
HydrateText { path: &[0], value: "0", id: ElementId(5) },
|
||||
//
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(7) },
|
||||
LoadTemplate { name: "template", index: 1, id: ElementId(8) },
|
||||
HydrateText { path: &[0], value: "1", id: ElementId(9) },
|
||||
//
|
||||
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) },
|
||||
//
|
||||
ReplacePlaceholder { path: &[0], m: 6 },
|
||||
//
|
||||
AppendChildren { id: ElementId(0), m: 1 }
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[Remove { id: ElementId(10) }, Remove { id: ElementId(12) }]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[Remove { id: ElementId(6) }, Remove { id: ElementId(8) }]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(8) },
|
||||
Remove { id: ElementId(2) },
|
||||
ReplaceWith { id: ElementId(4), 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,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(11,) },
|
||||
Remove { id: ElementId(9,) },
|
||||
Remove { id: ElementId(7,) },
|
||||
Remove { id: ElementId(5,) },
|
||||
Remove { id: ElementId(1,) },
|
||||
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(3,) },
|
||||
ReplaceWith { id: ElementId(11,), m: 1 },
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
//! It should be possible to swap out templates at runtime, enabling hotreloading
|
||||
@@ -0,0 +1,42 @@
|
||||
use dioxus::core::{ElementId, Mutation};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn basic_syntax_is_a_template(cx: Scope) -> Element {
|
||||
let asd = 123;
|
||||
let var = 123;
|
||||
|
||||
cx.render(rsx! {
|
||||
div { key: "12345",
|
||||
class: "asd",
|
||||
class: "{asd}",
|
||||
onclick: move |_| {},
|
||||
div { "{var}" }
|
||||
div {
|
||||
h1 { "var" }
|
||||
p { "you're great!" }
|
||||
div { background_color: "red",
|
||||
h1 { "var" }
|
||||
div { b { "asd" } "not great" }
|
||||
}
|
||||
p { "you're great!" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
#[test]
|
||||
fn dual_stream() {
|
||||
let mut dom = VirtualDom::new(basic_syntax_is_a_template);
|
||||
let edits = dom.rebuild().santize();
|
||||
|
||||
use Mutation::*;
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None },
|
||||
NewEventListener { name: "click", id: ElementId(1) },
|
||||
HydrateText { path: &[0, 0], value: "123", id: ElementId(2) },
|
||||
AppendChildren { id: ElementId(0), m: 1 }
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
#![allow(unused, non_upper_case_globals)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
//! Tests for the lifecycle of components.
|
||||
use dioxus::core::{ElementId, Mutation::*};
|
||||
use dioxus::prelude::*;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
type Shared<T> = Arc<Mutex<T>>;
|
||||
|
||||
#[test]
|
||||
fn manual_diffing() {
|
||||
struct AppProps {
|
||||
value: Shared<&'static str>,
|
||||
}
|
||||
|
||||
fn app(cx: Scope<AppProps>) -> Element {
|
||||
let val = cx.props.value.lock().unwrap();
|
||||
cx.render(rsx! { div { "{val}" } })
|
||||
};
|
||||
|
||||
let value = Arc::new(Mutex::new("Hello"));
|
||||
let mut dom = VirtualDom::new_with_props(app, AppProps { value: value.clone() });
|
||||
|
||||
let _ = dom.rebuild();
|
||||
|
||||
*value.lock().unwrap() = "goodbye";
|
||||
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
HydrateText { path: &[0], value: "goodbye", id: ElementId(4) },
|
||||
AppendChildren { m: 1, id: ElementId(0) }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn events_generate() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let count = cx.use_hook(|| 0);
|
||||
|
||||
match *count {
|
||||
0 => cx.render(rsx! {
|
||||
div { onclick: move |_| *count += 1,
|
||||
div { "nested" }
|
||||
"Click me!"
|
||||
}
|
||||
}),
|
||||
_ => cx.render(rsx!(())),
|
||||
}
|
||||
};
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
_ = dom.rebuild();
|
||||
|
||||
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
let edits = dom.render_immediate();
|
||||
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(2) },
|
||||
ReplaceWith { id: ElementId(1), m: 1 }
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn components_generate() {
|
||||
// fn app(cx: Scope) -> Element {
|
||||
// let render_phase = cx.use_hook(|| 0);
|
||||
// *render_phase += 1;
|
||||
|
||||
// cx.render(match *render_phase {
|
||||
// 1 => rsx_without_templates!("Text0"),
|
||||
// 2 => rsx_without_templates!(div {}),
|
||||
// 3 => rsx_without_templates!("Text2"),
|
||||
// 4 => rsx_without_templates!(Child {}),
|
||||
// 5 => rsx_without_templates!({ None as Option<()> }),
|
||||
// 6 => rsx_without_templates!("text 3"),
|
||||
// 7 => rsx_without_templates!({ (0..2).map(|f| rsx_without_templates!("text {f}")) }),
|
||||
// 8 => rsx_without_templates!(Child {}),
|
||||
// _ => todo!(),
|
||||
// })
|
||||
// };
|
||||
|
||||
// fn Child(cx: Scope) -> Element {
|
||||
// println!("Running child");
|
||||
// cx.render(rsx_without_templates! {
|
||||
// h1 {}
|
||||
// })
|
||||
// }
|
||||
|
||||
// let mut dom = VirtualDom::new(app);
|
||||
// let edits = dom.rebuild();
|
||||
// assert_eq!(
|
||||
// edits.edits,
|
||||
// [
|
||||
// CreateTextNode { root: Some(1), text: "Text0" },
|
||||
// AppendChildren { root: Some(0), children: vec![1] }
|
||||
// ]
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// dom.hard_diff(ScopeId(0)).edits,
|
||||
// [
|
||||
// CreateElement { root: Some(2), tag: "div", children: 0 },
|
||||
// ReplaceWith { root: Some(1), nodes: vec![2] }
|
||||
// ]
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// dom.hard_diff(ScopeId(0)).edits,
|
||||
// [
|
||||
// CreateTextNode { root: Some(1), text: "Text2" },
|
||||
// ReplaceWith { root: Some(2), nodes: vec![1] }
|
||||
// ]
|
||||
// );
|
||||
|
||||
// // child {}
|
||||
// assert_eq!(
|
||||
// dom.hard_diff(ScopeId(0)).edits,
|
||||
// [
|
||||
// CreateElement { root: Some(2), tag: "h1", children: 0 },
|
||||
// ReplaceWith { root: Some(1), nodes: vec![2] }
|
||||
// ]
|
||||
// );
|
||||
|
||||
// // placeholder
|
||||
// assert_eq!(
|
||||
// dom.hard_diff(ScopeId(0)).edits,
|
||||
// [
|
||||
// CreatePlaceholder { root: Some(1) },
|
||||
// ReplaceWith { root: Some(2), nodes: vec![1] }
|
||||
// ]
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// dom.hard_diff(ScopeId(0)).edits,
|
||||
// [
|
||||
// CreateTextNode { root: Some(2), text: "text 3" },
|
||||
// ReplaceWith { root: Some(1), nodes: vec![2] }
|
||||
// ]
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// dom.hard_diff(ScopeId(0)).edits,
|
||||
// [
|
||||
// CreateTextNode { text: "text 0", root: Some(1) },
|
||||
// CreateTextNode { text: "text 1", root: Some(3) },
|
||||
// ReplaceWith { root: Some(2), nodes: vec![1, 3] },
|
||||
// ]
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// dom.hard_diff(ScopeId(0)).edits,
|
||||
// [
|
||||
// CreateElement { tag: "h1", root: Some(2), children: 0 },
|
||||
// ReplaceWith { root: Some(1), nodes: vec![2] },
|
||||
// Remove { root: Some(3) },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
@@ -0,0 +1,49 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::ElementId;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[test]
|
||||
fn miri_rollover() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
for _ 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 = |_| 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_example { i: i, onhover: onhover }
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn child_example<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
|
||||
cx.render(rsx! {
|
||||
li {
|
||||
onmouseover: move |e| onhover.call(e),
|
||||
"{i}"
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn app_drops() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {}
|
||||
})
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hooks_drop() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.use_hook(|| String::from("asd"));
|
||||
cx.use_hook(|| String::from("asd"));
|
||||
cx.use_hook(|| String::from("asd"));
|
||||
cx.use_hook(|| String::from("asd"));
|
||||
|
||||
cx.render(rsx! {
|
||||
div {}
|
||||
})
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contexts_drop() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.provide_context(String::from("asd"));
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
child_comp {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn child_comp(cx: Scope) -> Element {
|
||||
let el = cx.consume_context::<String>().unwrap();
|
||||
|
||||
cx.render(rsx! {
|
||||
div { "hello {el}" }
|
||||
})
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tasks_drop() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.spawn(async {
|
||||
// tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
div { }
|
||||
})
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_props_drop() {
|
||||
struct RootProps(String);
|
||||
|
||||
let mut dom = VirtualDom::new_with_props(
|
||||
|cx| cx.render(rsx!( div { "{cx.props.0}" } )),
|
||||
RootProps("asdasd".to_string()),
|
||||
);
|
||||
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diffing_drops_old() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
match cx.generation() % 2 {
|
||||
0 => rsx!( child_comp1 { name: "asdasd".to_string() }),
|
||||
1 => rsx!( child_comp2 { name: "asdasd".to_string() }),
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn child_comp1(cx: Scope, name: String) -> Element {
|
||||
cx.render(rsx! { "Hello {name}" })
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn child_comp2(cx: Scope, name: String) -> Element {
|
||||
cx.render(rsx! { "Goodbye {name}" })
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
#![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 {});
|
||||
|
||||
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);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,24 @@
|
||||
//! Tests related to safety of the library.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::SuspenseContext;
|
||||
|
||||
/// Ensure no issues with not calling rebuild
|
||||
#[test]
|
||||
fn root_node_isnt_null() {
|
||||
let dom = VirtualDom::new(|cx| render!("Hello world!"));
|
||||
|
||||
let scope = dom.base_scope();
|
||||
|
||||
// We haven't built the tree, so trying to get out the root node should fail
|
||||
assert!(scope.try_root_node().is_none());
|
||||
|
||||
// The height should be 0
|
||||
assert_eq!(scope.height(), 0);
|
||||
|
||||
// There should be a default suspense context
|
||||
// todo: there should also be a default error boundary
|
||||
assert!(scope.has_context::<Rc<SuspenseContext>>().is_some());
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use dioxus::core::ElementId;
|
||||
use dioxus::core::{Mutation::*, SuspenseContext};
|
||||
use dioxus::prelude::*;
|
||||
use std::future::IntoFuture;
|
||||
use std::rc::Rc;
|
||||
use std::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::<Rc<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;
|
||||
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}" } ))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
static mut POLL_COUNT: usize = 0;
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_works() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let _ = dom.rebuild();
|
||||
|
||||
tokio::select! {
|
||||
_ = dom.wait_for_work() => {}
|
||||
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
|
||||
};
|
||||
|
||||
// By the time the tasks are finished, we should've accumulated ticks from two tasks
|
||||
// Be warned that by setting the delay to too short, tokio might not schedule in the tasks
|
||||
assert_eq!(unsafe { POLL_COUNT }, 135);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.use_hook(|| {
|
||||
cx.spawn(async {
|
||||
for x in 0..10 {
|
||||
tokio::time::sleep(Duration::from_micros(50)).await;
|
||||
unsafe { POLL_COUNT += x }
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(async {
|
||||
for x in 0..10 {
|
||||
tokio::time::sleep(Duration::from_micros(25)).await;
|
||||
unsafe { POLL_COUNT += x * 2 }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
cx.render(rsx!(()))
|
||||
}
|
||||
@@ -15,36 +15,41 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1" }
|
||||
dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true }
|
||||
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
thiserror = "1.0.30"
|
||||
log = "0.4.14"
|
||||
wry = { version = "0.20.2" }
|
||||
wry = { version = "0.23.4" }
|
||||
futures-channel = "0.3.21"
|
||||
tokio = { version = "1.16.1", features = [
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"rt",
|
||||
"time",
|
||||
"macros",
|
||||
], optional = true, default-features = false }
|
||||
webbrowser = "0.7.1"
|
||||
infer = "0.9.0"
|
||||
webbrowser = "0.8.0"
|
||||
infer = "0.11.0"
|
||||
dunce = "1.0.2"
|
||||
|
||||
interprocess = { version = "1.1.1", optional = true }
|
||||
interprocess = { version = "1.1.1", optional = true}
|
||||
futures-util = "0.3.25"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
objc_id = "0.1.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9.3"
|
||||
|
||||
[features]
|
||||
default = ["tokio_runtime"]
|
||||
default = ["tokio_runtime", "hot-reload"]
|
||||
tokio_runtime = ["tokio"]
|
||||
fullscreen = ["wry/fullscreen"]
|
||||
transparent = ["wry/transparent"]
|
||||
tray = ["wry/tray"]
|
||||
hot-reload = ["dioxus-rsx-interpreter", "interprocess"]
|
||||
hot-reload = ["interprocess"]
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
|
||||
@@ -21,13 +21,14 @@ pub struct Config {
|
||||
pub(crate) resource_dir: Option<PathBuf>,
|
||||
pub(crate) custom_head: Option<String>,
|
||||
pub(crate) custom_index: Option<String>,
|
||||
pub(crate) root_name: String,
|
||||
}
|
||||
|
||||
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
||||
|
||||
pub(crate) type WryProtocol = (
|
||||
String,
|
||||
Box<dyn Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static>,
|
||||
Box<dyn Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Vec<u8>>> + 'static>,
|
||||
);
|
||||
|
||||
impl Config {
|
||||
@@ -46,6 +47,7 @@ impl Config {
|
||||
resource_dir: None,
|
||||
custom_head: None,
|
||||
custom_index: None,
|
||||
root_name: "main".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +98,7 @@ impl Config {
|
||||
/// Set a custom protocol
|
||||
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static,
|
||||
F: Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Vec<u8>>> + 'static,
|
||||
{
|
||||
self.protocols.push((name, Box::new(handler)));
|
||||
self
|
||||
@@ -126,6 +128,14 @@ impl Config {
|
||||
self.custom_index = Some(index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name of the element that Dioxus will use as the root.
|
||||
///
|
||||
/// This is akint to calling React.render() on the element with the specified name.
|
||||
pub fn with_root_name(mut self, name: impl Into<String>) -> Self {
|
||||
self.root_name = name.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
||||
|
||||
use crate::events::{decode_event, EventMessage};
|
||||
use dioxus_core::*;
|
||||
use futures_channel::mpsc::{unbounded, UnboundedSender};
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(target_os = "ios")]
|
||||
use objc::runtime::Object;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::Arc,
|
||||
sync::{atomic::AtomicBool, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use wry::{
|
||||
self,
|
||||
@@ -14,10 +20,15 @@ use wry::{
|
||||
|
||||
pub(super) struct DesktopController {
|
||||
pub(super) webviews: HashMap<WindowId, WebView>,
|
||||
pub(super) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender<Value>,
|
||||
pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
|
||||
pub(super) quit_app_on_close: bool,
|
||||
pub(super) is_ready: Arc<AtomicBool>,
|
||||
pub(super) proxy: EventLoopProxy<UserWindowEvent>,
|
||||
pub(super) event_tx: UnboundedSender<serde_json::Value>,
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(super) views: Vec<*mut Object>,
|
||||
}
|
||||
|
||||
impl DesktopController {
|
||||
@@ -29,64 +40,65 @@ impl DesktopController {
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> Self {
|
||||
let edit_queue = Arc::new(Mutex::new(Vec::new()));
|
||||
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
|
||||
let (event_tx, mut event_rx) = unbounded();
|
||||
let proxy2 = proxy.clone();
|
||||
|
||||
let pending_edits = edit_queue.clone();
|
||||
let return_sender = sender.clone();
|
||||
let desktop_context_proxy = proxy.clone();
|
||||
let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::<Value>();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
|
||||
// We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
|
||||
// I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
runtime.block_on(async move {
|
||||
let mut dom =
|
||||
VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver));
|
||||
|
||||
let window_context = DesktopContext::new(desktop_context_proxy);
|
||||
|
||||
dom.base_scope().provide_context(window_context);
|
||||
|
||||
// allow other proccesses to send the new rsx text to the @dioxusin ipc channel and recieve erros on the @dioxusout channel
|
||||
#[cfg(feature = "hot-reload")]
|
||||
crate::hot_reload::init(&dom);
|
||||
|
||||
let edits = dom.rebuild();
|
||||
|
||||
edit_queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(serde_json::to_string(&edits.edits).unwrap());
|
||||
|
||||
// Make sure the window is ready for any new updates
|
||||
proxy.send_event(UserWindowEvent::Update).unwrap();
|
||||
let mut dom = VirtualDom::new_with_props(root, props)
|
||||
.with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever));
|
||||
{
|
||||
let edits = dom.rebuild();
|
||||
let mut queue = edit_queue.lock().unwrap();
|
||||
queue.push(serde_json::to_string(&edits).unwrap());
|
||||
proxy.send_event(UserWindowEvent::EditsReady).unwrap();
|
||||
}
|
||||
|
||||
loop {
|
||||
dom.wait_for_work().await;
|
||||
|
||||
let muts = dom.work_with_deadline(|| false);
|
||||
|
||||
for edit in muts {
|
||||
edit_queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(serde_json::to_string(&edit.edits).unwrap());
|
||||
tokio::select! {
|
||||
_ = dom.wait_for_work() => {}
|
||||
Some(json_value) = event_rx.next() => {
|
||||
if let Ok(value) = serde_json::from_value::<EventMessage>(json_value) {
|
||||
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, dioxus_html::events::event_bubbles(&name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = proxy.send_event(UserWindowEvent::Update);
|
||||
let muts = dom
|
||||
.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)))
|
||||
.await;
|
||||
|
||||
edit_queue.lock().unwrap().push(serde_json::to_string(&muts).unwrap());
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
pending_edits,
|
||||
sender: return_sender,
|
||||
eval_sender,
|
||||
webviews: HashMap::new(),
|
||||
is_ready: Arc::new(AtomicBool::new(false)),
|
||||
quit_app_on_close: true,
|
||||
proxy: proxy2,
|
||||
event_tx,
|
||||
#[cfg(target_os = "ios")]
|
||||
views: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,13 +112,23 @@ impl DesktopController {
|
||||
|
||||
pub(super) fn try_load_ready_webviews(&mut self) {
|
||||
if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let mut queue = self.pending_edits.lock().unwrap();
|
||||
let mut new_queue = Vec::new();
|
||||
|
||||
{
|
||||
let mut queue = self.pending_edits.lock().unwrap();
|
||||
std::mem::swap(&mut new_queue, &mut *queue);
|
||||
}
|
||||
|
||||
let (_id, view) = self.webviews.iter_mut().next().unwrap();
|
||||
|
||||
for edit in queue.drain(..) {
|
||||
for edit in new_queue.drain(..) {
|
||||
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_template(&self, _serialized_template: String) {
|
||||
todo!("hot reloading currently WIP")
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::controller::DesktopController;
|
||||
use dioxus_core::ScopeState;
|
||||
use serde::de::Error;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
use wry::application::event_loop::ControlFlow;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
#[cfg(target_os = "ios")]
|
||||
use wry::application::platform::ios::WindowExtIOS;
|
||||
use wry::application::window::Fullscreen as WryFullscreen;
|
||||
|
||||
use UserWindowEvent::*;
|
||||
@@ -31,11 +40,18 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
||||
pub struct DesktopContext {
|
||||
/// The wry/tao proxy to the current window
|
||||
pub proxy: ProxyType,
|
||||
pub(super) eval_reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Value>>>,
|
||||
}
|
||||
|
||||
impl DesktopContext {
|
||||
pub(crate) fn new(proxy: ProxyType) -> Self {
|
||||
Self { proxy }
|
||||
pub(crate) fn new(
|
||||
proxy: ProxyType,
|
||||
eval_reciever: tokio::sync::mpsc::UnboundedReceiver<Value>,
|
||||
) -> Self {
|
||||
Self {
|
||||
proxy,
|
||||
eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)),
|
||||
}
|
||||
}
|
||||
|
||||
/// trigger the drag-window event
|
||||
@@ -134,16 +150,34 @@ impl DesktopContext {
|
||||
pub fn eval(&self, script: impl std::string::ToString) {
|
||||
let _ = self.proxy.send_event(Eval(script.to_string()));
|
||||
}
|
||||
|
||||
/// Push view
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
|
||||
let _ = self.proxy.send_event(PushView(view));
|
||||
}
|
||||
|
||||
/// Push view
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn pop_view(&self) {
|
||||
let _ = self.proxy.send_event(PopView);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UserWindowEvent {
|
||||
Update,
|
||||
EditsReady,
|
||||
Initialize,
|
||||
|
||||
CloseWindow,
|
||||
DragWindow,
|
||||
FocusWindow,
|
||||
|
||||
/// Set a new Dioxus template for hot-reloading
|
||||
///
|
||||
/// Is a no-op in release builds. Must fit the right format for templates
|
||||
SetTemplate(String),
|
||||
|
||||
Visible(bool),
|
||||
Minimize(bool),
|
||||
Maximize(bool),
|
||||
@@ -164,73 +198,155 @@ pub enum UserWindowEvent {
|
||||
DevTool,
|
||||
|
||||
Eval(String),
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PushView(objc_id::ShareId<objc::runtime::Object>),
|
||||
#[cfg(target_os = "ios")]
|
||||
PopView,
|
||||
}
|
||||
|
||||
pub(super) fn handler(
|
||||
user_event: UserWindowEvent,
|
||||
desktop: &mut DesktopController,
|
||||
control_flow: &mut ControlFlow,
|
||||
) {
|
||||
// currently dioxus-desktop supports a single window only,
|
||||
// so we can grab the only webview from the map;
|
||||
let webview = desktop.webviews.values().next().unwrap();
|
||||
let window = webview.window();
|
||||
impl DesktopController {
|
||||
pub(super) fn handle_event(
|
||||
&mut self,
|
||||
user_event: UserWindowEvent,
|
||||
control_flow: &mut ControlFlow,
|
||||
) {
|
||||
// currently dioxus-desktop supports a single window only,
|
||||
// so we can grab the only webview from the map;
|
||||
// on wayland it is possible that a user event is emitted
|
||||
// before the webview is initialized. ignore the event.
|
||||
let webview = if let Some(webview) = self.webviews.values().next() {
|
||||
webview
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
match user_event {
|
||||
Update => desktop.try_load_ready_webviews(),
|
||||
CloseWindow => *control_flow = ControlFlow::Exit,
|
||||
DragWindow => {
|
||||
// if the drag_window has any errors, we don't do anything
|
||||
window.fullscreen().is_none().then(|| window.drag_window());
|
||||
}
|
||||
Visible(state) => window.set_visible(state),
|
||||
Minimize(state) => window.set_minimized(state),
|
||||
Maximize(state) => window.set_maximized(state),
|
||||
MaximizeToggle => window.set_maximized(!window.is_maximized()),
|
||||
Fullscreen(state) => {
|
||||
if let Some(handle) = window.current_monitor() {
|
||||
window.set_fullscreen(state.then(|| WryFullscreen::Borderless(Some(handle))));
|
||||
let window = webview.window();
|
||||
|
||||
match user_event {
|
||||
Initialize | EditsReady => self.try_load_ready_webviews(),
|
||||
SetTemplate(template) => self.set_template(template),
|
||||
CloseWindow => *control_flow = ControlFlow::Exit,
|
||||
DragWindow => {
|
||||
// if the drag_window has any errors, we don't do anything
|
||||
window.fullscreen().is_none().then(|| window.drag_window());
|
||||
}
|
||||
}
|
||||
FocusWindow => window.set_focus(),
|
||||
Resizable(state) => window.set_resizable(state),
|
||||
AlwaysOnTop(state) => window.set_always_on_top(state),
|
||||
|
||||
CursorVisible(state) => window.set_cursor_visible(state),
|
||||
CursorGrab(state) => {
|
||||
let _ = window.set_cursor_grab(state);
|
||||
}
|
||||
|
||||
SetTitle(content) => window.set_title(&content),
|
||||
SetDecorations(state) => window.set_decorations(state),
|
||||
|
||||
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
|
||||
|
||||
Print => {
|
||||
if let Err(e) = webview.print() {
|
||||
// we can't panic this error.
|
||||
log::warn!("Open print modal failed: {e}");
|
||||
Visible(state) => window.set_visible(state),
|
||||
Minimize(state) => window.set_minimized(state),
|
||||
Maximize(state) => window.set_maximized(state),
|
||||
MaximizeToggle => window.set_maximized(!window.is_maximized()),
|
||||
Fullscreen(state) => {
|
||||
if let Some(handle) = window.current_monitor() {
|
||||
window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle))));
|
||||
}
|
||||
}
|
||||
}
|
||||
DevTool => {
|
||||
#[cfg(debug_assertions)]
|
||||
webview.open_devtools();
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
FocusWindow => window.set_focus(),
|
||||
Resizable(state) => window.set_resizable(state),
|
||||
AlwaysOnTop(state) => window.set_always_on_top(state),
|
||||
|
||||
Eval(code) => {
|
||||
if let Err(e) = webview.evaluate_script(code.as_str()) {
|
||||
// we can't panic this error.
|
||||
log::warn!("Eval script error: {e}");
|
||||
Eval(code) => {
|
||||
let script = format!(
|
||||
r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{
|
||||
{}
|
||||
}})()}}));"#,
|
||||
code
|
||||
);
|
||||
if let Err(e) = webview.evaluate_script(&script) {
|
||||
// we can't panic this error.
|
||||
log::warn!("Eval script error: {e}");
|
||||
}
|
||||
}
|
||||
CursorVisible(state) => window.set_cursor_visible(state),
|
||||
CursorGrab(state) => {
|
||||
let _ = window.set_cursor_grab(state);
|
||||
}
|
||||
|
||||
SetTitle(content) => window.set_title(&content),
|
||||
SetDecorations(state) => window.set_decorations(state),
|
||||
|
||||
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
|
||||
|
||||
Print => {
|
||||
if let Err(e) = webview.print() {
|
||||
// we can't panic this error.
|
||||
log::warn!("Open print modal failed: {e}");
|
||||
}
|
||||
}
|
||||
DevTool => {
|
||||
#[cfg(debug_assertions)]
|
||||
webview.open_devtools();
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PushView(view) => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
let ui_view = window.ui_view() as *mut Object;
|
||||
let ui_view_frame: *mut Object = msg_send![ui_view, frame];
|
||||
let _: () = msg_send![view, setFrame: ui_view_frame];
|
||||
let _: () = msg_send![view, setAutoresizingMask: 31];
|
||||
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
self.views.push(ui_view);
|
||||
},
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PopView => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
if let Some(view) = self.views.pop() {
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) {
|
||||
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult {
|
||||
let desktop = use_window(cx).clone();
|
||||
cx.use_hook(|| {
|
||||
move |script| {
|
||||
desktop.eval(script);
|
||||
let recv = desktop.eval_reciever.clone();
|
||||
EvalResult { reciever: recv }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
cx.use_hook(|| move |script| desktop.eval(script))
|
||||
/// A future that resolves to the result of a JavaScript evaluation.
|
||||
pub struct EvalResult {
|
||||
reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>>,
|
||||
}
|
||||
|
||||
impl IntoFuture for EvalResult {
|
||||
type Output = Result<serde_json::Value, serde_json::Error>;
|
||||
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut reciever = self.reciever.lock().await;
|
||||
match reciever.recv().await {
|
||||
Some(result) => Ok(result),
|
||||
None => Err(serde_json::Error::custom("No result returned")),
|
||||
}
|
||||
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
fn is_main_thread() -> bool {
|
||||
use objc::runtime::{Class, BOOL, NO};
|
||||
use objc::*;
|
||||
|
||||
let cls = Class::get("NSThread").unwrap();
|
||||
let result: BOOL = unsafe { msg_send![cls, isMainThread] };
|
||||
result != NO
|
||||
}
|
||||
+54
-183
@@ -1,12 +1,10 @@
|
||||
//! Convert a serialized event to an event trigger
|
||||
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
use dioxus_core::{ElementId, EventPriority, UserEvent};
|
||||
use dioxus_html::event_bubbles;
|
||||
use dioxus_html::on::*;
|
||||
use dioxus_html::events::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::from_value;
|
||||
use std::any::Any;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub(crate) struct IpcMessage {
|
||||
@@ -34,187 +32,60 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct ImEvent {
|
||||
event: String,
|
||||
mounted_dom_id: u64,
|
||||
contents: serde_json::Value,
|
||||
macro_rules! match_data {
|
||||
(
|
||||
$m:ident;
|
||||
$name:ident;
|
||||
$(
|
||||
$tip:ty => $($mname:literal)|* ;
|
||||
)*
|
||||
) => {
|
||||
match $name {
|
||||
$( $($mname)|* => {
|
||||
let val: $tip = from_value::<$tip>($m).ok()?;
|
||||
Rc::new(val) as Rc<dyn Any>
|
||||
})*
|
||||
_ => return None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
||||
let ImEvent {
|
||||
event,
|
||||
mounted_dom_id,
|
||||
contents,
|
||||
} = serde_json::from_value(val).unwrap();
|
||||
|
||||
let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
|
||||
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,
|
||||
bubbles: event_bubbles(name),
|
||||
data: event,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct EventMessage {
|
||||
pub contents: serde_json::Value,
|
||||
pub event: String,
|
||||
pub mounted_dom_id: usize,
|
||||
}
|
||||
|
||||
fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Send + Sync> {
|
||||
match name {
|
||||
"copy" | "cut" | "paste" => {
|
||||
//
|
||||
Arc::new(ClipboardData {})
|
||||
}
|
||||
"compositionend" | "compositionstart" | "compositionupdate" => {
|
||||
Arc::new(serde_json::from_value::<CompositionData>(val).unwrap())
|
||||
}
|
||||
"keydown" | "keypress" | "keyup" => {
|
||||
let evt = serde_json::from_value::<KeyboardData>(val).unwrap();
|
||||
Arc::new(evt)
|
||||
}
|
||||
"focus" | "blur" | "focusout" | "focusin" => {
|
||||
//
|
||||
Arc::new(FocusData {})
|
||||
}
|
||||
pub fn decode_event(value: EventMessage) -> Option<Rc<dyn Any>> {
|
||||
let val = value.contents;
|
||||
let name = value.event.as_str();
|
||||
type DragData = MouseData;
|
||||
|
||||
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
|
||||
// don't have a good solution with the serialized event problem
|
||||
"change" | "input" | "invalid" | "reset" | "submit" => {
|
||||
Arc::new(serde_json::from_value::<FormData>(val).unwrap())
|
||||
}
|
||||
let evt = match_data! { val; name;
|
||||
MouseData => "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup";
|
||||
ClipboardData => "copy" | "cut" | "paste";
|
||||
CompositionData => "compositionend" | "compositionstart" | "compositionupdate";
|
||||
KeyboardData => "keydown" | "keypress" | "keyup";
|
||||
FocusData => "blur" | "focus" | "focusin" | "focusout";
|
||||
FormData => "change" | "input" | "invalid" | "reset" | "submit";
|
||||
DragData => "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop";
|
||||
PointerData => "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture";
|
||||
SelectionData => "selectstart" | "selectionchange" | "select";
|
||||
TouchData => "touchcancel" | "touchend" | "touchmove" | "touchstart";
|
||||
ScrollData => "scroll";
|
||||
WheelData => "wheel";
|
||||
MediaData => "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied"
|
||||
| "encrypted" | "ended" | "interruptbegin" | "interruptend" | "loadeddata"
|
||||
| "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress"
|
||||
| "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate"
|
||||
| "volumechange" | "waiting" | "error" | "load" | "loadend" | "timeout";
|
||||
AnimationData => "animationstart" | "animationend" | "animationiteration";
|
||||
TransitionData => "transitionend";
|
||||
ToggleData => "toggle";
|
||||
// ImageData => "load" | "error";
|
||||
// OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
|
||||
};
|
||||
|
||||
"click" | "contextmenu" | "dblclick" | "doubleclick" | "drag" | "dragend" | "dragenter"
|
||||
| "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown"
|
||||
| "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
|
||||
Arc::new(serde_json::from_value::<MouseData>(val).unwrap())
|
||||
}
|
||||
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
|
||||
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
|
||||
Arc::new(serde_json::from_value::<PointerData>(val).unwrap())
|
||||
}
|
||||
"select" => {
|
||||
//
|
||||
Arc::new(serde_json::from_value::<SelectionData>(val).unwrap())
|
||||
}
|
||||
|
||||
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
|
||||
Arc::new(serde_json::from_value::<TouchData>(val).unwrap())
|
||||
}
|
||||
|
||||
"scroll" => Arc::new(()),
|
||||
|
||||
"wheel" => Arc::new(serde_json::from_value::<WheelData>(val).unwrap()),
|
||||
|
||||
"animationstart" | "animationend" | "animationiteration" => {
|
||||
Arc::new(serde_json::from_value::<AnimationData>(val).unwrap())
|
||||
}
|
||||
|
||||
"transitionend" => Arc::new(serde_json::from_value::<TransitionData>(val).unwrap()),
|
||||
|
||||
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
|
||||
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
|
||||
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
|
||||
| "timeupdate" | "volumechange" | "waiting" => {
|
||||
//
|
||||
Arc::new(MediaData {})
|
||||
}
|
||||
|
||||
"toggle" => Arc::new(ToggleData {}),
|
||||
|
||||
_ => Arc::new(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_name_from_type(typ: &str) -> &'static str {
|
||||
match typ {
|
||||
"copy" => "copy",
|
||||
"cut" => "cut",
|
||||
"paste" => "paste",
|
||||
"compositionend" => "compositionend",
|
||||
"compositionstart" => "compositionstart",
|
||||
"compositionupdate" => "compositionupdate",
|
||||
"keydown" => "keydown",
|
||||
"keypress" => "keypress",
|
||||
"keyup" => "keyup",
|
||||
"focus" => "focus",
|
||||
"focusout" => "focusout",
|
||||
"focusin" => "focusin",
|
||||
"blur" => "blur",
|
||||
"change" => "change",
|
||||
"input" => "input",
|
||||
"invalid" => "invalid",
|
||||
"reset" => "reset",
|
||||
"submit" => "submit",
|
||||
"click" => "click",
|
||||
"contextmenu" => "contextmenu",
|
||||
"doubleclick" => "doubleclick",
|
||||
"dblclick" => "dblclick",
|
||||
"drag" => "drag",
|
||||
"dragend" => "dragend",
|
||||
"dragenter" => "dragenter",
|
||||
"dragexit" => "dragexit",
|
||||
"dragleave" => "dragleave",
|
||||
"dragover" => "dragover",
|
||||
"dragstart" => "dragstart",
|
||||
"drop" => "drop",
|
||||
"mousedown" => "mousedown",
|
||||
"mouseenter" => "mouseenter",
|
||||
"mouseleave" => "mouseleave",
|
||||
"mousemove" => "mousemove",
|
||||
"mouseout" => "mouseout",
|
||||
"mouseover" => "mouseover",
|
||||
"mouseup" => "mouseup",
|
||||
"pointerdown" => "pointerdown",
|
||||
"pointermove" => "pointermove",
|
||||
"pointerup" => "pointerup",
|
||||
"pointercancel" => "pointercancel",
|
||||
"gotpointercapture" => "gotpointercapture",
|
||||
"lostpointercapture" => "lostpointercapture",
|
||||
"pointerenter" => "pointerenter",
|
||||
"pointerleave" => "pointerleave",
|
||||
"pointerover" => "pointerover",
|
||||
"pointerout" => "pointerout",
|
||||
"select" => "select",
|
||||
"touchcancel" => "touchcancel",
|
||||
"touchend" => "touchend",
|
||||
"touchmove" => "touchmove",
|
||||
"touchstart" => "touchstart",
|
||||
"scroll" => "scroll",
|
||||
"wheel" => "wheel",
|
||||
"animationstart" => "animationstart",
|
||||
"animationend" => "animationend",
|
||||
"animationiteration" => "animationiteration",
|
||||
"transitionend" => "transitionend",
|
||||
"abort" => "abort",
|
||||
"canplay" => "canplay",
|
||||
"canplaythrough" => "canplaythrough",
|
||||
"durationchange" => "durationchange",
|
||||
"emptied" => "emptied",
|
||||
"encrypted" => "encrypted",
|
||||
"ended" => "ended",
|
||||
"error" => "error",
|
||||
"loadeddata" => "loadeddata",
|
||||
"loadedmetadata" => "loadedmetadata",
|
||||
"loadstart" => "loadstart",
|
||||
"pause" => "pause",
|
||||
"play" => "play",
|
||||
"playing" => "playing",
|
||||
"progress" => "progress",
|
||||
"ratechange" => "ratechange",
|
||||
"seeked" => "seeked",
|
||||
"seeking" => "seeking",
|
||||
"stalled" => "stalled",
|
||||
"suspend" => "suspend",
|
||||
"timeupdate" => "timeupdate",
|
||||
"volumechange" => "volumechange",
|
||||
"waiting" => "waiting",
|
||||
"toggle" => "toggle",
|
||||
a => {
|
||||
panic!("unsupported event type {:?}", a);
|
||||
}
|
||||
}
|
||||
Some(evt)
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use dioxus_core::VirtualDom;
|
||||
use dioxus_rsx_interpreter::{error::Error, ErrorHandler, SetManyRsxMessage, RSX_CONTEXT};
|
||||
|
||||
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::time::Duration;
|
||||
use std::{sync::Arc, sync::Mutex};
|
||||
|
||||
@@ -11,60 +13,34 @@ fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalS
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) fn init(dom: &VirtualDom) {
|
||||
pub(crate) fn init(_dom: &VirtualDom) {
|
||||
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
|
||||
Arc::new(Mutex::new(None));
|
||||
|
||||
let latest_in_connection_handle = latest_in_connection.clone();
|
||||
let latest_out_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
|
||||
Arc::new(Mutex::new(None));
|
||||
let latest_out_connection_handle = latest_out_connection.clone();
|
||||
|
||||
struct DesktopErrorHandler {
|
||||
latest_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>>,
|
||||
}
|
||||
impl ErrorHandler for DesktopErrorHandler {
|
||||
fn handle_error(&self, err: Error) {
|
||||
if let Some(conn) = &mut *self.latest_connection.lock().unwrap() {
|
||||
conn.get_mut()
|
||||
.write_all((serde_json::to_string(&err).unwrap() + "\n").as_bytes())
|
||||
.unwrap();
|
||||
} else {
|
||||
panic!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RSX_CONTEXT.set_error_handler(DesktopErrorHandler {
|
||||
latest_connection: latest_out_connection_handle,
|
||||
});
|
||||
RSX_CONTEXT.provide_scheduler_channel(dom.get_scheduler_channel());
|
||||
|
||||
// connect to processes for incoming data
|
||||
std::thread::spawn(move || {
|
||||
if let Ok(listener) = LocalSocketListener::bind("@dioxusin") {
|
||||
let temp_file = std::env::temp_dir().join("@dioxusin");
|
||||
|
||||
if let Ok(listener) = LocalSocketListener::bind(temp_file) {
|
||||
for conn in listener.incoming().filter_map(handle_error) {
|
||||
*latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// connect to processes for outgoing errors
|
||||
std::thread::spawn(move || {
|
||||
if let Ok(listener) = LocalSocketListener::bind("@dioxusout") {
|
||||
for conn in listener.incoming().filter_map(handle_error) {
|
||||
*latest_out_connection.lock().unwrap() = Some(BufReader::new(conn));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
if let Some(conn) = &mut *latest_in_connection.lock().unwrap() {
|
||||
let mut buf = String::new();
|
||||
match conn.read_line(&mut buf) {
|
||||
Ok(_) => {
|
||||
let msgs: SetManyRsxMessage = serde_json::from_str(&buf).unwrap();
|
||||
RSX_CONTEXT.extend(msgs);
|
||||
todo!()
|
||||
// let msg: SetTemplateMsg = serde_json::from_str(&buf).unwrap();
|
||||
// channel
|
||||
// .start_send(SchedulerMsg::SetTemplate(Box::new(msg)))
|
||||
// .unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
if err.kind() != std::io::ErrorKind::WouldBlock {
|
||||
|
||||
+135
-110
@@ -8,16 +8,20 @@ mod controller;
|
||||
mod desktop_context;
|
||||
mod escape;
|
||||
mod events;
|
||||
#[cfg(feature = "hot-reload")]
|
||||
mod hot_reload;
|
||||
mod protocol;
|
||||
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
mod hot_reload;
|
||||
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use desktop_context::UserWindowEvent;
|
||||
pub use desktop_context::{use_eval, use_window, DesktopContext};
|
||||
pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult};
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
|
||||
use crate::events::trigger_from_serialized;
|
||||
pub use cfg::Config;
|
||||
use controller::DesktopController;
|
||||
use dioxus_core::*;
|
||||
@@ -100,99 +104,141 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
||||
/// ```
|
||||
pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cfg: Config) {
|
||||
let event_loop = EventLoop::with_user_event();
|
||||
|
||||
let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy());
|
||||
let proxy = event_loop.create_proxy();
|
||||
|
||||
// We assume that if the icon is None, then the user just didnt set it
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => desktop.start(&mut cfg, event_loop),
|
||||
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::UserEvent(user_event) => desktop.handle_event(user_event, control_flow),
|
||||
Event::MainEventsCleared => {}
|
||||
Event::Resumed => {}
|
||||
Event::Suspended => {}
|
||||
Event::LoopDestroyed => {}
|
||||
Event::RedrawRequested(_id) => {}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl DesktopController {
|
||||
fn start(
|
||||
&mut self,
|
||||
cfg: &mut Config,
|
||||
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
||||
) {
|
||||
let webview = build_webview(
|
||||
cfg,
|
||||
event_loop,
|
||||
self.is_ready.clone(),
|
||||
self.proxy.clone(),
|
||||
self.eval_sender.clone(),
|
||||
self.event_tx.clone(),
|
||||
);
|
||||
|
||||
self.webviews.insert(webview.window().id(), webview);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_webview(
|
||||
cfg: &mut Config,
|
||||
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
||||
is_ready: Arc<AtomicBool>,
|
||||
proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
|
||||
eval_sender: tokio::sync::mpsc::UnboundedSender<serde_json::Value>,
|
||||
event_tx: UnboundedSender<serde_json::Value>,
|
||||
) -> wry::webview::WebView {
|
||||
let builder = cfg.window.clone();
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
let custom_head = cfg.custom_head.clone();
|
||||
let resource_dir = cfg.resource_dir.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
let root_name = cfg.root_name.clone();
|
||||
|
||||
// We assume that if the icon is None in cfg, then the user just didnt set it
|
||||
if cfg.window.window.window_icon.is_none() {
|
||||
cfg.window.window.window_icon = Some(
|
||||
window.set_window_icon(Some(
|
||||
tao::window::Icon::from_rgba(
|
||||
include_bytes!("./assets/default_icon.bin").to_vec(),
|
||||
460,
|
||||
460,
|
||||
)
|
||||
.expect("image parse failed"),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => {
|
||||
let builder = cfg.window.clone();
|
||||
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let window_id = window.id();
|
||||
|
||||
let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone());
|
||||
|
||||
let proxy = proxy.clone();
|
||||
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
let custom_head = cfg.custom_head.clone();
|
||||
let resource_dir = cfg.resource_dir.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
.with_transparent(cfg.window.window.transparent)
|
||||
.with_url("dioxus://index.html/")
|
||||
.unwrap()
|
||||
.with_ipc_handler(move |_window: &Window, payload: String| {
|
||||
parse_ipc_message(&payload)
|
||||
.map(|message| match message.method() {
|
||||
"user_event" => {
|
||||
let event = trigger_from_serialized(message.params());
|
||||
log::trace!("User event: {:?}", event);
|
||||
sender.unbounded_send(SchedulerMsg::Event(event)).unwrap();
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
.with_transparent(cfg.window.window.transparent)
|
||||
.with_url("dioxus://index.html/")
|
||||
.unwrap()
|
||||
.with_ipc_handler(move |_window: &Window, payload: String| {
|
||||
parse_ipc_message(&payload)
|
||||
.map(|message| match message.method() {
|
||||
"eval_result" => {
|
||||
let result = message.params();
|
||||
eval_sender.send(result).unwrap();
|
||||
}
|
||||
"user_event" => {
|
||||
_ = event_tx.unbounded_send(message.params());
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
"browser_open" => {
|
||||
let data = message.params();
|
||||
log::trace!("Open browser: {:?}", data);
|
||||
if let Some(temp) = data.as_object() {
|
||||
if temp.contains_key("href") {
|
||||
let url = temp.get("href").unwrap().as_str().unwrap();
|
||||
if let Err(e) = webbrowser::open(url) {
|
||||
log::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let _ = proxy.send_event(UserWindowEvent::Update);
|
||||
}
|
||||
"browser_open" => {
|
||||
let data = message.params();
|
||||
log::trace!("Open browser: {:?}", data);
|
||||
if let Some(temp) = data.as_object() {
|
||||
if temp.contains_key("href") {
|
||||
let url = temp.get("href").unwrap().as_str().unwrap();
|
||||
if let Err(e) = webbrowser::open(url) {
|
||||
log::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("invalid IPC message received");
|
||||
});
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(
|
||||
r,
|
||||
resource_dir.clone(),
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
)
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
.as_ref()
|
||||
.map(|handler| handler(window, evet))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("invalid IPC message received");
|
||||
});
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(
|
||||
r,
|
||||
resource_dir.clone(),
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
&root_name,
|
||||
)
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
.as_ref()
|
||||
.map(|handler| handler(window, evet))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
}
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
}
|
||||
|
||||
if cfg.disable_context_menu {
|
||||
// in release mode, we don't want to show the dev tool or reload menus
|
||||
webview = webview.with_initialization_script(
|
||||
r#"
|
||||
if cfg.disable_context_menu {
|
||||
// in release mode, we don't want to show the dev tool or reload menus
|
||||
webview = webview.with_initialization_script(
|
||||
r#"
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
@@ -203,32 +249,11 @@ pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cf
|
||||
});
|
||||
}
|
||||
"#,
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
|
||||
desktop.webviews.insert(window_id, webview.build().unwrap());
|
||||
}
|
||||
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::UserEvent(user_event) => {
|
||||
desktop_context::handler(user_event, &mut desktop, control_flow)
|
||||
}
|
||||
Event::MainEventsCleared => {}
|
||||
Event::Resumed => {}
|
||||
Event::Suspended => {}
|
||||
Event::LoopDestroyed => {}
|
||||
Event::RedrawRequested(_id) => {}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
webview.build().unwrap()
|
||||
}
|
||||
@@ -1,26 +1,32 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use wry::{
|
||||
http::{status::StatusCode, Request, Response, ResponseBuilder},
|
||||
http::{status::StatusCode, Request, Response},
|
||||
Result,
|
||||
};
|
||||
|
||||
const MODULE_LOADER: &str = r#"
|
||||
fn module_loader(root_name: &str) -> String {
|
||||
format!(
|
||||
"
|
||||
<script>
|
||||
import("./index.js").then(function (module) {
|
||||
module.main();
|
||||
});
|
||||
import(\"./index.js\").then(function (module) {{
|
||||
module.main(\"{}\");
|
||||
}});
|
||||
</script>
|
||||
"#;
|
||||
",
|
||||
root_name
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn desktop_handler(
|
||||
request: &Request,
|
||||
request: &Request<Vec<u8>>,
|
||||
asset_root: Option<PathBuf>,
|
||||
custom_head: Option<String>,
|
||||
custom_index: Option<String>,
|
||||
) -> Result<Response> {
|
||||
root_name: &str,
|
||||
) -> Result<Response<Vec<u8>>> {
|
||||
// Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case".
|
||||
// For now, we only serve two pieces of content which get included as bytes into the final binary.
|
||||
let path = request.uri().replace("dioxus://", "");
|
||||
let path = request.uri().to_string().replace("dioxus://", "");
|
||||
|
||||
// all assets should be called from index.html
|
||||
let trimmed = path.trim_start_matches("index.html/");
|
||||
@@ -30,25 +36,30 @@ pub(super) fn desktop_handler(
|
||||
// we'll look for the closing </body> tag and insert our little module loader there.
|
||||
if let Some(custom_index) = custom_index {
|
||||
let rendered = custom_index
|
||||
.replace("</body>", &format!("{}</body>", MODULE_LOADER))
|
||||
.replace("</body>", &format!("{}</body>", module_loader(root_name)))
|
||||
.into_bytes();
|
||||
ResponseBuilder::new().mimetype("text/html").body(rendered)
|
||||
Response::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.body(rendered)
|
||||
.map_err(From::from)
|
||||
} else {
|
||||
// Otherwise, we'll serve the default index.html and apply a custom head if that's specified.
|
||||
let mut template = include_str!("./index.html").to_string();
|
||||
if let Some(custom_head) = custom_head {
|
||||
template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
|
||||
}
|
||||
template = template.replace("<!-- MODULE LOADER -->", MODULE_LOADER);
|
||||
template = template.replace("<!-- MODULE LOADER -->", &module_loader(root_name));
|
||||
|
||||
ResponseBuilder::new()
|
||||
.mimetype("text/html")
|
||||
Response::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.body(template.into_bytes())
|
||||
.map_err(From::from)
|
||||
}
|
||||
} else if trimmed == "index.js" {
|
||||
ResponseBuilder::new()
|
||||
.mimetype("text/javascript")
|
||||
Response::builder()
|
||||
.header("Content-Type", "text/javascript")
|
||||
.body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec())
|
||||
.map_err(From::from)
|
||||
} else {
|
||||
let asset_root = asset_root
|
||||
.unwrap_or_else(|| get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf()))
|
||||
@@ -57,20 +68,23 @@ pub(super) fn desktop_handler(
|
||||
let asset = asset_root.join(trimmed).canonicalize()?;
|
||||
|
||||
if !asset.starts_with(asset_root) {
|
||||
return ResponseBuilder::new()
|
||||
return Response::builder()
|
||||
.status(StatusCode::FORBIDDEN)
|
||||
.body(String::from("Forbidden").into_bytes());
|
||||
.body(String::from("Forbidden").into_bytes())
|
||||
.map_err(From::from);
|
||||
}
|
||||
|
||||
if !asset.exists() {
|
||||
return ResponseBuilder::new()
|
||||
return Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(String::from("Not Found").into_bytes());
|
||||
.body(String::from("Not Found").into_bytes())
|
||||
.map_err(From::from);
|
||||
}
|
||||
|
||||
ResponseBuilder::new()
|
||||
.mimetype(get_mime_from_path(trimmed)?)
|
||||
Response::builder()
|
||||
.header("Content-Type", get_mime_from_path(trimmed)?)
|
||||
.body(std::fs::read(asset)?)
|
||||
.map_err(From::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +110,10 @@ fn get_asset_root() -> Option<PathBuf> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let bundle = core_foundation::bundle::CFBundle::main_bundle();
|
||||
let bundle_path = dbg!(bundle.path()?);
|
||||
let resources_path = dbg!(bundle.resources_path()?);
|
||||
let absolute_resources_root = dbg!(bundle_path.join(resources_path));
|
||||
let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?);
|
||||
let bundle_path = bundle.path()?;
|
||||
let resources_path = bundle.resources_path()?;
|
||||
let absolute_resources_root = bundle_path.join(resources_path);
|
||||
let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
|
||||
|
||||
return Some(canonical_resources_root);
|
||||
}
|
||||
@@ -113,7 +127,7 @@ fn get_mime_from_path(trimmed: &str) -> Result<&str> {
|
||||
return Ok("image/svg+xml");
|
||||
}
|
||||
|
||||
let res = match infer::get_from_path(&trimmed)?.map(|f| f.mime_type()) {
|
||||
let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
|
||||
Some(t) if t == "text/plain" => get_mime_by_ext(trimmed),
|
||||
Some(f) => f,
|
||||
None => get_mime_by_ext(trimmed),
|
||||
|
||||
@@ -17,22 +17,13 @@ dioxus-html = { path = "../html", version = "^0.2.1", optional = true }
|
||||
dioxus-core-macro = { path = "../core-macro", version = "^0.2.1", optional = true }
|
||||
dioxus-hooks = { path = "../hooks", version = "^0.2.1", optional = true }
|
||||
dioxus-rsx = { path = "../rsx", optional = true }
|
||||
dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true }
|
||||
|
||||
|
||||
# dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.2.1", optional = true }
|
||||
# dioxus-native-core = { path = "./packages/native-core", version = "^0.2.0", optional = true }
|
||||
# dioxus-native-core-macro = { path = "./packages/native-core-macro", version = "^0.2.0", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["macro", "hooks", "html"]
|
||||
macro = ["dioxus-core-macro", "dioxus-rsx"]
|
||||
html = ["dioxus-html"]
|
||||
hooks = ["dioxus-hooks"]
|
||||
hot-reload = [
|
||||
"dioxus-core-macro/hot-reload",
|
||||
"dioxus-rsx-interpreter",
|
||||
]
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
futures-util = "0.3.21"
|
||||
@@ -41,10 +32,9 @@ rand = { version = "0.8.4", features = ["small_rng"] }
|
||||
criterion = "0.3.5"
|
||||
thiserror = "1.0.30"
|
||||
env_logger = "0.9.0"
|
||||
tokio = { version = "1.21.2", features = ["full"] }
|
||||
# dioxus-edit-stream = { path = "../edit-stream" }
|
||||
|
||||
[[bench]]
|
||||
name = "create"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "jsframework"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
fn main() {}
|
||||
@@ -5,9 +5,14 @@
|
||||
//! to be made, but the change application phase will be just as performant as the vanilla wasm_bindgen code. In essence,
|
||||
//! we are measuring the overhead of Dioxus, not the performance of the "apply" phase.
|
||||
//!
|
||||
//! On my MBP 2019:
|
||||
//! - Dioxus takes 3ms to create 1_000 rows
|
||||
//! - Dioxus takes 30ms to create 10_000 rows
|
||||
//!
|
||||
//! Pre-templates (Mac M1):
|
||||
//! - 3ms to create 1_000 rows
|
||||
//! - 30ms to create 10_000 rows
|
||||
//!
|
||||
//! Post-templates
|
||||
//! - 580us to create 1_000 rows
|
||||
//! - 6.2ms to create 10_000 rows
|
||||
//!
|
||||
//! As pure "overhead", these are amazing good numbers, mostly slowed down by hitting the global allocator.
|
||||
//! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster.
|
||||
@@ -20,25 +25,26 @@ criterion_group!(mbenches, create_rows);
|
||||
criterion_main!(mbenches);
|
||||
|
||||
fn create_rows(c: &mut Criterion) {
|
||||
static App: Component = |cx| {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
|
||||
render!(table {
|
||||
tbody {
|
||||
(0..10_000_usize).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
rsx!(Row {
|
||||
row_id: f,
|
||||
label: label
|
||||
render!(
|
||||
table {
|
||||
tbody {
|
||||
(0..10_000_usize).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
rsx!( Row { row_id: f, label: label } )
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
c.bench_function("create rows", |b| {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
|
||||
b.iter(|| {
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let g = dom.rebuild();
|
||||
assert!(g.edits.len() > 1);
|
||||
})
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
use dioxus::prelude::*;
|
||||
use rand::prelude::*;
|
||||
|
||||
fn main() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
for _ in 0..1000 {
|
||||
_ = dom.rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
|
||||
cx.render(rsx! (
|
||||
table {
|
||||
tbody {
|
||||
(0..10_000_usize).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
rsx!( table_row { row_id: f, label: label } )
|
||||
})
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct RowProps {
|
||||
row_id: usize,
|
||||
label: Label,
|
||||
}
|
||||
fn table_row(cx: Scope<RowProps>) -> Element {
|
||||
let [adj, col, noun] = cx.props.label.0;
|
||||
cx.render(rsx! {
|
||||
tr {
|
||||
td { class:"col-md-1", "{cx.props.row_id}" }
|
||||
td { class:"col-md-1", onclick: move |_| { /* run onselect */ },
|
||||
a { class: "lbl", "{adj}" "{col}" "{noun}" }
|
||||
}
|
||||
td { class: "col-md-1",
|
||||
a { class: "remove", onclick: move |_| {/* remove */},
|
||||
span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct Label([&'static str; 3]);
|
||||
|
||||
impl Label {
|
||||
fn new(rng: &mut SmallRng) -> Self {
|
||||
Label([
|
||||
ADJECTIVES.choose(rng).unwrap(),
|
||||
COLOURS.choose(rng).unwrap(),
|
||||
NOUNS.choose(rng).unwrap(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
static ADJECTIVES: &[&str] = &[
|
||||
"pretty",
|
||||
"large",
|
||||
"big",
|
||||
"small",
|
||||
"tall",
|
||||
"short",
|
||||
"long",
|
||||
"handsome",
|
||||
"plain",
|
||||
"quaint",
|
||||
"clean",
|
||||
"elegant",
|
||||
"easy",
|
||||
"angry",
|
||||
"crazy",
|
||||
"helpful",
|
||||
"mushy",
|
||||
"odd",
|
||||
"unsightly",
|
||||
"adorable",
|
||||
"important",
|
||||
"inexpensive",
|
||||
"cheap",
|
||||
"expensive",
|
||||
"fancy",
|
||||
];
|
||||
|
||||
static COLOURS: &[&str] = &[
|
||||
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
|
||||
"orange",
|
||||
];
|
||||
|
||||
static NOUNS: &[&str] = &[
|
||||
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
|
||||
"pizza", "mouse", "keyboard",
|
||||
];
|
||||
@@ -1,5 +1,3 @@
|
||||
#![doc = include_str!("../../../notes/README.md")]
|
||||
|
||||
pub use dioxus_core as core;
|
||||
|
||||
#[cfg(feature = "hooks")]
|
||||
@@ -7,7 +5,7 @@ pub use dioxus_hooks as hooks;
|
||||
|
||||
pub mod events {
|
||||
#[cfg(feature = "html")]
|
||||
pub use dioxus_html::{on::*, KeyCode};
|
||||
pub use dioxus_html::prelude::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "html")]
|
||||
@@ -32,11 +30,5 @@ pub mod prelude {
|
||||
pub use dioxus_html as dioxus_elements;
|
||||
|
||||
#[cfg(feature = "html")]
|
||||
pub use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
|
||||
#[cfg(feature = "hot-reload")]
|
||||
pub use dioxus_rsx_interpreter::{
|
||||
captuered_context::{CapturedContext, FormattedArg, IfmtArgs},
|
||||
get_line_num, resolve_scope, CodeLocation, RsxContext,
|
||||
};
|
||||
pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes};
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
struct_lit_width = 80
|
||||
@@ -1,46 +0,0 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_borrowed_state() {
|
||||
let _ = VirtualDom::new(Parent);
|
||||
}
|
||||
|
||||
fn Parent(cx: Scope) -> Element {
|
||||
let value = cx.use_hook(String::new);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
Child { name: value }
|
||||
Child { name: value }
|
||||
Child { name: value }
|
||||
Child { name: value }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct ChildProps<'a> {
|
||||
name: &'a str,
|
||||
}
|
||||
|
||||
fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
h1 { "it's nested" }
|
||||
Child2 { name: cx.props.name }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct Grandchild<'a> {
|
||||
name: &'a str,
|
||||
}
|
||||
|
||||
fn Child2<'a>(cx: Scope<'a, Grandchild<'a>>) -> Element {
|
||||
cx.render(rsx! {
|
||||
div { "Hello {cx.props.name}!" }
|
||||
})
|
||||
}
|
||||
@@ -1,211 +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::*;
|
||||
|
||||
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,
|
||||
[
|
||||
CreateElement { root: 1, tag: "div" },
|
||||
CreateElement { root: 2, tag: "div" },
|
||||
CreateTextNode { root: 3, text: "Hello, world!" },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[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,
|
||||
[
|
||||
CreateElement { root: 1, tag: "div" },
|
||||
CreateElement { root: 2, tag: "div" },
|
||||
CreateTextNode { root: 3, text: "Hello, world!" },
|
||||
CreateElement { root: 4, tag: "div" },
|
||||
CreateElement { root: 5, tag: "div" },
|
||||
CreateTextNode { root: 6, text: "hello" },
|
||||
CreateTextNode { root: 7, text: "world" },
|
||||
AppendChildren { many: 2 },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 2 },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
// copilot wrote this test :P
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[
|
||||
CreateElement { root: 1, tag: "div" },
|
||||
CreateTextNode { root: 2, text: "hello" },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { root: 3, tag: "div" },
|
||||
CreateTextNode { root: 4, text: "hello" },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { root: 5, tag: "div" },
|
||||
CreateTextNode { root: 6, text: "hello" },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 3 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
// copilot wrote this test :P
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[
|
||||
CreateElement { root: 1, tag: "div" },
|
||||
CreateElement { root: 2, tag: "div" },
|
||||
CreateElement { root: 3, tag: "div" },
|
||||
CreateElement { root: 4, tag: "div" },
|
||||
AppendChildren { many: 4 },
|
||||
]
|
||||
);
|
||||
}
|
||||
#[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,
|
||||
[
|
||||
CreateElement { root: 1, tag: "h1" },
|
||||
CreateElement { root: 2, tag: "div" },
|
||||
CreateTextNode { root: 3, text: "abc1" },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { root: 4, tag: "p" },
|
||||
CreateElement { root: 5, tag: "h1" },
|
||||
CreateElement { root: 6, tag: "div" },
|
||||
CreateTextNode { root: 7, text: "abc2" },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { root: 8, tag: "p" },
|
||||
CreateElement { root: 9, tag: "h1" },
|
||||
CreateElement { root: 10, tag: "div" },
|
||||
CreateTextNode { root: 11, text: "abc3" },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { root: 12, tag: "p" },
|
||||
AppendChildren { many: 9 },
|
||||
]
|
||||
);
|
||||
}
|
||||
#[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,
|
||||
[
|
||||
CreateElement { root: 1, tag: "div" },
|
||||
CreateTextNode { root: 2, text: "hello" },
|
||||
AppendChildren { many: 1 },
|
||||
CreatePlaceholder { root: 3 },
|
||||
AppendChildren { many: 2 },
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -1,830 +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::prelude::*;
|
||||
|
||||
fn new_dom() -> VirtualDom {
|
||||
VirtualDom::new(|cx| render!("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! ( div { "Hello world" } ),
|
||||
rsx! ( div { "Goodbye world" } ),
|
||||
);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[
|
||||
CreateElement { root: 1, tag: "div" },
|
||||
CreateTextNode { root: 2, text: "Hello world" },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(change.edits, [SetText { text: "Goodbye world", root: 2 },]);
|
||||
}
|
||||
|
||||
/// Should result in 3 elements on the stack
|
||||
#[test]
|
||||
fn fragments_create_properly() {
|
||||
let dom = new_dom();
|
||||
|
||||
let create = dom.create_vnodes(rsx! {
|
||||
div { "Hello a" }
|
||||
div { "Hello b" }
|
||||
div { "Hello c" }
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[
|
||||
CreateElement { root: 1, tag: "div" },
|
||||
CreateTextNode { root: 2, text: "Hello a" },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { root: 3, tag: "div" },
|
||||
CreateTextNode { root: 4, text: "Hello b" },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { root: 5, tag: "div" },
|
||||
CreateTextNode { root: 6, text: "Hello c" },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 3 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// 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!({ (0..0).map(|_f| rsx! { div {}}) });
|
||||
let right = rsx!({ (0..1).map(|_f| rsx! { div {}}) });
|
||||
|
||||
let (create, change) = dom.diff_lazynodes(left, right);
|
||||
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }]
|
||||
);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreateElement { root: 2, tag: "div" },
|
||||
ReplaceWith { m: 1, root: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// 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!({ (0..0).map(|_f| rsx! { div {}}) });
|
||||
let right = rsx!({ (0..5).map(|_f| rsx! { div {}}) });
|
||||
|
||||
let (create, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreateElement { root: 2, tag: "div" },
|
||||
CreateElement { root: 3, tag: "div" },
|
||||
CreateElement { root: 4, tag: "div" },
|
||||
CreateElement { root: 5, tag: "div" },
|
||||
CreateElement { root: 6, tag: "div" },
|
||||
ReplaceWith { m: 5, root: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// 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!({ (0..0).map(|_| rsx! { div {} }) });
|
||||
let right = rsx!({
|
||||
(0..3).map(|f| {
|
||||
rsx! { div { "hello: {f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (create, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreateElement { tag: "div", root: 2 },
|
||||
CreateTextNode { text: "hello: 0", root: 3 },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { tag: "div", root: 4 },
|
||||
CreateTextNode { text: "hello: 1", root: 5 },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { tag: "div", root: 6 },
|
||||
CreateTextNode { text: "hello: 2", root: 7 },
|
||||
AppendChildren { many: 1 },
|
||||
ReplaceWith { root: 1, m: 3 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// 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!({
|
||||
(0..2).map(|_| {
|
||||
rsx! { div { "hello" }}
|
||||
})
|
||||
});
|
||||
let right = rsx!({ (0..0).map(|_| rsx! { div {} }) });
|
||||
|
||||
let (create, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[
|
||||
CreateElement { root: 1, tag: "div" },
|
||||
CreateTextNode { text: "hello", root: 2 },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { root: 3, tag: "div" },
|
||||
CreateTextNode { text: "hello", root: 4 },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 2 },
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreatePlaceholder { root: 5 },
|
||||
ReplaceWith { root: 1, m: 1 },
|
||||
Remove { root: 3 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in no edits
|
||||
#[test]
|
||||
fn two_equal_fragments_are_equal() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
(0..2).map(|_| {
|
||||
rsx! { div { "hello" }}
|
||||
})
|
||||
});
|
||||
let right = rsx!({
|
||||
(0..2).map(|_| {
|
||||
rsx! { 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!(
|
||||
{ (0..2).map(|_| rsx! { div { }} ) }
|
||||
p {}
|
||||
);
|
||||
let right = rsx!(
|
||||
{ (0..5).map(|_| rsx! (h1 { }) ) }
|
||||
p {}
|
||||
);
|
||||
|
||||
let (_create, changes) = dom.diff_lazynodes(left, right);
|
||||
println!("{:#?}", &changes);
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
// create the new h1s
|
||||
CreateElement { tag: "h1", root: 4 },
|
||||
CreateElement { tag: "h1", root: 5 },
|
||||
CreateElement { tag: "h1", root: 6 },
|
||||
InsertAfter { root: 2, n: 3 },
|
||||
// replace the divs with new h1s
|
||||
CreateElement { tag: "h1", root: 7 },
|
||||
ReplaceWith { root: 1, m: 1 },
|
||||
CreateElement { tag: "h1", root: 1 }, // notice how 1 gets re-used
|
||||
ReplaceWith { root: 2, m: 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!(
|
||||
{(0..5).map(|f| {rsx! { div { }}})}
|
||||
p {}
|
||||
);
|
||||
let right = rsx!(
|
||||
{(0..2).map(|f| {rsx! { h1 { }}})}
|
||||
p {}
|
||||
);
|
||||
|
||||
let (create, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[
|
||||
CreateElement { root: 1, tag: "div" },
|
||||
CreateElement { root: 2, tag: "div" },
|
||||
CreateElement { root: 3, tag: "div" },
|
||||
CreateElement { root: 4, tag: "div" },
|
||||
CreateElement { root: 5, tag: "div" },
|
||||
CreateElement { root: 6, tag: "p" },
|
||||
AppendChildren { many: 6 },
|
||||
]
|
||||
);
|
||||
|
||||
// note: key reuse is always the last node that got used
|
||||
// slab maintains a linked list, essentially
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
Remove { root: 3 },
|
||||
Remove { root: 4 },
|
||||
Remove { root: 5 },
|
||||
CreateElement { root: 5, tag: "h1" }, // 3 gets reused
|
||||
ReplaceWith { root: 1, m: 1 }, // 1 gets deleted
|
||||
CreateElement { root: 1, tag: "h1" }, // 1 gets reused
|
||||
ReplaceWith { root: 2, m: 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!(
|
||||
{(0..2).map(|f| {rsx! { div { }}})}
|
||||
p {}
|
||||
);
|
||||
let right = rsx!(
|
||||
{(0..5).map(|f| {rsx! { div { }}})}
|
||||
p {}
|
||||
);
|
||||
|
||||
let (create, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
create.edits,
|
||||
[
|
||||
CreateElement { root: 1, tag: "div" },
|
||||
CreateElement { root: 2, tag: "div" },
|
||||
CreateElement { root: 3, tag: "p" },
|
||||
AppendChildren { many: 3 },
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreateElement { root: 4, tag: "div" },
|
||||
CreateElement { root: 5, tag: "div" },
|
||||
CreateElement { root: 6, tag: "div" },
|
||||
InsertAfter { root: 2, n: 3 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// should result in the removal of elements
|
||||
#[test]
|
||||
fn keyed_diffing_order() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!(
|
||||
{(0..5).map(|f| {rsx! { div { key: "{f}" }}})}
|
||||
p {"e"}
|
||||
);
|
||||
let right = rsx!(
|
||||
{(0..2).map(|f| rsx! { div { key: "{f}" }})}
|
||||
p {"e"}
|
||||
);
|
||||
|
||||
let (create, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[Remove { root: 3 }, Remove { root: 4 }, Remove { root: 5 },]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves, but not removals or additions
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, changes) = dom.diff_lazynodes(left, right);
|
||||
println!("{:?}", &changes);
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[PushRoot { root: 7 }, InsertBefore { root: 5, n: 1 }]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves only
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 8, 7, 4, 5, 6 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
PushRoot { root: 5 },
|
||||
PushRoot { root: 4 },
|
||||
InsertBefore { n: 2, root: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_2() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 7, 8, 4, 5, 6 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
PushRoot { root: 4 },
|
||||
PushRoot { root: 5 },
|
||||
InsertBefore { n: 2, root: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_3() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 4, 8, 7, 5, 6 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
PushRoot { root: 5 },
|
||||
PushRoot { root: 4 },
|
||||
InsertBefore { n: 2, root: 2 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_4() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 4, 5, 8, 7, 6 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
PushRoot { root: 5 },
|
||||
PushRoot { root: 4 },
|
||||
InsertBefore { n: 2, root: 3 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves onl
|
||||
#[test]
|
||||
fn keyed_diffing_out_of_order_adds_5() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 4, 5, 6, 8, 7 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[PushRoot { root: 5 }, InsertBefore { n: 1, root: 4 }]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyed_diffing_additions() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 4, 5, 6, 7, 8, 9, 10 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.diff_lazynodes(left, right);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
CreateElement { root: 6, tag: "div" },
|
||||
CreateElement { root: 7, tag: "div" },
|
||||
InsertAfter { n: 2, root: 5 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyed_diffing_additions_and_moves_on_ends() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 4, 5, 6, 7 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, change) = dom.diff_lazynodes(left, right);
|
||||
println!("{:?}", change);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
// create 11, 12
|
||||
CreateElement { tag: "div", root: 5 },
|
||||
CreateElement { tag: "div", root: 6 },
|
||||
InsertAfter { root: 3, n: 2 },
|
||||
// move 7 to the front
|
||||
PushRoot { root: 4 },
|
||||
InsertBefore { root: 1, n: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyed_diffing_additions_and_moves_in_middle() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[/**/ 1, 2, 3, 4 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
// LIS: 4, 5, 6
|
||||
let (_, change) = dom.diff_lazynodes(left, right);
|
||||
println!("{:#?}", change);
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
// create 5, 6
|
||||
CreateElement { tag: "div", root: 5 },
|
||||
CreateElement { tag: "div", root: 6 },
|
||||
InsertBefore { root: 3, n: 2 },
|
||||
// create 7, 8
|
||||
CreateElement { tag: "div", root: 7 },
|
||||
CreateElement { tag: "div", root: 8 },
|
||||
InsertBefore { root: 2, n: 2 },
|
||||
// move 7
|
||||
PushRoot { root: 4 },
|
||||
InsertBefore { root: 1, n: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controlled_keyed_diffing_out_of_order() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[4, 5, 6, 7].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[0, 5, 9, 6, 4].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
// LIS: 5, 6
|
||||
let (_, changes) = dom.diff_lazynodes(left, right);
|
||||
println!("{:#?}", &changes);
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
Remove { root: 4 },
|
||||
// move 4 to after 6
|
||||
PushRoot { root: 1 },
|
||||
InsertAfter { n: 1, root: 3 },
|
||||
// remove 7
|
||||
|
||||
// create 9 and insert before 6
|
||||
CreateElement { root: 4, tag: "div" },
|
||||
InsertBefore { n: 1, root: 3 },
|
||||
// create 0 and insert before 5
|
||||
CreateElement { root: 5, tag: "div" },
|
||||
InsertBefore { n: 1, root: 2 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controlled_keyed_diffing_out_of_order_max_test() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
[0, 1, 2, 3, 4].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
[3, 0, 1, 10, 2].iter().map(|f| {
|
||||
rsx! { div { key: "{f}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (_, changes) = dom.diff_lazynodes(left, right);
|
||||
println!("{:#?}", &changes);
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
Remove { root: 5 },
|
||||
CreateElement { root: 5, tag: "div" },
|
||||
InsertBefore { n: 1, root: 3 },
|
||||
PushRoot { root: 4 },
|
||||
InsertBefore { n: 1, root: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// 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!({
|
||||
(0..10).rev().take(5).map(|i| {
|
||||
rsx! { Fragment { key: "{i}", "{i}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
(0..10).rev().take(2).map(|i| {
|
||||
rsx! { 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: 3 },
|
||||
Remove { root: 4 },
|
||||
Remove { root: 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!({
|
||||
(0..10).rev().take(5).map(|i| {
|
||||
rsx! { Fragment { "{i}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
(0..10).rev().take(2).map(|i| {
|
||||
rsx! { Fragment { "{i}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (create, changes) = dom.diff_lazynodes(left, right);
|
||||
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
// remove 5, 4, 3
|
||||
Remove { root: 3 },
|
||||
Remove { root: 4 },
|
||||
Remove { root: 5 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_nested_elements() {
|
||||
let vdom = new_dom();
|
||||
|
||||
let (_create, change) = vdom.diff_lazynodes(
|
||||
rsx! {
|
||||
div{}
|
||||
},
|
||||
rsx! {
|
||||
div{
|
||||
div{}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
PushRoot { root: 1 },
|
||||
CreateElement { root: 2, tag: "div" },
|
||||
AppendChildren { many: 1 },
|
||||
PopRoot {},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_listeners() {
|
||||
let vdom = new_dom();
|
||||
|
||||
let (_create, change) = vdom.diff_lazynodes(
|
||||
rsx! {
|
||||
div{}
|
||||
},
|
||||
rsx! {
|
||||
div{
|
||||
onkeyup: |_| {},
|
||||
onkeydown: |_| {},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
NewEventListener { event_name: "keyup", scope: ScopeId(0), root: 1 },
|
||||
NewEventListener { event_name: "keydown", scope: ScopeId(0), root: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_listeners() {
|
||||
let vdom = new_dom();
|
||||
|
||||
let (_create, change) = vdom.diff_lazynodes(
|
||||
rsx! {
|
||||
div{
|
||||
onkeyup: |_| {},
|
||||
onkeydown: |_| {},
|
||||
}
|
||||
},
|
||||
rsx! {
|
||||
div{}
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
RemoveEventListener { event: "keyup", root: 1 },
|
||||
RemoveEventListener { event: "keydown", root: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_listeners() {
|
||||
let vdom = new_dom();
|
||||
|
||||
let (_create, change) = vdom.diff_lazynodes(
|
||||
rsx! {
|
||||
div{
|
||||
onkeydown: |_| {},
|
||||
}
|
||||
},
|
||||
rsx! {
|
||||
div{
|
||||
onkeyup: |_| {},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
change.edits,
|
||||
[
|
||||
RemoveEventListener { root: 1, event: "keydown" },
|
||||
NewEventListener { event_name: "keyup", scope: ScopeId(0), root: 1 }
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -1,63 +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,
|
||||
[
|
||||
CreateElement { tag: "div", root: 1 },
|
||||
CreateTextNode { text: "Hello, world!", root: 2 },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
let edits = dom.hard_diff(ScopeId(0));
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[CreatePlaceholder { root: 3 }, ReplaceWith { root: 1, m: 1 },],
|
||||
);
|
||||
|
||||
let edits = dom.hard_diff(ScopeId(0));
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
CreateElement { tag: "div", root: 1 }, // keys get reused
|
||||
CreateTextNode { text: "Hello, world!", root: 2 }, // keys get reused
|
||||
AppendChildren { many: 1 },
|
||||
ReplaceWith { root: 3, m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
#![allow(unused, non_upper_case_globals)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
//! Tests for the lifecycle of components.
|
||||
use dioxus::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! { 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! {
|
||||
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 { tag: "div", root: 1 },
|
||||
NewEventListener { event_name: "click", scope: ScopeId(0), root: 1 },
|
||||
CreateElement { tag: "div", root: 2 },
|
||||
CreateTextNode { text: "nested", root: 3 },
|
||||
AppendChildren { many: 1 },
|
||||
CreateTextNode { text: "Click me!", root: 4 },
|
||||
AppendChildren { many: 2 },
|
||||
AppendChildren { many: 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!("Text0"),
|
||||
2 => rsx!(div {}),
|
||||
3 => rsx!("Text2"),
|
||||
4 => rsx!(Child {}),
|
||||
5 => rsx!({ None as Option<()> }),
|
||||
6 => rsx!("text 3"),
|
||||
7 => rsx!({ (0..2).map(|f| rsx!("text {f}")) }),
|
||||
8 => rsx!(Child {}),
|
||||
_ => todo!(),
|
||||
})
|
||||
};
|
||||
|
||||
fn Child(cx: Scope) -> Element {
|
||||
println!("Running child");
|
||||
cx.render(rsx! {
|
||||
h1 {}
|
||||
})
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let edits = dom.rebuild();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
CreateTextNode { text: "Text0", root: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
dom.hard_diff(ScopeId(0)).edits,
|
||||
[
|
||||
CreateElement { tag: "div", root: 2 },
|
||||
ReplaceWith { root: 1, m: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
dom.hard_diff(ScopeId(0)).edits,
|
||||
[
|
||||
CreateTextNode { text: "Text2", root: 1 },
|
||||
ReplaceWith { root: 2, m: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
// child {}
|
||||
assert_eq!(
|
||||
dom.hard_diff(ScopeId(0)).edits,
|
||||
[
|
||||
CreateElement { tag: "h1", root: 2 },
|
||||
ReplaceWith { root: 1, m: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
// placeholder
|
||||
assert_eq!(
|
||||
dom.hard_diff(ScopeId(0)).edits,
|
||||
[CreatePlaceholder { root: 1 }, ReplaceWith { root: 2, m: 1 },]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
dom.hard_diff(ScopeId(0)).edits,
|
||||
[
|
||||
CreateTextNode { text: "text 3", root: 2 },
|
||||
ReplaceWith { root: 1, m: 1 },
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
dom.hard_diff(ScopeId(0)).edits,
|
||||
[
|
||||
CreateTextNode { text: "text 0", root: 1 },
|
||||
CreateTextNode { text: "text 1", root: 3 },
|
||||
ReplaceWith { root: 2, m: 2 },
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
dom.hard_diff(ScopeId(0)).edits,
|
||||
[
|
||||
CreateElement { tag: "h1", root: 2 },
|
||||
ReplaceWith { root: 1, m: 1 },
|
||||
Remove { root: 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!(
|
||||
div {
|
||||
NavBar {}
|
||||
Dashboard {}
|
||||
}
|
||||
),
|
||||
1 => rsx!(
|
||||
div {
|
||||
NavBar {}
|
||||
Results {}
|
||||
}
|
||||
),
|
||||
2 => rsx!(
|
||||
div {
|
||||
NavBar {}
|
||||
Dashboard {}
|
||||
}
|
||||
),
|
||||
3 => rsx!(
|
||||
div {
|
||||
NavBar {}
|
||||
Results {}
|
||||
}
|
||||
),
|
||||
4 => rsx!(
|
||||
div {
|
||||
NavBar {}
|
||||
Dashboard {}
|
||||
}
|
||||
),
|
||||
_ => rsx!("blah"),
|
||||
})
|
||||
};
|
||||
|
||||
static NavBar: Component = |cx| {
|
||||
println!("running navbar");
|
||||
cx.render(rsx! {
|
||||
h1 {
|
||||
"NavBar"
|
||||
{(0..3).map(|f| rsx!(NavLink {}))}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
static NavLink: Component = |cx| {
|
||||
println!("running navlink");
|
||||
cx.render(rsx! {
|
||||
h1 {
|
||||
"NavLink"
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
static Dashboard: Component = |cx| {
|
||||
println!("running dashboard");
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
"dashboard"
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
static Results: Component = |cx| {
|
||||
println!("running results");
|
||||
cx.render(rsx! {
|
||||
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,412 +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]
|
||||
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,100 +0,0 @@
|
||||
#![allow(unused, non_upper_case_globals)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
//! 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::prelude::*;
|
||||
|
||||
fn new_dom() -> VirtualDom {
|
||||
VirtualDom::new(|cx| render!("hi"))
|
||||
}
|
||||
|
||||
use dioxus_core::DomEdit::*;
|
||||
|
||||
/// Should push the text node onto the stack and modify it
|
||||
#[test]
|
||||
fn nested_passthru_creates() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
Child {
|
||||
Child {
|
||||
Child {
|
||||
div {
|
||||
"hi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
#[inline_props]
|
||||
fn Child<'a>(cx: Scope, children: Element<'a>) -> Element {
|
||||
cx.render(rsx! { children })
|
||||
};
|
||||
|
||||
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 { tag: "div", root: 1 },
|
||||
CreateTextNode { text: "hi", root: 2 },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 1 },
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/// Should push the text node onto the stack and modify it
|
||||
#[test]
|
||||
fn nested_passthru_creates_add() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
Child {
|
||||
"1"
|
||||
Child {
|
||||
"2"
|
||||
Child {
|
||||
"3"
|
||||
div {
|
||||
"hi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
#[inline_props]
|
||||
fn Child<'a>(cx: Scope, children: Element<'a>) -> Element {
|
||||
cx.render(rsx! {
|
||||
children
|
||||
})
|
||||
};
|
||||
|
||||
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,
|
||||
[
|
||||
CreateTextNode { text: "1", root: 1 },
|
||||
CreateTextNode { text: "2", root: 2 },
|
||||
CreateTextNode { text: "3", root: 3 },
|
||||
CreateElement { tag: "div", root: 4 },
|
||||
CreateTextNode { text: "hi", root: 5 },
|
||||
AppendChildren { many: 1 },
|
||||
AppendChildren { many: 4 },
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
//! Tests for the scheduler.
|
||||
//!
|
||||
//! TODO
|
||||
//! - priority lanes
|
||||
//! - periodic checking
|
||||
//!
|
||||
@@ -1,126 +0,0 @@
|
||||
#![allow(unused, non_upper_case_globals, non_snake_case)]
|
||||
|
||||
use dioxus::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!(Child {}))
|
||||
};
|
||||
|
||||
static Child: Component = |cx| {
|
||||
let shared = cx.consume_context::<Rc<MySharedState>>()?;
|
||||
cx.render(rsx!("Hello, {shared.0}"))
|
||||
};
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let Mutations { edits, .. } = dom.rebuild();
|
||||
|
||||
assert_eq!(
|
||||
edits,
|
||||
[
|
||||
CreateTextNode { root: 1, text: "Hello, world!" },
|
||||
AppendChildren { many: 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!(
|
||||
Child1 {
|
||||
Child1 { }
|
||||
Child2 { }
|
||||
}
|
||||
),
|
||||
_ => rsx!(
|
||||
Child2 {
|
||||
Child2 { }
|
||||
Child2 { }
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
cx.render(rsx!(
|
||||
Router {
|
||||
div { child }
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn Router<'a>(cx: Scope, children: Element<'a>) -> Element<'a> {
|
||||
cx.render(rsx!(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! {
|
||||
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! {
|
||||
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 +0,0 @@
|
||||
fn main() {}
|
||||
@@ -1,95 +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]
|
||||
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: 1, tag: "h1" },
|
||||
CreateTextNode { root: 2, text: "hello" },
|
||||
AppendChildren { many: 1 },
|
||||
CreateElement { root: 3, tag: "span" },
|
||||
CreateTextNode { root: 4, text: "a" },
|
||||
AppendChildren { many: 1 },
|
||||
CreatePlaceholder { root: 5 },
|
||||
AppendChildren { many: 3 },
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ From anywhere in our app, we can read our the value of our atom:
|
||||
|
||||
```rust, ignore
|
||||
fn NameCard(cx: Scope) -> Element {
|
||||
let name = use_read(&cx, NAME);
|
||||
let name = use_read(cx, NAME);
|
||||
cx.render(rsx!{ h1 { "Hello, {name}"} })
|
||||
}
|
||||
```
|
||||
@@ -53,7 +53,7 @@ We can also set the value of our atom, also from anywhere in our app:
|
||||
|
||||
```rust, ignore
|
||||
fn NameCard(cx: Scope) -> Element {
|
||||
let set_name = use_set(&cx, NAME);
|
||||
let set_name = use_set(cx, NAME);
|
||||
cx.render(rsx!{
|
||||
button {
|
||||
onclick: move |_| set_name("Fermi"),
|
||||
@@ -77,7 +77,7 @@ fermi = { git = "https://github.com/dioxuslabs/dioxus" }
|
||||
## Running examples
|
||||
|
||||
The examples here use Dioxus Desktop to showcase their functionality. To run an example, use
|
||||
```
|
||||
```sh
|
||||
$ cargo run --example fermi
|
||||
```
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::AtomRoot;
|
||||
use dioxus_core::ScopeState;
|
||||
use std::rc::Rc;
|
||||
|
||||
// Returns the atom root, initiaizing it at the root of the app if it does not exist.
|
||||
pub fn use_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
|
||||
cx.use_hook(|| match cx.consume_context::<Rc<AtomRoot>>() {
|
||||
Some(root) => root,
|
||||
None => cx.provide_root_context(Rc::new(AtomRoot::new(cx.schedule_update_any()))),
|
||||
None => panic!("No atom root found in context. Did you forget place an AtomRoot component at the top of your app?"),
|
||||
})
|
||||
}
|
||||
@@ -19,7 +19,7 @@ use std::{
|
||||
/// static COUNT: Atom<u32> = |_| 0;
|
||||
///
|
||||
/// fn Example(cx: Scope) -> Element {
|
||||
/// let mut count = use_atom_state(&cx, COUNT);
|
||||
/// let mut count = use_atom_state(cx, COUNT);
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// div {
|
||||
@@ -74,7 +74,7 @@ impl<T: 'static> AtomState<T> {
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let count = use_state(&cx, || 0);
|
||||
/// let count = use_state(cx, || 0);
|
||||
/// cx.spawn({
|
||||
/// let set_count = count.to_owned();
|
||||
/// async move {
|
||||
@@ -101,7 +101,7 @@ impl<T: 'static> AtomState<T> {
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let value = use_state(&cx, || 0);
|
||||
/// let value = use_state(cx, || 0);
|
||||
///
|
||||
/// rsx!{
|
||||
/// Component {
|
||||
@@ -128,7 +128,7 @@ impl<T: 'static> AtomState<T> {
|
||||
/// # use dioxus_core::prelude::*;
|
||||
/// # use dioxus_hooks::*;
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let value = use_state(&cx, || 0);
|
||||
/// let value = use_state(cx, || 0);
|
||||
///
|
||||
/// // to increment the value
|
||||
/// value.modify(|v| v + 1);
|
||||
@@ -168,7 +168,7 @@ impl<T: 'static> AtomState<T> {
|
||||
/// # use dioxus_core::prelude::*;
|
||||
/// # use dioxus_hooks::*;
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let value = use_state(&cx, || 0);
|
||||
/// let value = use_state(cx, || 0);
|
||||
///
|
||||
/// let as_rc = value.get();
|
||||
/// assert_eq!(as_rc.as_ref(), &0);
|
||||
@@ -190,7 +190,7 @@ impl<T: 'static> AtomState<T> {
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let count = use_state(&cx, || 0);
|
||||
/// let count = use_state(cx, || 0);
|
||||
/// cx.spawn({
|
||||
/// let count = count.to_owned();
|
||||
/// async move {
|
||||
@@ -220,7 +220,7 @@ impl<T: Clone> AtomState<T> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// let val = use_state(&cx, || 0);
|
||||
/// let val = use_state(cx, || 0);
|
||||
///
|
||||
/// val.with_mut(|v| *v = 1);
|
||||
/// ```
|
||||
@@ -243,7 +243,7 @@ impl<T: Clone> AtomState<T> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// let val = use_state(&cx, || 0);
|
||||
/// let val = use_state(cx, || 0);
|
||||
///
|
||||
/// *val.make_mut() += 1;
|
||||
/// ```
|
||||
|
||||
@@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../../packages/core", version = "^0.2.1" }
|
||||
futures-channel = "0.3.21"
|
||||
log = { version = "0.4" }
|
||||
log = "0.4"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -15,8 +15,8 @@ You can always use it "normally" with the `split` method:
|
||||
|
||||
```rust
|
||||
// Rusty-smart-pointer usage:
|
||||
let value = use_state(&cx, || 10);
|
||||
let value = use_state(cx, || 10);
|
||||
|
||||
// "Classic" usage:
|
||||
let (value, set_value) = use_state(&cx, || 0).split();
|
||||
let (value, set_value) = use_state(cx, || 0).split();
|
||||
```
|
||||
@@ -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};
|
||||
@@ -19,5 +42,5 @@ pub use usefuture::*;
|
||||
mod useeffect;
|
||||
pub use useeffect::*;
|
||||
|
||||
// mod usesuspense;
|
||||
// pub use usesuspense::*;
|
||||
mod usecallback;
|
||||
pub use usecallback::*;
|
||||
@@ -60,7 +60,7 @@ impl<T> ProvidedStateInner<T> {
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn use_context<T: 'static>(cx: &ScopeState) -> Option<UseSharedState<T>> {
|
||||
pub fn use_shared_state<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>>();
|
||||
@@ -165,20 +165,14 @@ where
|
||||
}
|
||||
|
||||
/// Provide some state for components down the hierarchy to consume without having to drill props.
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn use_context_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) {
|
||||
pub fn use_shared_state_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) {
|
||||
cx.use_hook(|| {
|
||||
let state: ProvidedState<T> = Rc::new(RefCell::new(ProvidedStateInner {
|
||||
value: Rc::new(RefCell::new(f())),
|
||||
notify_any: cx.schedule_update_any(),
|
||||
consumers: HashSet::new(),
|
||||
}));
|
||||
cx.provide_context(state)
|
||||
|
||||
cx.provide_context(state);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use dioxus_core::ScopeState;
|
||||
use std::future::Future;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! use_callback {
|
||||
// ($cx:ident, || || $($rest:tt)*) => { use_callback( $cx, (), |_| $($rest)* ) };
|
||||
// ($cx:ident, || || $($rest:tt)*) => { use_callback( $cx, (), |_| $($rest)* ) };
|
||||
($cx:ident, || $($rest:tt)*) => {
|
||||
use_callback(
|
||||
$cx,
|
||||
move || $($rest)*
|
||||
)
|
||||
};
|
||||
($cx:ident, |$($args:tt),* | $($rest:tt)*) => {
|
||||
use_callback(
|
||||
$cx,
|
||||
move || $($rest)*
|
||||
)
|
||||
};
|
||||
($cx:ident, $($rest:tt)*) => {
|
||||
use_callback(
|
||||
$cx,
|
||||
move || $($rest)*
|
||||
)
|
||||
};
|
||||
}
|
||||
pub fn use_callback<T, R, F>(cx: &ScopeState, make: impl FnOnce() -> R) -> impl FnMut(T) + '_
|
||||
where
|
||||
R: FnMut(T) -> F + 'static,
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
let mut hook = make();
|
||||
|
||||
move |evt| cx.spawn(hook(evt))
|
||||
}
|
||||
|
||||
fn _it_works(cx: &ScopeState) {
|
||||
let _p = use_callback(cx, || {
|
||||
|()| async {
|
||||
//
|
||||
}
|
||||
});
|
||||
|
||||
// let p = use_callback!(cx, || |evt| async {
|
||||
// //
|
||||
// });
|
||||
}
|
||||
@@ -16,7 +16,7 @@ This is moderately efficient because the fields of the map are moved, but the da
|
||||
However, if you used similar approach with Dioxus:
|
||||
|
||||
```rust
|
||||
let (map, set_map) = use_state(&cx, || HashMap::new());
|
||||
let (map, set_map) = use_state(cx, || HashMap::new());
|
||||
set_map({
|
||||
let mut newmap = map.clone();
|
||||
newmap.set(key, value);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use dioxus_core::ScopeState;
|
||||
|
||||
/// Consume some context in the tree, providing a sharable handle to the value
|
||||
///
|
||||
/// Does not regenerate the value if the value is changed at the parent.
|
||||
pub fn use_context<T: 'static + Clone>(cx: &ScopeState) -> Option<&T> {
|
||||
cx.use_hook(|| cx.consume_context::<T>()).as_ref()
|
||||
}
|
||||
|
||||
/// 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 + Clone>(cx: &ScopeState, f: impl FnOnce() -> T) -> &T {
|
||||
cx.use_hook(|| {
|
||||
let val = f();
|
||||
cx.provide_context(val.clone());
|
||||
val
|
||||
})
|
||||
}
|
||||
@@ -42,7 +42,7 @@ use std::future::Future;
|
||||
/// Stop,
|
||||
/// }
|
||||
///
|
||||
/// let chat_client = use_coroutine(&cx, |rx: UnboundedReceiver<Action>| async move {
|
||||
/// let chat_client = use_coroutine(cx, |rx: UnboundedReceiver<Action>| async move {
|
||||
/// while let Some(action) = rx.next().await {
|
||||
/// match action {
|
||||
/// Action::Start => {}
|
||||
@@ -59,7 +59,7 @@ 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,
|
||||
@@ -68,24 +68,25 @@ where
|
||||
cx.use_hook(|| {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
let task = cx.push_future(init(rx));
|
||||
cx.provide_context(CoroutineHandle { tx, task })
|
||||
cx.provide_context(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>>())
|
||||
pub fn use_coroutine_handle<M: 'static>(cx: &ScopeState) -> Option<&Coroutine<M>> {
|
||||
cx.use_hook(|| cx.consume_context::<Coroutine<M>>())
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
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 +95,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,22 +113,22 @@ 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 {
|
||||
let task = use_coroutine(&cx, |mut rx: UnboundedReceiver<i32>| async move {
|
||||
let task = use_coroutine(cx, |mut rx: UnboundedReceiver<i32>| async move {
|
||||
while let Some(msg) = rx.next().await {
|
||||
println!("got message: {}", msg);
|
||||
}
|
||||
});
|
||||
|
||||
let task2 = use_coroutine(&cx, view_task);
|
||||
let task2 = use_coroutine(cx, view_task);
|
||||
|
||||
let task3 = use_coroutine(&cx, |rx| complex_task(rx, 10));
|
||||
let task3 = use_coroutine(cx, |rx| complex_task(rx, 10));
|
||||
|
||||
None
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn view_task(mut rx: UnboundedReceiver<i32>) {
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::UseFutureDep;
|
||||
///
|
||||
/// #[inline_props]
|
||||
/// fn app(cx: Scope, name: &str) -> Element {
|
||||
/// use_effect(&cx, (name,), |(name,)| async move {
|
||||
/// use_effect(cx, (name,), |(name,)| async move {
|
||||
/// set_title(name);
|
||||
/// }))
|
||||
/// }
|
||||
@@ -72,21 +72,21 @@ mod tests {
|
||||
|
||||
fn app(cx: Scope<MyProps>) -> Element {
|
||||
// should only ever run once
|
||||
use_effect(&cx, (), |_| async move {
|
||||
use_effect(cx, (), |_| async move {
|
||||
//
|
||||
});
|
||||
|
||||
// runs when a is changed
|
||||
use_effect(&cx, (&cx.props.a,), |(a,)| async move {
|
||||
use_effect(cx, (&cx.props.a,), |(a,)| async move {
|
||||
//
|
||||
});
|
||||
|
||||
// runs when a or b is changed
|
||||
use_effect(&cx, (&cx.props.a, &cx.props.b), |(a, b)| async move {
|
||||
use_effect(cx, (&cx.props.a, &cx.props.b), |(a, b)| async move {
|
||||
//
|
||||
});
|
||||
|
||||
None
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
-38
@@ -1,6 +1,12 @@
|
||||
#![allow(missing_docs)]
|
||||
use dioxus_core::{ScopeState, TaskId};
|
||||
use std::{any::Any, cell::Cell, future::Future, rc::Rc, sync::Arc};
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
future::{Future, IntoFuture},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A future that resolves to a value.
|
||||
///
|
||||
@@ -28,16 +34,13 @@ where
|
||||
let state = cx.use_hook(move || UseFuture {
|
||||
update: cx.schedule_update(),
|
||||
needs_regen: Cell::new(true),
|
||||
slot: Rc::new(Cell::new(None)),
|
||||
value: None,
|
||||
values: Default::default(),
|
||||
task: Cell::new(None),
|
||||
dependencies: Vec::new(),
|
||||
waker: Default::default(),
|
||||
});
|
||||
|
||||
if let Some(value) = state.slot.take() {
|
||||
state.value = Some(value);
|
||||
state.task.set(None);
|
||||
}
|
||||
*state.waker.borrow_mut() = None;
|
||||
|
||||
if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() {
|
||||
// We don't need regen anymore
|
||||
@@ -47,8 +50,9 @@ where
|
||||
let fut = future(dependencies.out());
|
||||
|
||||
// Clone in our cells
|
||||
let slot = state.slot.clone();
|
||||
let values = state.values.clone();
|
||||
let schedule_update = state.update.clone();
|
||||
let waker = state.waker.clone();
|
||||
|
||||
// Cancel the current future
|
||||
if let Some(current) = state.task.take() {
|
||||
@@ -57,8 +61,13 @@ where
|
||||
|
||||
state.task.set(Some(cx.push_future(async move {
|
||||
let res = fut.await;
|
||||
slot.set(Some(res));
|
||||
schedule_update();
|
||||
values.borrow_mut().push(Box::leak(Box::new(res)));
|
||||
|
||||
// if there's a waker, we dont re-render the component. Instead we just progress that future
|
||||
match waker.borrow().as_ref() {
|
||||
Some(waker) => waker.wake_by_ref(),
|
||||
None => schedule_update(),
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
@@ -74,10 +83,10 @@ pub enum FutureState<'a, T> {
|
||||
pub struct UseFuture<T> {
|
||||
update: Arc<dyn Fn()>,
|
||||
needs_regen: Cell<bool>,
|
||||
value: Option<T>,
|
||||
slot: Rc<Cell<Option<T>>>,
|
||||
task: Cell<Option<TaskId>>,
|
||||
dependencies: Vec<Box<dyn Any>>,
|
||||
waker: Rc<RefCell<Option<std::task::Waker>>>,
|
||||
values: Rc<RefCell<Vec<*mut T>>>,
|
||||
}
|
||||
|
||||
pub enum UseFutureState<'a, T> {
|
||||
@@ -105,22 +114,28 @@ impl<T> UseFuture<T> {
|
||||
|
||||
// clears the value in the future slot without starting the future over
|
||||
pub fn clear(&self) -> Option<T> {
|
||||
(self.update)();
|
||||
self.slot.replace(None)
|
||||
todo!()
|
||||
// (self.update)();
|
||||
// self.slot.replace(None)
|
||||
}
|
||||
|
||||
// Manually set the value in the future slot without starting the future over
|
||||
pub fn set(&self, new_value: T) {
|
||||
self.slot.set(Some(new_value));
|
||||
self.needs_regen.set(true);
|
||||
(self.update)();
|
||||
pub fn set(&self, _new_value: T) {
|
||||
// self.slot.set(Some(new_value));
|
||||
// self.needs_regen.set(true);
|
||||
// (self.update)();
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Return any value, even old values if the future has not yet resolved.
|
||||
///
|
||||
/// If the future has never completed, the returned value will be `None`.
|
||||
pub fn value(&self) -> Option<&T> {
|
||||
self.value.as_ref()
|
||||
self.values
|
||||
.borrow_mut()
|
||||
.last()
|
||||
.cloned()
|
||||
.map(|x| unsafe { &*x })
|
||||
}
|
||||
|
||||
/// Get the ID of the future in Dioxus' internal scheduler
|
||||
@@ -130,18 +145,48 @@ impl<T> UseFuture<T> {
|
||||
|
||||
/// Get the current stateof the future.
|
||||
pub fn state(&self) -> UseFutureState<T> {
|
||||
match (&self.task.get(), &self.value) {
|
||||
// If we have a task and an existing value, we're reloading
|
||||
(Some(_), Some(val)) => UseFutureState::Reloading(val),
|
||||
todo!()
|
||||
// match (&self.task.get(), &self.value) {
|
||||
// // If we have a task and an existing value, we're reloading
|
||||
// (Some(_), Some(val)) => UseFutureState::Reloading(val),
|
||||
|
||||
// no task, but value - we're done
|
||||
(None, Some(val)) => UseFutureState::Complete(val),
|
||||
// // no task, but value - we're done
|
||||
// (None, Some(val)) => UseFutureState::Complete(val),
|
||||
|
||||
// no task, no value - something's wrong? return pending
|
||||
(None, None) => UseFutureState::Pending,
|
||||
// // no task, no value - something's wrong? return pending
|
||||
// (None, None) => UseFutureState::Pending,
|
||||
|
||||
// Task, no value - we're still pending
|
||||
(Some(_), None) => UseFutureState::Pending,
|
||||
// // Task, no value - we're still pending
|
||||
// (Some(_), None) => UseFutureState::Pending,
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoFuture for &'a UseFuture<T> {
|
||||
type Output = &'a T;
|
||||
type IntoFuture = UseFutureAwait<'a, T>;
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
UseFutureAwait { hook: self }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UseFutureAwait<'a, T> {
|
||||
hook: &'a UseFuture<T>,
|
||||
}
|
||||
|
||||
impl<'a, T> Future for UseFutureAwait<'a, T> {
|
||||
type Output = &'a T;
|
||||
|
||||
fn poll(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Self::Output> {
|
||||
match self.hook.values.borrow_mut().last().cloned() {
|
||||
Some(value) => std::task::Poll::Ready(unsafe { &*value }),
|
||||
None => {
|
||||
self.hook.waker.replace(Some(cx.waker().clone()));
|
||||
std::task::Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,6 +284,19 @@ impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f,);
|
||||
impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f, G = g,);
|
||||
impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f, G = g, H = h,);
|
||||
|
||||
/// A helper macro that merges uses the closure syntax to elaborate the dependency array
|
||||
#[macro_export]
|
||||
macro_rules! use_future {
|
||||
($cx:ident, || $($rest:tt)*) => { use_future( $cx, (), |_| $($rest)* ) };
|
||||
($cx:ident, | $($args:tt),* | $($rest:tt)*) => {
|
||||
use_future(
|
||||
$cx,
|
||||
($($args),*),
|
||||
|($($args),*)| $($rest)*
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -256,23 +314,33 @@ mod tests {
|
||||
e: i32,
|
||||
}
|
||||
|
||||
fn app(cx: Scope<MyProps>) -> Element {
|
||||
async fn app(cx: Scope<'_, MyProps>) -> Element {
|
||||
// should only ever run once
|
||||
let fut = use_future(&cx, (), |_| async move {
|
||||
//
|
||||
});
|
||||
let fut = use_future(cx, (), |_| async move {});
|
||||
|
||||
// runs when a is changed
|
||||
let fut = use_future(&cx, (&cx.props.a,), |(a,)| async move {
|
||||
//
|
||||
});
|
||||
let fut = use_future(cx, (&cx.props.a,), |(a,)| async move {});
|
||||
|
||||
// runs when a or b is changed
|
||||
let fut = use_future(&cx, (&cx.props.a, &cx.props.b), |(a, b)| async move {
|
||||
//
|
||||
let fut = use_future(cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { 123 });
|
||||
|
||||
let a = use_future!(cx, || async move {
|
||||
// do the thing!
|
||||
});
|
||||
|
||||
None
|
||||
let b = &123;
|
||||
let c = &123;
|
||||
|
||||
let a = use_future!(cx, |b, c| async move {
|
||||
let a = b + c;
|
||||
let blah = "asd";
|
||||
});
|
||||
|
||||
let g2 = a.await;
|
||||
|
||||
let g = fut.await;
|
||||
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ use std::{
|
||||
///
|
||||
/// ```ignore
|
||||
/// const Example: Component = |cx| {
|
||||
/// let count = use_state(&cx, || 0);
|
||||
/// let count = use_state(cx, || 0);
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// div {
|
||||
@@ -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();
|
||||
@@ -92,7 +92,7 @@ impl<T: 'static> UseState<T> {
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let count = use_state(&cx, || 0);
|
||||
/// let count = use_state(cx, || 0);
|
||||
/// cx.spawn({
|
||||
/// let set_count = count.to_owned();
|
||||
/// async move {
|
||||
@@ -119,7 +119,7 @@ impl<T: 'static> UseState<T> {
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let value = use_state(&cx, || 0);
|
||||
/// let value = use_state(cx, || 0);
|
||||
///
|
||||
/// rsx!{
|
||||
/// Component {
|
||||
@@ -144,7 +144,7 @@ impl<T: 'static> UseState<T> {
|
||||
/// # use dioxus_core::prelude::*;
|
||||
/// # use dioxus_hooks::*;
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let value = use_state(&cx, || 0);
|
||||
/// let value = use_state(cx, || 0);
|
||||
///
|
||||
/// // to increment the value
|
||||
/// value.modify(|v| v + 1);
|
||||
@@ -185,7 +185,7 @@ impl<T: 'static> UseState<T> {
|
||||
/// # use dioxus_core::prelude::*;
|
||||
/// # use dioxus_hooks::*;
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let value = use_state(&cx, || 0);
|
||||
/// let value = use_state(cx, || 0);
|
||||
///
|
||||
/// let as_rc = value.get();
|
||||
/// assert_eq!(as_rc.as_ref(), &0);
|
||||
@@ -207,7 +207,7 @@ impl<T: 'static> UseState<T> {
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let count = use_state(&cx, || 0);
|
||||
/// let count = use_state(cx, || 0);
|
||||
/// cx.spawn({
|
||||
/// let count = count.to_owned();
|
||||
/// async move {
|
||||
@@ -237,7 +237,7 @@ impl<T: Clone> UseState<T> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let val = use_state(&cx, || 0);
|
||||
/// let val = use_state(cx, || 0);
|
||||
///
|
||||
/// val.with_mut(|v| *v = 1);
|
||||
/// ```
|
||||
@@ -269,7 +269,7 @@ impl<T: Clone> UseState<T> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let val = use_state(&cx, || 0);
|
||||
/// let val = use_state(cx, || 0);
|
||||
///
|
||||
/// *val.make_mut() += 1;
|
||||
/// ```
|
||||
@@ -448,7 +448,7 @@ impl<T: Div<Output = T> + Copy> std::ops::DivAssign<T> for UseState<T> {
|
||||
fn api_makes_sense() {
|
||||
#[allow(unused)]
|
||||
fn app(cx: Scope) -> Element {
|
||||
let val = use_state(&cx, || 0);
|
||||
let val = use_state(cx, || 0);
|
||||
|
||||
val.set(0);
|
||||
val.modify(|v| v + 1);
|
||||
@@ -466,12 +466,14 @@ fn api_makes_sense() {
|
||||
}
|
||||
|
||||
cx.spawn({
|
||||
dioxus_core::to_owned![val];
|
||||
to_owned![val];
|
||||
async move {
|
||||
val.modify(|f| f + 1);
|
||||
}
|
||||
});
|
||||
|
||||
cx.render(LazyNodes::new(|f| f.static_text("asd")))
|
||||
// cx.render(LazyNodes::new(|f| f.static_text("asd")))
|
||||
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
Loaded 100 of 336 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user