Merge branch 'master' into master
This commit is contained in:
281 files changed
+15193
-3917
No files matched your search
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-core-macro"
|
||||
version = "0.1.7"
|
||||
version = "0.2.0"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
description = "Core macro for Dioxus Virtual DOM"
|
||||
@@ -15,8 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
once_cell = "1.8"
|
||||
proc-macro-error = "1.0.4"
|
||||
proc-macro-error = "1"
|
||||
proc-macro2 = { version = "1.0.6" }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
||||
|
||||
@@ -1,443 +0,0 @@
|
||||
//!
|
||||
//! TODO:
|
||||
//! - [ ] Support for VComponents
|
||||
//! - [ ] Support for inline format in text
|
||||
//! - [ ] Support for expressions in attribute positions
|
||||
//! - [ ] Support for iterators
|
||||
//! - [ ] support for inline html!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
|
||||
use {
|
||||
proc_macro2::TokenStream as TokenStream2,
|
||||
quote::{quote, ToTokens, TokenStreamExt},
|
||||
syn::{
|
||||
ext::IdentExt,
|
||||
parse::{Parse, ParseStream},
|
||||
token, Error, Expr, ExprClosure, Ident, LitStr, Result, Token,
|
||||
},
|
||||
};
|
||||
|
||||
// ==============================================
|
||||
// Parse any stream coming from the html! macro
|
||||
// ==============================================
|
||||
pub struct HtmlRender {
|
||||
kind: NodeOrList,
|
||||
}
|
||||
|
||||
impl Parse for HtmlRender {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
if input.peek(LitStr) {
|
||||
return input.parse::<LitStr>()?.parse::<HtmlRender>();
|
||||
}
|
||||
|
||||
// let __cx: Ident = s.parse()?;
|
||||
// s.parse::<Token![,]>()?;
|
||||
// if elements are in an array, return a bumpalo::collections::Vec rather than a Node.
|
||||
let kind = if input.peek(token::Bracket) {
|
||||
let nodes_toks;
|
||||
syn::bracketed!(nodes_toks in input);
|
||||
let mut nodes: Vec<MaybeExpr<Node>> = vec![nodes_toks.parse()?];
|
||||
while nodes_toks.peek(Token![,]) {
|
||||
nodes_toks.parse::<Token![,]>()?;
|
||||
nodes.push(nodes_toks.parse()?);
|
||||
}
|
||||
NodeOrList::List(NodeList(nodes))
|
||||
} else {
|
||||
NodeOrList::Node(input.parse()?)
|
||||
};
|
||||
Ok(HtmlRender { kind })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlRender {
|
||||
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
|
||||
let new_toks = ToToksCtx::new(&self.kind).to_token_stream();
|
||||
|
||||
// create a lazy tree that accepts a bump allocator
|
||||
let final_tokens = quote! {
|
||||
dioxus::prelude::LazyNodes::new(move |__cx| {
|
||||
let bump = __cx.bump();
|
||||
|
||||
#new_toks
|
||||
})
|
||||
};
|
||||
|
||||
final_tokens.to_tokens(out_tokens);
|
||||
}
|
||||
}
|
||||
|
||||
/// =============================================
|
||||
/// Parse any child as a node or list of nodes
|
||||
/// =============================================
|
||||
/// - [ ] Allow iterators
|
||||
///
|
||||
///
|
||||
enum NodeOrList {
|
||||
Node(Node),
|
||||
List(NodeList),
|
||||
}
|
||||
|
||||
impl ToTokens for ToToksCtx<&NodeOrList> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match self.inner {
|
||||
NodeOrList::Node(node) => self.recurse(node).to_tokens(tokens),
|
||||
NodeOrList::List(list) => self.recurse(list).to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NodeList(Vec<MaybeExpr<Node>>);
|
||||
|
||||
impl ToTokens for ToToksCtx<&NodeList> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let nodes = self.inner.0.iter().map(|node| self.recurse(node));
|
||||
tokens.append_all(quote! {
|
||||
dioxus::bumpalo::vec![in bump;
|
||||
#(#nodes),*
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enum Node {
|
||||
Element(Element),
|
||||
Text(TextNode),
|
||||
}
|
||||
|
||||
impl ToTokens for ToToksCtx<&Node> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match &self.inner {
|
||||
Node::Element(el) => self.recurse(el).to_tokens(tokens),
|
||||
Node::Text(txt) => self.recurse(txt).to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn _peek(s: ParseStream) -> bool {
|
||||
(s.peek(Token![<]) && !s.peek2(Token![/])) || s.peek(token::Brace) || s.peek(LitStr)
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Node {
|
||||
fn parse(s: ParseStream) -> Result<Self> {
|
||||
Ok(if s.peek(Token![<]) {
|
||||
Node::Element(s.parse()?)
|
||||
} else {
|
||||
Node::Text(s.parse()?)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// =======================================
|
||||
/// Parse the VNode::Element type
|
||||
/// =======================================
|
||||
/// - [ ] Allow VComponent
|
||||
///
|
||||
///
|
||||
struct Element {
|
||||
name: Ident,
|
||||
attrs: Vec<Attr>,
|
||||
children: MaybeExpr<Vec<Node>>,
|
||||
}
|
||||
|
||||
impl ToTokens for ToToksCtx<&Element> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
// let __cx = self.__cx;
|
||||
let name = &self.inner.name;
|
||||
// let name = &self.inner.name.to_string();
|
||||
tokens.append_all(quote! {
|
||||
__cx.element(dioxus_elements::#name)
|
||||
// dioxus::builder::ElementBuilder::new( #name)
|
||||
});
|
||||
for attr in self.inner.attrs.iter() {
|
||||
self.recurse(attr).to_tokens(tokens);
|
||||
}
|
||||
|
||||
// if is_valid_svg_tag(&name.to_string()) {
|
||||
// tokens.append_all(quote! {
|
||||
// .namespace(Some("http://www.w3.org/2000/svg"))
|
||||
// });
|
||||
// }
|
||||
|
||||
match &self.inner.children {
|
||||
MaybeExpr::Expr(expr) => tokens.append_all(quote! {
|
||||
.children(#expr)
|
||||
}),
|
||||
MaybeExpr::Literal(nodes) => {
|
||||
let mut children = nodes.iter();
|
||||
if let Some(child) = children.next() {
|
||||
let mut inner_toks = TokenStream2::new();
|
||||
self.recurse(child).to_tokens(&mut inner_toks);
|
||||
for child in children {
|
||||
quote!(,).to_tokens(&mut inner_toks);
|
||||
self.recurse(child).to_tokens(&mut inner_toks);
|
||||
}
|
||||
tokens.append_all(quote! {
|
||||
.children([#inner_toks])
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
tokens.append_all(quote! {
|
||||
.finish()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Element {
|
||||
fn parse(s: ParseStream) -> Result<Self> {
|
||||
s.parse::<Token![<]>()?;
|
||||
let name = Ident::parse_any(s)?;
|
||||
let mut attrs = vec![];
|
||||
let _children: Vec<Node> = vec![];
|
||||
|
||||
// keep looking for attributes
|
||||
while !s.peek(Token![>]) {
|
||||
// self-closing
|
||||
if s.peek(Token![/]) {
|
||||
s.parse::<Token![/]>()?;
|
||||
s.parse::<Token![>]>()?;
|
||||
return Ok(Self {
|
||||
name,
|
||||
attrs,
|
||||
children: MaybeExpr::Literal(vec![]),
|
||||
});
|
||||
}
|
||||
attrs.push(s.parse()?);
|
||||
}
|
||||
s.parse::<Token![>]>()?;
|
||||
|
||||
// Contents of an element can either be a brace (in which case we just copy verbatim), or a
|
||||
// sequence of nodes.
|
||||
let children = if s.peek(token::Brace) {
|
||||
// expr
|
||||
let content;
|
||||
syn::braced!(content in s);
|
||||
MaybeExpr::Expr(content.parse()?)
|
||||
} else {
|
||||
// nodes
|
||||
let mut children = vec![];
|
||||
while !(s.peek(Token![<]) && s.peek2(Token![/])) {
|
||||
children.push(s.parse()?);
|
||||
}
|
||||
MaybeExpr::Literal(children)
|
||||
};
|
||||
|
||||
// closing element
|
||||
s.parse::<Token![<]>()?;
|
||||
s.parse::<Token![/]>()?;
|
||||
let close = Ident::parse_any(s)?;
|
||||
if close != name {
|
||||
return Err(Error::new_spanned(
|
||||
close,
|
||||
"closing element does not match opening",
|
||||
));
|
||||
}
|
||||
s.parse::<Token![>]>()?;
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
attrs,
|
||||
children,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// =======================================
|
||||
/// Parse a VElement's Attributes
|
||||
/// =======================================
|
||||
/// - [ ] Allow expressions as attribute
|
||||
///
|
||||
///
|
||||
struct Attr {
|
||||
name: Ident,
|
||||
ty: AttrType,
|
||||
}
|
||||
|
||||
impl Parse for Attr {
|
||||
fn parse(s: ParseStream) -> Result<Self> {
|
||||
let mut name = Ident::parse_any(s)?;
|
||||
let name_str = name.to_string();
|
||||
s.parse::<Token![=]>()?;
|
||||
|
||||
// Check if this is an event handler
|
||||
// If so, parse into literal tokens
|
||||
let ty = if name_str.starts_with("on") {
|
||||
// remove the "on" bit
|
||||
name = Ident::new(name_str.trim_start_matches("on"), name.span());
|
||||
let content;
|
||||
syn::braced!(content in s);
|
||||
// AttrType::Value(content.parse()?)
|
||||
AttrType::Event(content.parse()?)
|
||||
// AttrType::Event(content.parse()?)
|
||||
} else {
|
||||
let lit_str = if name_str == "style" && s.peek(token::Brace) {
|
||||
// special-case to deal with literal styles.
|
||||
let outer;
|
||||
syn::braced!(outer in s);
|
||||
// double brace for inline style.
|
||||
// todo!("Style support not ready yet");
|
||||
|
||||
// if outer.peek(token::Brace) {
|
||||
// let inner;
|
||||
// syn::braced!(inner in outer);
|
||||
// let styles: Styles = inner.parse()?;
|
||||
// MaybeExpr::Literal(LitStr::new(&styles.to_string(), Span::call_site()))
|
||||
// } else {
|
||||
// just parse as an expression
|
||||
MaybeExpr::Expr(outer.parse()?)
|
||||
// }
|
||||
} else {
|
||||
s.parse()?
|
||||
};
|
||||
AttrType::Value(lit_str)
|
||||
};
|
||||
Ok(Attr { name, ty })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ToToksCtx<&Attr> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let name = self.inner.name.to_string();
|
||||
let _attr_stream = TokenStream2::new();
|
||||
match &self.inner.ty {
|
||||
AttrType::Value(value) => {
|
||||
let value = self.recurse(value);
|
||||
if name == "xmlns" {
|
||||
tokens.append_all(quote! {
|
||||
.namespace(Some(#value))
|
||||
});
|
||||
} else {
|
||||
tokens.append_all(quote! {
|
||||
.attr(#name, format_args_f!(#value))
|
||||
});
|
||||
}
|
||||
}
|
||||
AttrType::Event(event) => {
|
||||
tokens.append_all(quote! {
|
||||
.on(#name, #event)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AttrType {
|
||||
Value(MaybeExpr<LitStr>),
|
||||
Event(ExprClosure),
|
||||
// todo Bool(MaybeExpr<LitBool>)
|
||||
}
|
||||
|
||||
/// =======================================
|
||||
/// Parse just plain text
|
||||
/// =======================================
|
||||
/// - [ ] Perform formatting automatically
|
||||
///
|
||||
///
|
||||
struct TextNode(MaybeExpr<LitStr>);
|
||||
|
||||
impl Parse for TextNode {
|
||||
fn parse(s: ParseStream) -> Result<Self> {
|
||||
Ok(Self(s.parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ToToksCtx<&TextNode> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let mut token_stream = TokenStream2::new();
|
||||
self.recurse(&self.inner.0).to_tokens(&mut token_stream);
|
||||
tokens.append_all(quote! {
|
||||
__cx.text(format_args_f!(#token_stream))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum MaybeExpr<T> {
|
||||
Literal(T),
|
||||
Expr(Expr),
|
||||
}
|
||||
|
||||
impl<T: Parse> Parse for MaybeExpr<T> {
|
||||
fn parse(s: ParseStream) -> Result<Self> {
|
||||
if s.peek(token::Brace) {
|
||||
let content;
|
||||
syn::braced!(content in s);
|
||||
Ok(MaybeExpr::Expr(content.parse()?))
|
||||
} else {
|
||||
Ok(MaybeExpr::Literal(s.parse()?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> ToTokens for ToToksCtx<&'a MaybeExpr<T>>
|
||||
where
|
||||
T: 'a,
|
||||
ToToksCtx<&'a T>: ToTokens,
|
||||
{
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match &self.inner {
|
||||
MaybeExpr::Literal(v) => self.recurse(v).to_tokens(tokens),
|
||||
MaybeExpr::Expr(expr) => expr.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ToTokens context
|
||||
struct ToToksCtx<T> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<'a, T> ToToksCtx<T> {
|
||||
fn new(inner: T) -> Self {
|
||||
ToToksCtx { inner }
|
||||
}
|
||||
|
||||
fn recurse<U>(&self, inner: U) -> ToToksCtx<U> {
|
||||
ToToksCtx { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ToToksCtx<&LitStr> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
self.inner.to_tokens(tokens)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
fn parse(input: &str) -> super::Result<super::HtmlRender> {
|
||||
syn::parse_str(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div() {
|
||||
parse("bump, <div class=\"test\"/>").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested() {
|
||||
parse("bump, <div class=\"test\"><div />\"text\"</div>").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex() {
|
||||
parse(
|
||||
"bump,
|
||||
<section style={{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 95%;
|
||||
}} class=\"map-panel\">{contact_details}</section>
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,11 @@ use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
token, Block, FnArg, Generics, Ident, Pat, Result, ReturnType, Token, Visibility,
|
||||
*,
|
||||
};
|
||||
|
||||
pub struct InlinePropsBody {
|
||||
pub attrs: Vec<Attribute>,
|
||||
pub vis: syn::Visibility,
|
||||
pub fn_token: Token![fn],
|
||||
pub ident: Ident,
|
||||
@@ -22,6 +23,7 @@ pub struct InlinePropsBody {
|
||||
/// The custom rusty variant of parsing rsx!
|
||||
impl Parse for InlinePropsBody {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
|
||||
let vis: Visibility = input.parse()?;
|
||||
|
||||
let fn_token = input.parse()?;
|
||||
@@ -34,7 +36,7 @@ impl Parse for InlinePropsBody {
|
||||
let first_arg: FnArg = content.parse()?;
|
||||
let cx_token = {
|
||||
match first_arg {
|
||||
FnArg::Receiver(_) => panic!("first argument must not be a reciver argument"),
|
||||
FnArg::Receiver(_) => panic!("first argument must not be a receiver argument"),
|
||||
FnArg::Typed(f) => f.pat,
|
||||
}
|
||||
};
|
||||
@@ -57,6 +59,7 @@ impl Parse for InlinePropsBody {
|
||||
output,
|
||||
block,
|
||||
cx_token,
|
||||
attrs,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -72,6 +75,7 @@ impl ToTokens for InlinePropsBody {
|
||||
output,
|
||||
block,
|
||||
cx_token,
|
||||
attrs,
|
||||
..
|
||||
} = self;
|
||||
|
||||
@@ -86,26 +90,58 @@ impl ToTokens for InlinePropsBody {
|
||||
FnArg::Typed(t) => Some(&t.pat),
|
||||
});
|
||||
|
||||
let modifiers = if generics.params.is_empty() {
|
||||
quote! { #[derive(Props, PartialEq)] }
|
||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||
Some(lt)
|
||||
} else {
|
||||
quote! { #[derive(Props)] }
|
||||
None
|
||||
};
|
||||
|
||||
let lifetime = if generics.params.is_empty() {
|
||||
quote! {}
|
||||
let modifiers = if first_lifetime.is_some() {
|
||||
quote! { #[derive(Props)] }
|
||||
} else {
|
||||
quote! { 'a, }
|
||||
quote! { #[derive(Props, PartialEq)] }
|
||||
};
|
||||
|
||||
let (scope_lifetime, fn_generics, struct_generics) = if let Some(lt) = first_lifetime {
|
||||
let struct_generics: Punctuated<_, token::Comma> = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.push(parse_quote!( 'a ));
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
(
|
||||
quote! { #lt, },
|
||||
generics.clone(),
|
||||
quote! { <#struct_generics> },
|
||||
)
|
||||
} else {
|
||||
let lifetime: LifetimeDef = parse_quote! { 'a };
|
||||
|
||||
let mut fn_generics = generics.clone();
|
||||
fn_generics
|
||||
.params
|
||||
.insert(0, GenericParam::Lifetime(lifetime.clone()));
|
||||
|
||||
(quote! { #lifetime, }, fn_generics, quote! { #generics })
|
||||
};
|
||||
|
||||
out_tokens.append_all(quote! {
|
||||
#modifiers
|
||||
#[allow(non_camel_case_types)]
|
||||
#vis struct #struct_name #generics {
|
||||
#vis struct #struct_name #struct_generics {
|
||||
#(#fields),*
|
||||
}
|
||||
|
||||
#vis fn #ident #generics (#cx_token: Scope<#lifetime #struct_name #generics>) #output {
|
||||
#(#attrs)*
|
||||
#vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output {
|
||||
let #struct_name { #(#field_names),* } = &cx.props;
|
||||
#block
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ use proc_macro::TokenStream;
|
||||
use quote::ToTokens;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
pub(crate) mod ifmt;
|
||||
pub(crate) mod inlineprops;
|
||||
pub(crate) mod props;
|
||||
pub(crate) mod router;
|
||||
pub(crate) mod rsx;
|
||||
mod ifmt;
|
||||
mod inlineprops;
|
||||
mod props;
|
||||
mod rsx;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn format_args_f(input: TokenStream) -> TokenStream {
|
||||
@@ -186,36 +185,6 @@ pub fn rsx(s: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive macro used to mark an enum as Routable.
|
||||
///
|
||||
/// This macro can only be used on enums. Every varient of the macro needs to be marked
|
||||
/// with the `at` attribute to specify the URL of the route. It generates an implementation of
|
||||
/// `yew_router::Routable` trait and `const`s for the routes passed which are used with `Route`
|
||||
/// component.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use yew_router::Routable;
|
||||
/// #[derive(Debug, Clone, Copy, PartialEq, Routable)]
|
||||
/// enum Routes {
|
||||
/// #[at("/")]
|
||||
/// Home,
|
||||
/// #[at("/secure")]
|
||||
/// Secure,
|
||||
/// #[at("/profile/{id}")]
|
||||
/// Profile(u32),
|
||||
/// #[at("/404")]
|
||||
/// NotFound,
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_derive(Routable, attributes(at, not_found))]
|
||||
pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
use router::{routable_derive_impl, Routable};
|
||||
let input = parse_macro_input!(input as Routable);
|
||||
routable_derive_impl(input).into()
|
||||
}
|
||||
|
||||
/// Derive props for a component within the component definition.
|
||||
///
|
||||
/// This macro provides a simple transformation from `Scope<{}>` to `Scope<P>`,
|
||||
|
||||
@@ -43,27 +43,21 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
|
||||
syn::Fields::Unnamed(_) => {
|
||||
return Err(Error::new(
|
||||
ast.span(),
|
||||
"TypedBuilder is not supported for tuple structs",
|
||||
"Props is not supported for tuple structs",
|
||||
))
|
||||
}
|
||||
syn::Fields::Unit => {
|
||||
return Err(Error::new(
|
||||
ast.span(),
|
||||
"TypedBuilder is not supported for unit structs",
|
||||
"Props is not supported for unit structs",
|
||||
))
|
||||
}
|
||||
},
|
||||
syn::Data::Enum(_) => {
|
||||
return Err(Error::new(
|
||||
ast.span(),
|
||||
"TypedBuilder is not supported for enums",
|
||||
))
|
||||
return Err(Error::new(ast.span(), "Props is not supported for enums"))
|
||||
}
|
||||
syn::Data::Union(_) => {
|
||||
return Err(Error::new(
|
||||
ast.span(),
|
||||
"TypedBuilder is not supported for unions",
|
||||
))
|
||||
return Err(Error::new(ast.span(), "Props is not supported for unions"))
|
||||
}
|
||||
};
|
||||
Ok(data)
|
||||
@@ -169,6 +163,7 @@ mod util {
|
||||
}
|
||||
|
||||
mod field_info {
|
||||
use crate::props::type_from_inside_option;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::Error;
|
||||
@@ -202,6 +197,16 @@ mod field_info {
|
||||
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
||||
}
|
||||
|
||||
// auto detect optional
|
||||
let strip_option_auto = builder_attr.strip_option
|
||||
|| !builder_attr.ignore_option
|
||||
&& type_from_inside_option(&field.ty, true).is_some();
|
||||
if !builder_attr.strip_option && strip_option_auto {
|
||||
builder_attr.strip_option = true;
|
||||
builder_attr.default =
|
||||
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
||||
}
|
||||
|
||||
Ok(FieldInfo {
|
||||
ordinal,
|
||||
name,
|
||||
@@ -236,31 +241,8 @@ mod field_info {
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn type_from_inside_option(&self) -> Option<&syn::Type> {
|
||||
let path = if let syn::Type::Path(type_path) = self.ty {
|
||||
if type_path.qself.is_some() {
|
||||
return None;
|
||||
} else {
|
||||
&type_path.path
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let segment = path.segments.last()?;
|
||||
if segment.ident != "Option" {
|
||||
return None;
|
||||
}
|
||||
let generic_params =
|
||||
if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments {
|
||||
generic_params
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
if let syn::GenericArgument::Type(ty) = generic_params.args.first()? {
|
||||
Some(ty)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
|
||||
type_from_inside_option(self.ty, check_option_name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +253,7 @@ mod field_info {
|
||||
pub skip: bool,
|
||||
pub auto_into: bool,
|
||||
pub strip_option: bool,
|
||||
pub ignore_option: bool,
|
||||
}
|
||||
|
||||
impl FieldBuilderAttr {
|
||||
@@ -427,8 +410,9 @@ mod field_info {
|
||||
self.auto_into = false;
|
||||
Ok(())
|
||||
}
|
||||
"strip_option" => {
|
||||
"optional" => {
|
||||
self.strip_option = false;
|
||||
self.ignore_option = true;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(Error::new_spanned(path, "Unknown setting".to_owned())),
|
||||
@@ -446,6 +430,33 @@ mod field_info {
|
||||
}
|
||||
}
|
||||
|
||||
fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&syn::Type> {
|
||||
let path = if let syn::Type::Path(type_path) = ty {
|
||||
if type_path.qself.is_some() {
|
||||
return None;
|
||||
} else {
|
||||
&type_path.path
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let segment = path.segments.last()?;
|
||||
if check_option_name && segment.ident != "Option" {
|
||||
return None;
|
||||
}
|
||||
let generic_params =
|
||||
if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments {
|
||||
generic_params
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
if let syn::GenericArgument::Type(ty) = generic_params.args.first()? {
|
||||
Some(ty)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
mod struct_info {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
@@ -766,7 +777,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
// NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of
|
||||
// nesting is different so we have to do this little dance.
|
||||
let arg_type = if field.builder_attr.strip_option {
|
||||
let internal_type = field.type_from_inside_option().ok_or_else(|| {
|
||||
let internal_type = field.type_from_inside_option(false).ok_or_else(|| {
|
||||
Error::new_spanned(
|
||||
&field_type,
|
||||
"can't `strip_option` - field is not `Option<...>`",
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant};
|
||||
|
||||
const AT_ATTR_IDENT: &str = "at";
|
||||
const NOT_FOUND_ATTR_IDENT: &str = "not_found";
|
||||
|
||||
pub struct Routable {
|
||||
ident: Ident,
|
||||
ats: Vec<LitStr>,
|
||||
variants: Punctuated<Variant, syn::token::Comma>,
|
||||
not_found_route: Option<Ident>,
|
||||
}
|
||||
|
||||
impl Parse for Routable {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let DeriveInput { ident, data, .. } = input.parse()?;
|
||||
|
||||
let data = match data {
|
||||
Data::Enum(data) => data,
|
||||
Data::Struct(s) => {
|
||||
return Err(syn::Error::new(
|
||||
s.struct_token.span(),
|
||||
"expected enum, found struct",
|
||||
))
|
||||
}
|
||||
Data::Union(u) => {
|
||||
return Err(syn::Error::new(
|
||||
u.union_token.span(),
|
||||
"expected enum, found union",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let (not_found_route, ats) = parse_variants_attributes(&data.variants)?;
|
||||
|
||||
Ok(Self {
|
||||
ident,
|
||||
variants: data.variants,
|
||||
ats,
|
||||
not_found_route,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_variants_attributes(
|
||||
variants: &Punctuated<Variant, syn::token::Comma>,
|
||||
) -> syn::Result<(Option<Ident>, Vec<LitStr>)> {
|
||||
let mut not_founds = vec![];
|
||||
let mut ats: Vec<LitStr> = vec![];
|
||||
|
||||
let mut not_found_attrs = vec![];
|
||||
|
||||
for variant in variants.iter() {
|
||||
if let Fields::Unnamed(ref field) = variant.fields {
|
||||
return Err(syn::Error::new(
|
||||
field.span(),
|
||||
"only named fields are supported",
|
||||
));
|
||||
}
|
||||
|
||||
let attrs = &variant.attrs;
|
||||
let at_attrs = attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.path.is_ident(AT_ATTR_IDENT))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let attr = match at_attrs.len() {
|
||||
1 => *at_attrs.first().unwrap(),
|
||||
0 => {
|
||||
return Err(syn::Error::new(
|
||||
variant.span(),
|
||||
format!(
|
||||
"{} attribute must be present on every variant",
|
||||
AT_ATTR_IDENT
|
||||
),
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
quote! { #(#at_attrs)* },
|
||||
format!("only one {} attribute must be present", AT_ATTR_IDENT),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let lit = attr.parse_args::<LitStr>()?;
|
||||
ats.push(lit);
|
||||
|
||||
for attr in attrs.iter() {
|
||||
if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) {
|
||||
not_found_attrs.push(attr);
|
||||
not_founds.push(variant.ident.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if not_founds.len() > 1 {
|
||||
return Err(syn::Error::new_spanned(
|
||||
quote! { #(#not_found_attrs)* },
|
||||
format!("there can only be one {}", NOT_FOUND_ATTR_IDENT),
|
||||
));
|
||||
}
|
||||
|
||||
Ok((not_founds.into_iter().next(), ats))
|
||||
}
|
||||
|
||||
impl Routable {
|
||||
// fn build_from_path(&self) -> TokenStream {
|
||||
// let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
|
||||
// let ident = &variant.ident;
|
||||
// let right = match &variant.fields {
|
||||
// Fields::Unit => quote! { Self::#ident },
|
||||
// Fields::Named(field) => {
|
||||
// let fields = field.named.iter().map(|it| {
|
||||
// //named fields have idents
|
||||
// it.ident.as_ref().unwrap()
|
||||
// });
|
||||
// quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } }
|
||||
// }
|
||||
// Fields::Unnamed(_) => unreachable!(), // already checked
|
||||
// };
|
||||
|
||||
// let left = self.ats.get(i).unwrap();
|
||||
// quote! {
|
||||
// #left => ::std::option::Option::Some(#right)
|
||||
// }
|
||||
// });
|
||||
|
||||
// quote! {
|
||||
// fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option<Self> {
|
||||
// match path {
|
||||
// #(#from_path_matches),*,
|
||||
// _ => ::std::option::Option::None,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn build_to_path(&self) -> TokenStream {
|
||||
// let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
|
||||
// let ident = &variant.ident;
|
||||
// let mut right = self.ats.get(i).unwrap().value();
|
||||
|
||||
// match &variant.fields {
|
||||
// Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) },
|
||||
// Fields::Named(field) => {
|
||||
// let fields = field
|
||||
// .named
|
||||
// .iter()
|
||||
// .map(|it| it.ident.as_ref().unwrap())
|
||||
// .collect::<Vec<_>>();
|
||||
|
||||
// for field in fields.iter() {
|
||||
// // :param -> {param}
|
||||
// // so we can pass it to `format!("...", param)`
|
||||
// right = right.replace(&format!(":{}", field), &format!("{{{}}}", field))
|
||||
// }
|
||||
|
||||
// quote! {
|
||||
// Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*)
|
||||
// }
|
||||
// }
|
||||
// Fields::Unnamed(_) => unreachable!(), // already checked
|
||||
// }
|
||||
// });
|
||||
|
||||
// quote! {
|
||||
// fn to_path(&self) -> ::std::string::String {
|
||||
// match self {
|
||||
// #(#to_path_matches),*,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn routable_derive_impl(input: Routable) -> TokenStream {
|
||||
let Routable {
|
||||
// ats,
|
||||
// not_found_route,
|
||||
// ident,
|
||||
..
|
||||
} = &input;
|
||||
|
||||
// let from_path = input.build_from_path();
|
||||
// let to_path = input.build_to_path();
|
||||
|
||||
quote! {
|
||||
// #[automatically_derived]
|
||||
// impl ::dioxus::router::Routable for #ident {
|
||||
|
||||
// fn recognize(pathname: &str) -> ::std::option::Option<Self> {
|
||||
// todo!()
|
||||
// // ::std::thread_local! {
|
||||
// // static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>();
|
||||
// // }
|
||||
// // let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname));
|
||||
// // {
|
||||
// // let route = ::std::clone::Clone::clone(&route);
|
||||
// // #cache_thread_local_ident.with(move |val| {
|
||||
// // *val.borrow_mut() = route;
|
||||
// // });
|
||||
// // }
|
||||
// // route
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,10 @@ use syn::{
|
||||
};
|
||||
|
||||
pub struct Component {
|
||||
name: syn::Path,
|
||||
body: Vec<ComponentField>,
|
||||
children: Vec<BodyNode>,
|
||||
manual_props: Option<Expr>,
|
||||
pub name: syn::Path,
|
||||
pub body: Vec<ComponentField>,
|
||||
pub children: Vec<BodyNode>,
|
||||
pub manual_props: Option<Expr>,
|
||||
}
|
||||
|
||||
impl Parse for Component {
|
||||
@@ -199,8 +199,7 @@ impl Parse for ComponentField {
|
||||
}
|
||||
|
||||
if input.peek(LitStr) && input.peek2(LitStr) {
|
||||
let item = input.parse::<LitStr>().unwrap();
|
||||
proc_macro_error::emit_error!(item, "This attribute is missing a trailing comma")
|
||||
missing_trailing_comma!(input.span());
|
||||
}
|
||||
|
||||
let content = ContentField::ManExpr(input.parse()?);
|
||||
|
||||
@@ -11,12 +11,11 @@ use syn::{
|
||||
// Parse the VNode::Element type
|
||||
// =======================================
|
||||
pub struct Element {
|
||||
name: Ident,
|
||||
key: Option<LitStr>,
|
||||
attributes: Vec<ElementAttrNamed>,
|
||||
listeners: Vec<ElementAttrNamed>,
|
||||
children: Vec<BodyNode>,
|
||||
_is_static: bool,
|
||||
pub name: Ident,
|
||||
pub key: Option<LitStr>,
|
||||
pub attributes: Vec<ElementAttrNamed>,
|
||||
pub children: Vec<BodyNode>,
|
||||
pub _is_static: bool,
|
||||
}
|
||||
|
||||
impl Parse for Element {
|
||||
@@ -28,7 +27,6 @@ impl Parse for Element {
|
||||
syn::braced!(content in stream);
|
||||
|
||||
let mut attributes: Vec<ElementAttrNamed> = vec![];
|
||||
let mut listeners: Vec<ElementAttrNamed> = vec![];
|
||||
let mut children: Vec<BodyNode> = vec![];
|
||||
let mut key = None;
|
||||
let mut _el_ref = None;
|
||||
@@ -54,6 +52,7 @@ impl Parse for Element {
|
||||
});
|
||||
} else {
|
||||
let value = content.parse::<Expr>()?;
|
||||
|
||||
attributes.push(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::CustomAttrExpression { name, value },
|
||||
@@ -64,12 +63,8 @@ impl Parse for Element {
|
||||
break;
|
||||
}
|
||||
|
||||
// todo: add a message saying you need to include commas between fields
|
||||
if content.parse::<Token![,]>().is_err() {
|
||||
proc_macro_error::emit_error!(
|
||||
ident,
|
||||
"This attribute is missing a trailing comma"
|
||||
)
|
||||
missing_trailing_comma!(ident);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -82,7 +77,7 @@ impl Parse for Element {
|
||||
content.parse::<Token![:]>()?;
|
||||
|
||||
if name_str.starts_with("on") {
|
||||
listeners.push(ElementAttrNamed {
|
||||
attributes.push(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::EventTokens {
|
||||
name,
|
||||
@@ -127,10 +122,7 @@ impl Parse for Element {
|
||||
|
||||
// todo: add a message saying you need to include commas between fields
|
||||
if content.parse::<Token![,]>().is_err() {
|
||||
proc_macro_error::emit_error!(
|
||||
ident,
|
||||
"This attribute is missing a trailing comma"
|
||||
)
|
||||
missing_trailing_comma!(ident);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -140,33 +132,11 @@ impl Parse for Element {
|
||||
|
||||
while !content.is_empty() {
|
||||
if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
|
||||
let ident = content.parse::<LitStr>().unwrap();
|
||||
let name = ident.value();
|
||||
proc_macro_error::emit_error!(
|
||||
ident, "This attribute `{}` is in the wrong place.", name;
|
||||
help =
|
||||
"All attribute fields must be placed above children elements.
|
||||
|
||||
div {
|
||||
attr: \"...\", <---- attribute is above children
|
||||
div { } <---- children are below attributes
|
||||
}";
|
||||
)
|
||||
attr_after_element!(content.span());
|
||||
}
|
||||
|
||||
if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
|
||||
let ident = content.parse::<Ident>().unwrap();
|
||||
let name = ident.to_string();
|
||||
proc_macro_error::emit_error!(
|
||||
ident, "This attribute `{}` is in the wrong place.", name;
|
||||
help =
|
||||
"All attribute fields must be placed above children elements.
|
||||
|
||||
div {
|
||||
attr: \"...\", <---- attribute is above children
|
||||
div { } <---- children are below attributes
|
||||
}";
|
||||
)
|
||||
attr_after_element!(content.span());
|
||||
}
|
||||
|
||||
children.push(content.parse::<BodyNode>()?);
|
||||
@@ -182,7 +152,6 @@ impl Parse for Element {
|
||||
name: el_name,
|
||||
attributes,
|
||||
children,
|
||||
listeners,
|
||||
_is_static: false,
|
||||
})
|
||||
}
|
||||
@@ -193,14 +162,21 @@ impl ToTokens for Element {
|
||||
let name = &self.name;
|
||||
let children = &self.children;
|
||||
|
||||
let listeners = &self.listeners;
|
||||
let attr = &self.attributes;
|
||||
|
||||
let key = match &self.key {
|
||||
Some(ty) => quote! { Some(format_args_f!(#ty)) },
|
||||
None => quote! { None },
|
||||
};
|
||||
|
||||
let listeners = self
|
||||
.attributes
|
||||
.iter()
|
||||
.filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. }));
|
||||
|
||||
let attr = self
|
||||
.attributes
|
||||
.iter()
|
||||
.filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. }));
|
||||
|
||||
tokens.append_all(quote! {
|
||||
__cx.element(
|
||||
dioxus_elements::#name,
|
||||
@@ -213,29 +189,28 @@ impl ToTokens for Element {
|
||||
}
|
||||
}
|
||||
|
||||
enum ElementAttr {
|
||||
// attribute: "valuee {}"
|
||||
pub enum ElementAttr {
|
||||
/// attribute: "valuee {}"
|
||||
AttrText { name: Ident, value: LitStr },
|
||||
|
||||
// attribute: true,
|
||||
/// attribute: true,
|
||||
AttrExpression { name: Ident, value: Expr },
|
||||
|
||||
// "attribute": "value {}"
|
||||
/// "attribute": "value {}"
|
||||
CustomAttrText { name: LitStr, value: LitStr },
|
||||
|
||||
// "attribute": true,
|
||||
/// "attribute": true,
|
||||
CustomAttrExpression { name: LitStr, value: Expr },
|
||||
|
||||
// // onclick: move |_| {}
|
||||
// /// onclick: move |_| {}
|
||||
// EventClosure { name: Ident, closure: ExprClosure },
|
||||
|
||||
// onclick: {}
|
||||
/// onclick: {}
|
||||
EventTokens { name: Ident, tokens: Expr },
|
||||
}
|
||||
|
||||
struct ElementAttrNamed {
|
||||
el_name: Ident,
|
||||
attr: ElementAttr,
|
||||
pub struct ElementAttrNamed {
|
||||
pub el_name: Ident,
|
||||
pub attr: ElementAttr,
|
||||
}
|
||||
|
||||
impl ToTokens for ElementAttrNamed {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
macro_rules! missing_trailing_comma {
|
||||
($span:expr) => {
|
||||
proc_macro_error::emit_error!($span, "missing trailing comma")
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! attr_after_element {
|
||||
($span:expr) => {
|
||||
proc_macro_error::emit_error!(
|
||||
$span,
|
||||
"expected element";
|
||||
help = "move the attribute above all the children and text elements"
|
||||
)
|
||||
};
|
||||
}
|
||||
@@ -11,10 +11,15 @@
|
||||
//!
|
||||
//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful.
|
||||
|
||||
#[macro_use]
|
||||
mod errors;
|
||||
|
||||
mod component;
|
||||
mod element;
|
||||
mod node;
|
||||
|
||||
pub mod pretty;
|
||||
|
||||
// Re-export the namespaces into each other
|
||||
pub use component::*;
|
||||
pub use element::*;
|
||||
@@ -29,8 +34,8 @@ use syn::{
|
||||
};
|
||||
|
||||
pub struct CallBody {
|
||||
custom_context: Option<Ident>,
|
||||
roots: Vec<BodyNode>,
|
||||
pub custom_context: Option<Ident>,
|
||||
pub roots: Vec<BodyNode>,
|
||||
}
|
||||
|
||||
impl Parse for CallBody {
|
||||
@@ -77,7 +82,7 @@ impl ToTokens for CallBody {
|
||||
match &self.custom_context {
|
||||
// The `in cx` pattern allows directly rendering
|
||||
Some(ident) => out_tokens.append_all(quote! {
|
||||
#ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
|
||||
#ident.render(LazyNodes::new(move |__cx: NodeFactory| -> VNode {
|
||||
use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
#inner
|
||||
}))
|
||||
@@ -85,7 +90,7 @@ impl ToTokens for CallBody {
|
||||
|
||||
// Otherwise we just build the LazyNode wrapper
|
||||
None => out_tokens.append_all(quote! {
|
||||
LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
|
||||
LazyNodes::new(move |__cx: NodeFactory| -> VNode {
|
||||
use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
#inner
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
//! pretty printer for rsx!
|
||||
@@ -1,84 +0,0 @@
|
||||
use crate::{rsx::RsxBody, util::is_valid_svg_tag};
|
||||
|
||||
use {
|
||||
proc_macro::TokenStream,
|
||||
proc_macro2::{Span, TokenStream as TokenStream2},
|
||||
quote::{quote, ToTokens, TokenStreamExt},
|
||||
syn::{
|
||||
ext::IdentExt,
|
||||
parse::{Parse, ParseStream},
|
||||
token, Error, Expr, ExprClosure, Ident, LitBool, LitStr, Path, Result, Token,
|
||||
},
|
||||
};
|
||||
|
||||
// ==============================================
|
||||
// Parse any stream coming from the html! macro
|
||||
// ==============================================
|
||||
pub struct RsxTemplate {
|
||||
inner: RsxBody,
|
||||
}
|
||||
|
||||
impl Parse for RsxTemplate {
|
||||
fn parse(s: ParseStream) -> Result<Self> {
|
||||
if s.peek(LitStr) {
|
||||
use std::str::FromStr;
|
||||
|
||||
let lit = s.parse::<LitStr>()?;
|
||||
let g = lit.span();
|
||||
let mut value = lit.value();
|
||||
if value.ends_with('\n') {
|
||||
value.pop();
|
||||
if value.ends_with('\r') {
|
||||
value.pop();
|
||||
}
|
||||
}
|
||||
let lit = LitStr::new(&value, lit.span());
|
||||
|
||||
// panic!("{:#?}", lit);
|
||||
match lit.parse::<crate::rsx::RsxBody>() {
|
||||
Ok(r) => Ok(Self { inner: r }),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
panic!("Not a str lit")
|
||||
}
|
||||
// let t = s.parse::<LitStr>()?;
|
||||
|
||||
// let new_stream = TokenStream::from(t.to_s)
|
||||
|
||||
// let cx: Ident = s.parse()?;
|
||||
// s.parse::<Token![,]>()?;
|
||||
// if elements are in an array, return a bumpalo::collections::Vec rather than a Node.
|
||||
// let kind = if s.peek(token::Bracket) {
|
||||
// let nodes_toks;
|
||||
// syn::bracketed!(nodes_toks in s);
|
||||
// let mut nodes: Vec<MaybeExpr<Node>> = vec![nodes_toks.parse()?];
|
||||
// while nodes_toks.peek(Token![,]) {
|
||||
// nodes_toks.parse::<Token![,]>()?;
|
||||
// nodes.push(nodes_toks.parse()?);
|
||||
// }
|
||||
// NodeOrList::List(NodeList(nodes))
|
||||
// } else {
|
||||
// NodeOrList::Node(s.parse()?)
|
||||
// };
|
||||
// Ok(HtmlRender { kind })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for RsxTemplate {
|
||||
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
|
||||
self.inner.to_tokens(out_tokens);
|
||||
// let new_toks = ToToksCtx::new(&self.kind).to_token_stream();
|
||||
|
||||
// // create a lazy tree that accepts a bump allocator
|
||||
// let final_tokens = quote! {
|
||||
// dioxus::prelude::LazyNodes::new(move |cx| {
|
||||
// let bump = &cx.bump();
|
||||
|
||||
// #new_toks
|
||||
// })
|
||||
// };
|
||||
|
||||
// final_tokens.to_tokens(out_tokens);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-core"
|
||||
version = "0.1.9"
|
||||
version = "0.2.0"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
@@ -24,7 +24,7 @@ fxhash = "0.2"
|
||||
longest-increasing-subsequence = "0.1.0"
|
||||
|
||||
# internall used
|
||||
log = { version = "0.4", features = ["release_max_level_off"] }
|
||||
log = { version = "0.4" }
|
||||
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
|
||||
@@ -32,7 +32,7 @@ smallvec = "1.6"
|
||||
|
||||
slab = "0.4"
|
||||
|
||||
futures-channel = "0.3"
|
||||
futures-channel = "0.3.21"
|
||||
|
||||
# used for noderefs
|
||||
once_cell = "1.8"
|
||||
@@ -45,24 +45,8 @@ serde = { version = "1", features = ["derive"], optional = true }
|
||||
# todo: I want to get rid of this
|
||||
backtrace = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.42"
|
||||
fern = { version = "0.6.0", features = ["colored"] }
|
||||
rand = { version = "0.8.4", features = ["small_rng"] }
|
||||
dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"}
|
||||
criterion = "0.3.5"
|
||||
thiserror = "1.0.30"
|
||||
dioxus-html = { path = "../html" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
serialize = ["serde"]
|
||||
debug_vdom = []
|
||||
|
||||
[[bench]]
|
||||
name = "create"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "jsframework"
|
||||
harness = false
|
||||
@@ -1 +0,0 @@
|
||||
fn main() {}
|
||||
@@ -1,123 +0,0 @@
|
||||
#![allow(non_snake_case, non_upper_case_globals)]
|
||||
//! This benchmark tests just the overhead of Dioxus itself.
|
||||
//!
|
||||
//! For the JS Framework Benchmark, both the framework and the browser is benchmarked together. Dioxus prepares changes
|
||||
//! 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
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use rand::prelude::*;
|
||||
|
||||
criterion_group!(mbenches, create_rows);
|
||||
criterion_main!(mbenches);
|
||||
|
||||
fn create_rows(c: &mut Criterion) {
|
||||
static App: Component = |cx| {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
|
||||
rsx!(cx, 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| {
|
||||
b.iter(|| {
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let g = dom.rebuild();
|
||||
assert!(g.edits.len() > 1);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct RowProps {
|
||||
row_id: usize,
|
||||
label: Label,
|
||||
}
|
||||
fn 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",
|
||||
];
|
||||
+19
-25
@@ -257,11 +257,10 @@ impl<'b> DiffState<'b> {
|
||||
vcomponent.scope.set(Some(new_idx));
|
||||
|
||||
log::trace!(
|
||||
"created component \"{}\", id: {:?} parent {:?} orig: {:?}",
|
||||
"created component \"{}\", id: {:?} parent {:?}",
|
||||
vcomponent.fn_name,
|
||||
new_idx,
|
||||
parent_idx,
|
||||
vcomponent.originator
|
||||
);
|
||||
|
||||
// if vcomponent.can_memoize {
|
||||
@@ -424,8 +423,10 @@ impl<'b> DiffState<'b> {
|
||||
match (old.children.len(), new.children.len()) {
|
||||
(0, 0) => {}
|
||||
(0, _) => {
|
||||
self.mutations.push_root(root);
|
||||
let created = self.create_children(new.children);
|
||||
self.mutations.append_children(created as u32);
|
||||
self.mutations.pop_root();
|
||||
}
|
||||
(_, _) => self.diff_children(old.children, new.children),
|
||||
};
|
||||
@@ -814,6 +815,15 @@ impl<'b> DiffState<'b> {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove any old children that are not shared
|
||||
// todo: make this an iterator
|
||||
for child in old {
|
||||
let key = child.key().unwrap();
|
||||
if !shared_keys.contains(&key) {
|
||||
self.remove_nodes([child], true);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Compute the LIS of this list
|
||||
let mut lis_sequence = Vec::default();
|
||||
lis_sequence.reserve(new_index_to_old_index.len());
|
||||
@@ -853,7 +863,7 @@ impl<'b> DiffState<'b> {
|
||||
nodes_created += self.create_node(new_node);
|
||||
} else {
|
||||
self.diff_node(&old[old_index], new_node);
|
||||
nodes_created += self.push_all_nodes(new_node);
|
||||
nodes_created += self.push_all_real_nodes(new_node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,7 +886,7 @@ impl<'b> DiffState<'b> {
|
||||
nodes_created += self.create_node(new_node);
|
||||
} else {
|
||||
self.diff_node(&old[old_index], new_node);
|
||||
nodes_created += self.push_all_nodes(new_node);
|
||||
nodes_created += self.push_all_real_nodes(new_node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -899,7 +909,7 @@ impl<'b> DiffState<'b> {
|
||||
nodes_created += self.create_node(new_node);
|
||||
} else {
|
||||
self.diff_node(&old[old_index], new_node);
|
||||
nodes_created += self.push_all_nodes(new_node);
|
||||
nodes_created += self.push_all_real_nodes(new_node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -957,13 +967,6 @@ impl<'b> DiffState<'b> {
|
||||
let props = scope.props.take().unwrap();
|
||||
c.props.borrow_mut().replace(props);
|
||||
self.scopes.try_remove(scope_id).unwrap();
|
||||
|
||||
// // we can only remove components if they are actively being diffed
|
||||
// if self.scope_stack.contains(&c.originator) {
|
||||
// log::trace!("Removing component {:?}", old);
|
||||
|
||||
// self.scopes.try_remove(scope_id).unwrap();
|
||||
// }
|
||||
}
|
||||
self.leave_scope();
|
||||
}
|
||||
@@ -1100,9 +1103,9 @@ impl<'b> DiffState<'b> {
|
||||
}
|
||||
|
||||
// recursively push all the nodes of a tree onto the stack and return how many are there
|
||||
fn push_all_nodes(&mut self, node: &'b VNode<'b>) -> usize {
|
||||
fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
|
||||
match node {
|
||||
VNode::Text(_) | VNode::Placeholder(_) => {
|
||||
VNode::Text(_) | VNode::Placeholder(_) | VNode::Element(_) => {
|
||||
self.mutations.push_root(node.mounted_id());
|
||||
1
|
||||
}
|
||||
@@ -1110,7 +1113,7 @@ impl<'b> DiffState<'b> {
|
||||
VNode::Fragment(frag) => {
|
||||
let mut added = 0;
|
||||
for child in frag.children {
|
||||
added += self.push_all_nodes(child);
|
||||
added += self.push_all_real_nodes(child);
|
||||
}
|
||||
added
|
||||
}
|
||||
@@ -1118,16 +1121,7 @@ impl<'b> DiffState<'b> {
|
||||
VNode::Component(c) => {
|
||||
let scope_id = c.scope.get().unwrap();
|
||||
let root = self.scopes.root_node(scope_id);
|
||||
self.push_all_nodes(root)
|
||||
}
|
||||
|
||||
VNode::Element(el) => {
|
||||
let mut num_on_stack = 0;
|
||||
for child in el.children.iter() {
|
||||
num_on_stack += self.push_all_nodes(child);
|
||||
}
|
||||
self.mutations.push_root(el.id.get().unwrap());
|
||||
num_on_stack + 1
|
||||
self.push_all_real_nodes(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,24 +119,41 @@ pub enum EventPriority {
|
||||
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();
|
||||
/// ```
|
||||
pub fn downcast<T: Send + Sync + 'static>(self) -> Option<UiEvent<T>> {
|
||||
let AnyEvent { data, bubble_state } = self;
|
||||
|
||||
if let Ok(data) = data.downcast::<T>() {
|
||||
Some(UiEvent { bubble_state, data })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
data.downcast::<T>()
|
||||
.ok()
|
||||
.map(|data| UiEvent { bubble_state, data })
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
|
||||
#[allow(unused)]
|
||||
|
||||
@@ -35,14 +35,32 @@ enum StackNodeStorage<'a, 'b> {
|
||||
}
|
||||
|
||||
impl<'a, 'b> LazyNodes<'a, 'b> {
|
||||
pub fn new_some<F>(_val: F) -> Self
|
||||
where
|
||||
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
|
||||
{
|
||||
Self::new(_val)
|
||||
/// Create a new LazyNodes closure, optimistically placing it onto the stack.
|
||||
///
|
||||
/// 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 {
|
||||
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
|
||||
let mut slot = Some(val);
|
||||
|
||||
let val = move |fac: Option<NodeFactory<'a>>| {
|
||||
let inner = slot.take().unwrap();
|
||||
fac.map(inner)
|
||||
};
|
||||
|
||||
// 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) }
|
||||
}
|
||||
}
|
||||
|
||||
/// force this call onto the stack
|
||||
/// Create a new LazyNodes closure, but force it onto the heap.
|
||||
pub fn new_boxed<F>(_val: F) -> Self
|
||||
where
|
||||
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
|
||||
@@ -60,25 +78,6 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(_val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self {
|
||||
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
|
||||
let mut slot = Some(_val);
|
||||
|
||||
let val = move |fac: Option<NodeFactory<'a>>| {
|
||||
let inner = slot.take().unwrap();
|
||||
fac.map(inner)
|
||||
};
|
||||
|
||||
// miri does not know how to work with mucking directly into bytes
|
||||
if cfg!(miri) {
|
||||
Self {
|
||||
inner: StackNodeStorage::Heap(Box::new(val)),
|
||||
}
|
||||
} else {
|
||||
unsafe { LazyNodes::new_inner(val) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn new_inner<F>(val: F) -> Self
|
||||
where
|
||||
F: FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b,
|
||||
@@ -153,6 +152,15 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the closure with the given factory to produce real VNodes.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
|
||||
///
|
||||
/// let fac = NodeFactory::new(&cx);
|
||||
///
|
||||
/// let node = f.call(cac);
|
||||
/// ```
|
||||
pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> {
|
||||
match self.inner {
|
||||
StackNodeStorage::Heap(mut lazy) => lazy(Some(f)).unwrap(),
|
||||
@@ -245,9 +253,7 @@ mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
fn app(cx: Scope<()>) -> Element {
|
||||
cx.render(LazyNodes::new_some(|f| {
|
||||
f.text(format_args!("hello world!"))
|
||||
}))
|
||||
cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
@@ -271,7 +277,7 @@ mod tests {
|
||||
}
|
||||
impl Drop for DropInner {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("dropping inner");
|
||||
eprintln!("dropping inner");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,16 +286,16 @@ mod tests {
|
||||
.map(|i| {
|
||||
let val = cx.props.inner.clone();
|
||||
|
||||
LazyNodes::new_some(move |f| {
|
||||
log::debug!("hell closure");
|
||||
LazyNodes::new(move |f| {
|
||||
eprintln!("hell closure");
|
||||
let inner = DropInner { id: i };
|
||||
f.text(format_args!("hello world {:?}, {:?}", inner.id, val))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
LazyNodes::new_some(|f| {
|
||||
log::debug!("main closure");
|
||||
LazyNodes::new(|f| {
|
||||
eprintln!("main closure");
|
||||
f.fragment_from_iter(it)
|
||||
})
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![allow(non_snake_case)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
pub(crate) mod diff;
|
||||
pub(crate) mod events;
|
||||
@@ -76,10 +77,13 @@ pub use crate::innerlude::{
|
||||
VElement, VFragment, VNode, VPlaceholder, VText, VirtualDom,
|
||||
};
|
||||
|
||||
/// The purpose of this module is to alleviate imports of many common types
|
||||
///
|
||||
/// 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, ScopeState, VNode, VirtualDom,
|
||||
LazyNodes, NodeFactory, Properties, Scope, ScopeId, ScopeState, VNode, VirtualDom,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -98,3 +102,26 @@ pub(crate) mod unsafe_utils {
|
||||
std::mem::transmute(node)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
/// A helper macro for using hooks in async environements.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
///
|
||||
/// ```
|
||||
/// 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();
|
||||
)*}
|
||||
}
|
||||
+104
-12
@@ -16,8 +16,13 @@ use std::{any::Any, fmt::Debug};
|
||||
///
|
||||
/// Mutations are the only link between the RealDOM and the VirtualDOM.
|
||||
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 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>>,
|
||||
}
|
||||
|
||||
@@ -39,75 +44,150 @@ impl Debug for Mutations<'_> {
|
||||
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.
|
||||
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,
|
||||
},
|
||||
|
||||
// // save a possibly-fragment node as a template
|
||||
// SaveAsTemplate {
|
||||
// many: u32,
|
||||
// },
|
||||
|
||||
// "Root" refers to the item directly
|
||||
// it's a waste of an instruction to push the root directly
|
||||
/// 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,
|
||||
|
||||
/// How many nodes should be popped from the stack to replace the target node.
|
||||
m: u32,
|
||||
},
|
||||
|
||||
/// Insert a number of nodes after a given node.
|
||||
InsertAfter {
|
||||
/// The ID of the node to insert after.
|
||||
root: u64,
|
||||
|
||||
/// How many nodes should be popped from the stack to insert after the target node.
|
||||
n: u32,
|
||||
},
|
||||
|
||||
/// Insert a number of nodes before a given node.
|
||||
InsertBefore {
|
||||
/// The ID of the node to insert before.
|
||||
root: u64,
|
||||
|
||||
/// How many nodes should be popped from the stack to insert before the target node.
|
||||
n: u32,
|
||||
},
|
||||
|
||||
/// Remove a particular node from the DOM
|
||||
Remove {
|
||||
/// The ID of the node to remove.
|
||||
root: u64,
|
||||
},
|
||||
|
||||
/// Create a new purely-text node
|
||||
CreateTextNode {
|
||||
/// The ID the new node should have.
|
||||
root: u64,
|
||||
|
||||
/// The textcontent of the node
|
||||
text: &'bump str,
|
||||
root: u64,
|
||||
},
|
||||
|
||||
/// Create a new purely-element node
|
||||
CreateElement {
|
||||
tag: &'bump str,
|
||||
/// 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 {
|
||||
tag: &'bump str,
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// Create a new Event Listener.
|
||||
NewEventListener {
|
||||
/// The name of the event to listen for.
|
||||
event_name: &'static 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,
|
||||
},
|
||||
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// 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: &'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>,
|
||||
},
|
||||
|
||||
/// Manually pop a root node from the stack.
|
||||
PopRoot {},
|
||||
}
|
||||
|
||||
use fxhash::FxHashSet;
|
||||
@@ -128,6 +208,11 @@ impl<'a> Mutations<'a> {
|
||||
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 });
|
||||
@@ -218,8 +303,15 @@ impl<'a> Mutations<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) {
|
||||
let name = attribute.name;
|
||||
self.edits.push(RemoveAttribute { name, root });
|
||||
let Attribute {
|
||||
name, namespace, ..
|
||||
} = attribute;
|
||||
|
||||
self.edits.push(RemoveAttribute {
|
||||
name,
|
||||
ns: *namespace,
|
||||
root,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) {
|
||||
|
||||
+96
-21
@@ -148,7 +148,7 @@ impl<'src> VNode<'src> {
|
||||
}
|
||||
|
||||
// Create an "owned" version of the vnode.
|
||||
pub fn decouple(&self) -> VNode<'src> {
|
||||
pub(crate) fn decouple(&self) -> VNode<'src> {
|
||||
match *self {
|
||||
VNode::Text(t) => VNode::Text(t),
|
||||
VNode::Element(e) => VNode::Element(e),
|
||||
@@ -163,25 +163,32 @@ impl Debug for VNode<'_> {
|
||||
fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
match &self {
|
||||
VNode::Element(el) => s
|
||||
.debug_struct("VNode::VElement")
|
||||
.debug_struct("VNode::Element")
|
||||
.field("name", &el.tag)
|
||||
.field("key", &el.key)
|
||||
.field("attrs", &el.attributes)
|
||||
.field("children", &el.children)
|
||||
.field("id", &el.id)
|
||||
.finish(),
|
||||
VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text),
|
||||
VNode::Placeholder(t) => write!(s, "VNode::VPlaceholder {{ id: {:?} }}", t.id),
|
||||
VNode::Fragment(frag) => {
|
||||
write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children)
|
||||
}
|
||||
VNode::Text(t) => s
|
||||
.debug_struct("VNode::Text")
|
||||
.field("text", &t.text)
|
||||
.field("id", &t.id)
|
||||
.finish(),
|
||||
VNode::Placeholder(t) => s
|
||||
.debug_struct("VNode::Placholder")
|
||||
.field("id", &t.id)
|
||||
.finish(),
|
||||
VNode::Fragment(frag) => s
|
||||
.debug_struct("VNode::Fragment")
|
||||
.field("children", &frag.children)
|
||||
.finish(),
|
||||
VNode::Component(comp) => s
|
||||
.debug_struct("VNode::VComponent")
|
||||
.debug_struct("VNode::Component")
|
||||
.field("name", &comp.fn_name)
|
||||
.field("fnptr", &comp.user_fc)
|
||||
.field("key", &comp.key)
|
||||
.field("scope", &comp.scope)
|
||||
.field("originator", &comp.originator)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
@@ -200,6 +207,7 @@ impl std::fmt::Display for ElementId {
|
||||
}
|
||||
|
||||
impl ElementId {
|
||||
/// Convertt the ElementId to a `u64`.
|
||||
pub fn as_u64(self) -> u64 {
|
||||
self.0 as u64
|
||||
}
|
||||
@@ -211,18 +219,26 @@ fn empty_cell() -> Cell<Option<ElementId>> {
|
||||
|
||||
/// A placeholder node only generated when Fragments don't have any children.
|
||||
pub struct VPlaceholder {
|
||||
/// The [`ElementId`] of the placeholder.
|
||||
pub id: Cell<Option<ElementId>>,
|
||||
}
|
||||
|
||||
/// A bump-allocated string slice and metadata.
|
||||
pub struct VText<'src> {
|
||||
pub text: &'src str,
|
||||
/// The [`ElementId`] of the VText.
|
||||
pub id: Cell<Option<ElementId>>,
|
||||
|
||||
/// The text of the VText.
|
||||
pub text: &'src str,
|
||||
|
||||
/// An indiciation if this VText can be ignored during diffing
|
||||
/// Is usually only when there are no strings to be formatted (so the text is &'static str)
|
||||
pub is_static: bool,
|
||||
}
|
||||
|
||||
/// A list of VNodes with no single root.
|
||||
pub struct VFragment<'src> {
|
||||
/// The key of the fragment to be used during keyed diffing.
|
||||
pub key: Option<&'src str>,
|
||||
|
||||
/// Fragments can never have zero children. Enforced by NodeFactory.
|
||||
@@ -233,13 +249,34 @@ pub struct VFragment<'src> {
|
||||
|
||||
/// An element like a "div" with children, listeners, and attributes.
|
||||
pub struct VElement<'a> {
|
||||
pub tag: &'static str,
|
||||
pub namespace: Option<&'static str>,
|
||||
pub key: Option<&'a str>,
|
||||
/// The [`ElementId`] of the VText.
|
||||
pub id: Cell<Option<ElementId>>,
|
||||
|
||||
/// The key of the element to be used during keyed diffing.
|
||||
pub key: Option<&'a str>,
|
||||
|
||||
/// The tag name of the element.
|
||||
///
|
||||
/// IE "div"
|
||||
pub tag: &'static str,
|
||||
|
||||
/// The namespace of the VElement
|
||||
///
|
||||
/// IE "svg"
|
||||
pub namespace: Option<&'static str>,
|
||||
|
||||
/// The parent of the Element (if any).
|
||||
///
|
||||
/// Used when bubbling events
|
||||
pub parent: Cell<Option<ElementId>>,
|
||||
|
||||
/// The Listeners of the VElement.
|
||||
pub listeners: &'a [Listener<'a>],
|
||||
|
||||
/// The attributes of the VElement.
|
||||
pub attributes: &'a [Attribute<'a>],
|
||||
|
||||
/// The children of the VElement.
|
||||
pub children: &'a [VNode<'a>],
|
||||
}
|
||||
|
||||
@@ -275,12 +312,19 @@ impl Debug for VElement<'_> {
|
||||
/// };
|
||||
/// ```
|
||||
pub trait DioxusElement {
|
||||
/// The tag name of the element.
|
||||
const TAG_NAME: &'static str;
|
||||
|
||||
/// The namespace of the element.
|
||||
const NAME_SPACE: Option<&'static str>;
|
||||
|
||||
/// The tag name of the element.
|
||||
#[inline]
|
||||
fn tag_name(&self) -> &'static str {
|
||||
Self::TAG_NAME
|
||||
}
|
||||
|
||||
/// The namespace of the element.
|
||||
#[inline]
|
||||
fn namespace(&self) -> Option<&'static str> {
|
||||
Self::NAME_SPACE
|
||||
@@ -291,16 +335,25 @@ pub trait DioxusElement {
|
||||
/// `href="https://example.com"`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Attribute<'a> {
|
||||
/// The name of the attribute.
|
||||
pub name: &'static str,
|
||||
|
||||
/// The value of the attribute.
|
||||
pub value: &'a str,
|
||||
|
||||
/// An indication if this attribute can be ignored during diffing
|
||||
///
|
||||
/// Usually only when there are no strings to be formatted (so the value is &'static str)
|
||||
pub is_static: bool,
|
||||
|
||||
/// An indication of we should always try and set the attribute.
|
||||
/// Used in controlled components to ensure changes are propagated.
|
||||
pub is_volatile: bool,
|
||||
|
||||
// Doesn't exist in the html spec.
|
||||
// Used in Dioxus to denote "style" tags.
|
||||
/// The namespace of the attribute.
|
||||
///
|
||||
/// Doesn't exist in the html spec.
|
||||
/// Used in Dioxus to denote "style" tags and other attribute groups.
|
||||
pub namespace: Option<&'static str>,
|
||||
}
|
||||
|
||||
@@ -353,6 +406,8 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>;
|
||||
///
|
||||
/// ```
|
||||
pub struct EventHandler<'bump, T = ()> {
|
||||
/// The (optional) callback that the user specified
|
||||
/// Uses a `RefCell` to allow for interior mutability, and FnMut closures.
|
||||
pub callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
||||
}
|
||||
|
||||
@@ -381,12 +436,23 @@ impl<T> EventHandler<'_, T> {
|
||||
/// Virtual Components for custom user-defined components
|
||||
/// Only supports the functional syntax
|
||||
pub struct VComponent<'src> {
|
||||
/// The key of the component to be used during keyed diffing.
|
||||
pub key: Option<&'src str>,
|
||||
pub originator: ScopeId,
|
||||
|
||||
/// The ID of the component.
|
||||
/// Will not be assigned until after the component has been initialized.
|
||||
pub scope: Cell<Option<ScopeId>>,
|
||||
|
||||
/// An indication if the component is static (can be memozied)
|
||||
pub can_memoize: bool,
|
||||
|
||||
/// The function pointer to the component's render function.
|
||||
pub user_fc: ComponentPtr,
|
||||
|
||||
/// The actual name of the component.
|
||||
pub fn_name: &'static str,
|
||||
|
||||
/// The props of the component.
|
||||
pub props: RefCell<Option<Box<dyn AnyProps + 'src>>>,
|
||||
}
|
||||
|
||||
@@ -434,6 +500,7 @@ pub struct NodeFactory<'a> {
|
||||
}
|
||||
|
||||
impl<'a> NodeFactory<'a> {
|
||||
/// Create a new [`NodeFactory`] from a [`Scope`] or [`ScopeState`]
|
||||
pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> {
|
||||
NodeFactory {
|
||||
scope,
|
||||
@@ -441,6 +508,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the custom allocator for this component
|
||||
#[inline]
|
||||
pub fn bump(&self) -> &'a bumpalo::Bump {
|
||||
self.bump
|
||||
@@ -482,6 +550,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VElement`]
|
||||
pub fn element(
|
||||
&self,
|
||||
el: impl DioxusElement,
|
||||
@@ -500,6 +569,9 @@ impl<'a> NodeFactory<'a> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VElement`] without the trait bound
|
||||
///
|
||||
/// IE pass in "div" instead of `div`
|
||||
pub fn raw_element(
|
||||
&self,
|
||||
tag_name: &'static str,
|
||||
@@ -529,6 +601,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a new [`Attribute`]
|
||||
pub fn attr(
|
||||
&self,
|
||||
name: &'static str,
|
||||
@@ -546,6 +619,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VComponent`]
|
||||
pub fn component<P>(
|
||||
&self,
|
||||
component: fn(Scope<'a, P>) -> Element,
|
||||
@@ -561,11 +635,8 @@ impl<'a> NodeFactory<'a> {
|
||||
scope: Default::default(),
|
||||
can_memoize: P::IS_STATIC,
|
||||
user_fc: component as ComponentPtr,
|
||||
originator: self.scope.scope_id(),
|
||||
fn_name,
|
||||
props: RefCell::new(Some(Box::new(VComponentProps {
|
||||
// local_props: RefCell::new(Some(props)),
|
||||
// heap_props: RefCell::new(None),
|
||||
props,
|
||||
memo: P::memoize, // smuggle the memoization function across borders
|
||||
|
||||
@@ -586,6 +657,7 @@ impl<'a> NodeFactory<'a> {
|
||||
VNode::Component(vcomp)
|
||||
}
|
||||
|
||||
/// Create a new [`Listener`]
|
||||
pub fn listener(self, event: &'static str, callback: InternalHandler<'a>) -> Listener<'a> {
|
||||
Listener {
|
||||
event,
|
||||
@@ -594,6 +666,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VFragment`] from a root of the rsx! call
|
||||
pub fn fragment_root<'b, 'c>(
|
||||
self,
|
||||
node_iter: impl IntoIterator<Item = impl IntoVNode<'a> + 'c> + 'b,
|
||||
@@ -614,6 +687,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VFragment`] from any iterator
|
||||
pub fn fragment_from_iter<'b, 'c>(
|
||||
self,
|
||||
node_iter: impl IntoIterator<Item = impl IntoVNode<'a> + 'c> + 'b,
|
||||
@@ -653,8 +727,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// this isn't quite feasible yet
|
||||
// I think we need some form of interior mutability or state on nodefactory that stores which subtree was created
|
||||
/// Create a new [`VNode`] from any iterator of children
|
||||
pub fn create_children(
|
||||
self,
|
||||
node_iter: impl IntoIterator<Item = impl IntoVNode<'a>>,
|
||||
@@ -679,6 +752,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`EventHandler`] from an [`FnMut`]
|
||||
pub fn event_handler<T>(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> {
|
||||
let handler: &mut dyn FnMut(T) = self.bump.alloc(f);
|
||||
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
|
||||
@@ -709,6 +783,7 @@ impl Debug for NodeFactory<'_> {
|
||||
/// As such, all node creation must go through the factory, which is only available in the component context.
|
||||
/// These strict requirements make it possible to manage lifetimes and state.
|
||||
pub trait IntoVNode<'a> {
|
||||
/// Convert this into a [`VNode`], using the [`NodeFactory`] as a source of allocation
|
||||
fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a>;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,8 +128,14 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Properties: Sized {
|
||||
/// The type of the builder for this component.
|
||||
/// Used to create "in-progress" versions of the props.
|
||||
type Builder;
|
||||
|
||||
/// An indication if these props are can be memoized automatically.
|
||||
const IS_STATIC: bool;
|
||||
|
||||
/// Create a builder for this component.
|
||||
fn builder() -> Self::Builder;
|
||||
|
||||
/// Memoization can only happen if the props are valid for the 'static lifetime
|
||||
|
||||
+91
-46
@@ -7,10 +7,11 @@ use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::Borrow,
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// for traceability, we use the raw fn pointer to identify the function
|
||||
@@ -67,7 +68,12 @@ impl ScopeArena {
|
||||
heuristics: RefCell::new(FxHashMap::default()),
|
||||
free_scopes: RefCell::new(Vec::new()),
|
||||
nodes: RefCell::new(nodes),
|
||||
tasks: TaskQueue::new(sender),
|
||||
tasks: Rc::new(TaskQueue {
|
||||
tasks: RefCell::new(FxHashMap::default()),
|
||||
task_map: RefCell::new(FxHashMap::default()),
|
||||
gen: Cell::new(0),
|
||||
sender,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,11 +102,10 @@ impl ScopeArena {
|
||||
|
||||
// Get the height of the scope
|
||||
let height = parent_scope
|
||||
.map(|id| self.get_scope(id).map(|scope| scope.height + 1))
|
||||
.flatten()
|
||||
.and_then(|id| self.get_scope(id).map(|scope| scope.height + 1))
|
||||
.unwrap_or_default();
|
||||
|
||||
let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten();
|
||||
let parent_scope = parent_scope.and_then(|f| self.get_scope_raw(f));
|
||||
|
||||
/*
|
||||
This scopearena aggressively reuses old scopes when possible.
|
||||
@@ -179,6 +184,15 @@ impl ScopeArena {
|
||||
log::trace!("removing scope {:?}", id);
|
||||
self.ensure_drop_safety(id);
|
||||
|
||||
// Dispose of any ongoing tasks
|
||||
let mut tasks = self.tasks.tasks.borrow_mut();
|
||||
let mut task_map = self.tasks.task_map.borrow_mut();
|
||||
if let Some(cur_tasks) = task_map.remove(&id) {
|
||||
for task in cur_tasks {
|
||||
tasks.remove(&task);
|
||||
}
|
||||
}
|
||||
|
||||
// Safety:
|
||||
// - ensure_drop_safety ensures that no references to this scope are in use
|
||||
// - this raw pointer is removed from the map
|
||||
@@ -313,12 +327,12 @@ impl ScopeArena {
|
||||
while let Some(id) = cur_el.take() {
|
||||
if let Some(el) = nodes.get(id.0) {
|
||||
let real_el = unsafe { &**el };
|
||||
log::debug!("looking for listener on {:?}", real_el);
|
||||
log::trace!("looking for listener on {:?}", real_el);
|
||||
|
||||
if let VNode::Element(real_el) = real_el {
|
||||
for listener in real_el.listeners.borrow().iter() {
|
||||
if listener.event == event.name {
|
||||
log::debug!("calling listener {:?}", listener.event);
|
||||
log::trace!("calling listener {:?}", listener.event);
|
||||
if state.canceled.get() {
|
||||
// stop bubbling if canceled
|
||||
break;
|
||||
@@ -396,7 +410,10 @@ impl ScopeArena {
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Scope<'a, P = ()> {
|
||||
/// The internal ScopeState for this component
|
||||
pub scope: &'a ScopeState,
|
||||
|
||||
/// The props for this component
|
||||
pub props: &'a P,
|
||||
}
|
||||
|
||||
@@ -423,7 +440,7 @@ impl<'a, P> std::ops::Deref for Scope<'a, P> {
|
||||
/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM and across time. ScopeIDs will never be reused
|
||||
/// once a component has been unmounted.
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
||||
pub struct ScopeId(pub usize);
|
||||
|
||||
/// A task's unique identifier.
|
||||
@@ -432,7 +449,13 @@ pub struct ScopeId(pub usize);
|
||||
/// 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);
|
||||
pub struct TaskId {
|
||||
/// The global ID of the task
|
||||
pub id: usize,
|
||||
|
||||
/// The original scope that this task was scheduled in
|
||||
pub scope: ScopeId,
|
||||
}
|
||||
|
||||
/// Every component in Dioxus is represented by a `ScopeState`.
|
||||
///
|
||||
@@ -466,7 +489,7 @@ pub struct ScopeState {
|
||||
pub(crate) hook_idx: Cell<usize>,
|
||||
|
||||
// shared state -> todo: move this out of scopestate
|
||||
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
|
||||
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Box<dyn Any>>>,
|
||||
pub(crate) tasks: Rc<TaskQueue>,
|
||||
}
|
||||
|
||||
@@ -579,12 +602,17 @@ impl ScopeState {
|
||||
self.our_arena_idx
|
||||
}
|
||||
|
||||
/// Get a handle to the raw update scheduler channel
|
||||
pub fn scheduler_channel(&self) -> UnboundedSender<SchedulerMsg> {
|
||||
self.tasks.sender.clone()
|
||||
}
|
||||
|
||||
/// Create a subscription that schedules a future render for the reference component
|
||||
///
|
||||
/// ## Notice: you should prefer using prepare_update and get_scope_id
|
||||
pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
|
||||
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
||||
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
||||
Rc::new(move || {
|
||||
Arc::new(move || {
|
||||
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
|
||||
})
|
||||
}
|
||||
@@ -594,9 +622,9 @@ impl ScopeState {
|
||||
/// A component's ScopeId can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
|
||||
///
|
||||
/// This method should be used when you want to schedule an update for a component
|
||||
pub fn schedule_update_any(&self) -> Rc<dyn Fn(ScopeId)> {
|
||||
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||
let chan = self.tasks.sender.clone();
|
||||
Rc::new(move |id| {
|
||||
Arc::new(move |id| {
|
||||
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
|
||||
})
|
||||
}
|
||||
@@ -648,13 +676,11 @@ impl ScopeState {
|
||||
/// rsx!(cx, div { "hello {state.0}" })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn provide_context<T: 'static>(&self, value: T) -> Rc<T> {
|
||||
let value = Rc::new(value);
|
||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||
self.shared_contexts
|
||||
.borrow_mut()
|
||||
.insert(TypeId::of::<T>(), value.clone())
|
||||
.map(|f| f.downcast::<T>().ok())
|
||||
.flatten();
|
||||
.insert(TypeId::of::<T>(), Box::new(value.clone()))
|
||||
.and_then(|f| f.downcast::<T>().ok());
|
||||
value
|
||||
}
|
||||
|
||||
@@ -676,16 +702,13 @@ impl ScopeState {
|
||||
/// rsx!(cx, div { "hello {state.0}" })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn provide_root_context<T: 'static>(&self, value: T) -> Rc<T> {
|
||||
let value = Rc::new(value);
|
||||
|
||||
pub fn provide_root_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||
// if we *are* the root component, then we can just provide the context directly
|
||||
if self.scope_id() == ScopeId(0) {
|
||||
self.shared_contexts
|
||||
.borrow_mut()
|
||||
.insert(TypeId::of::<T>(), value.clone())
|
||||
.map(|f| f.downcast::<T>().ok())
|
||||
.flatten();
|
||||
.insert(TypeId::of::<T>(), Box::new(value.clone()))
|
||||
.and_then(|f| f.downcast::<T>().ok());
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -698,7 +721,7 @@ impl ScopeState {
|
||||
let exists = parent
|
||||
.shared_contexts
|
||||
.borrow_mut()
|
||||
.insert(TypeId::of::<T>(), value.clone());
|
||||
.insert(TypeId::of::<T>(), Box::new(value.clone()));
|
||||
|
||||
if exists.is_some() {
|
||||
log::warn!("Context already provided to parent scope - replacing it");
|
||||
@@ -713,9 +736,9 @@ impl ScopeState {
|
||||
}
|
||||
|
||||
/// Try to retrieve a SharedState with type T from the any parent Scope.
|
||||
pub fn consume_context<T: 'static>(&self) -> Option<Rc<T>> {
|
||||
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
||||
Some(shared.clone().downcast::<T>().unwrap())
|
||||
Some((*shared.downcast_ref::<T>().unwrap()).clone())
|
||||
} else {
|
||||
let mut search_parent = self.parent_scope;
|
||||
|
||||
@@ -723,7 +746,7 @@ impl ScopeState {
|
||||
// safety: all parent pointers are valid thanks to the bump arena
|
||||
let parent = unsafe { &*parent_ptr };
|
||||
if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
||||
return Some(shared.clone().downcast::<T>().unwrap());
|
||||
return Some(shared.downcast_ref::<T>().unwrap().clone());
|
||||
}
|
||||
search_parent = parent.parent_scope;
|
||||
}
|
||||
@@ -739,7 +762,7 @@ impl ScopeState {
|
||||
.unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx))
|
||||
.unwrap();
|
||||
|
||||
self.tasks.push_fut(fut)
|
||||
self.tasks.spawn(self.our_arena_idx, fut)
|
||||
}
|
||||
|
||||
/// Spawns the future but does not return the TaskId
|
||||
@@ -747,9 +770,24 @@ impl ScopeState {
|
||||
self.push_future(fut);
|
||||
}
|
||||
|
||||
// todo: attach some state to the future to know if we should poll it
|
||||
/// Spawn a future that Dioxus will never clean up
|
||||
///
|
||||
/// This is good for tasks that need to be run after the component has been dropped.
|
||||
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
// wake up the scheduler if it is sleeping
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx))
|
||||
.unwrap();
|
||||
|
||||
// The root scope will never be unmounted so we can just add the task at the top of the app
|
||||
self.tasks.spawn(ScopeId(0), fut)
|
||||
}
|
||||
|
||||
/// Informs the scheduler that this task is no longer needed and should be removed
|
||||
/// on next poll.
|
||||
pub fn remove_future(&self, id: TaskId) {
|
||||
self.tasks.remove_fut(id);
|
||||
self.tasks.remove(id);
|
||||
}
|
||||
|
||||
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
|
||||
@@ -933,36 +971,43 @@ impl BumpFrame {
|
||||
|
||||
pub(crate) struct TaskQueue {
|
||||
pub(crate) tasks: RefCell<FxHashMap<TaskId, InnerTask>>,
|
||||
pub(crate) task_map: RefCell<FxHashMap<ScopeId, HashSet<TaskId>>>,
|
||||
gen: Cell<usize>,
|
||||
sender: UnboundedSender<SchedulerMsg>,
|
||||
}
|
||||
|
||||
pub(crate) type InnerTask = Pin<Box<dyn Future<Output = ()>>>;
|
||||
impl TaskQueue {
|
||||
fn new(sender: UnboundedSender<SchedulerMsg>) -> Rc<Self> {
|
||||
Rc::new(Self {
|
||||
tasks: RefCell::new(FxHashMap::default()),
|
||||
gen: Cell::new(0),
|
||||
sender,
|
||||
})
|
||||
}
|
||||
fn push_fut(&self, task: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
fn spawn(&self, scope: ScopeId, task: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
let pinned = Box::pin(task);
|
||||
let id = self.gen.get();
|
||||
self.gen.set(id + 1);
|
||||
let tid = TaskId(id);
|
||||
let tid = TaskId { id, scope };
|
||||
|
||||
self.tasks.borrow_mut().insert(tid, pinned);
|
||||
|
||||
// also add to the task map
|
||||
// when the component is unmounted we know to remove it from the map
|
||||
self.task_map
|
||||
.borrow_mut()
|
||||
.entry(scope)
|
||||
.or_default()
|
||||
.insert(tid);
|
||||
|
||||
tid
|
||||
}
|
||||
fn remove_fut(&self, id: TaskId) {
|
||||
|
||||
fn remove(&self, id: TaskId) {
|
||||
if let Ok(mut tasks) = self.tasks.try_borrow_mut() {
|
||||
let _ = tasks.remove(&id);
|
||||
} else {
|
||||
// todo: it should be okay to remote a fut while the queue is being polled
|
||||
// However, it's not currently possible to do that.
|
||||
log::trace!("Unable to remove task from task queue. This is probably a bug.");
|
||||
}
|
||||
|
||||
// the task map is still around, but it'll be removed when the scope is unmounted
|
||||
if let Some(task_map) = self.task_map.borrow_mut().get_mut(&id.scope) {
|
||||
task_map.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_tasks(&self) -> bool {
|
||||
!self.tasks.borrow().is_empty()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::innerlude::*;
|
||||
|
||||
/// An iterator that only yields "real" [`Element`]s. IE only Elements that are
|
||||
/// not [`VNode::VComponent`] or [`VNode::VFragment`], .
|
||||
pub struct ElementIdIterator<'a> {
|
||||
vdom: &'a VirtualDom,
|
||||
|
||||
@@ -9,6 +11,9 @@ pub struct ElementIdIterator<'a> {
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -114,15 +114,18 @@ pub struct VirtualDom {
|
||||
),
|
||||
}
|
||||
|
||||
/// The type of message that can be sent to the scheduler.
|
||||
///
|
||||
/// These messages control how the scheduler will process updates to the UI.
|
||||
#[derive(Debug)]
|
||||
pub enum SchedulerMsg {
|
||||
// events from the host
|
||||
/// Events from the Renderer
|
||||
Event(UserEvent),
|
||||
|
||||
// setstate
|
||||
/// Immediate updates from Components that mark them as dirty
|
||||
Immediate(ScopeId),
|
||||
|
||||
// an async task pushed from an event handler (or just spawned)
|
||||
/// New tasks from components that should be polled when the next poll is ready
|
||||
NewTask(ScopeId),
|
||||
}
|
||||
|
||||
@@ -388,6 +391,9 @@ impl VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an individual message for the scheduler.
|
||||
///
|
||||
/// This will either call an event listener or mark a component as dirty.
|
||||
pub fn process_message(&mut self, msg: SchedulerMsg) {
|
||||
match msg {
|
||||
SchedulerMsg::NewTask(_id) => {
|
||||
@@ -469,7 +475,7 @@ impl VirtualDom {
|
||||
h1.cmp(&h2).reverse()
|
||||
});
|
||||
|
||||
log::debug!("dirty_scopes: {:?}", self.dirty_scopes);
|
||||
log::trace!("dirty_scopes: {:?}", self.dirty_scopes);
|
||||
|
||||
if let Some(scopeid) = self.dirty_scopes.pop() {
|
||||
if !ran_scopes.contains(&scopeid) {
|
||||
@@ -481,12 +487,15 @@ impl VirtualDom {
|
||||
|
||||
let DiffState { mutations, .. } = diff_state;
|
||||
|
||||
log::debug!("succesffuly resolved scopes {:?}", mutations.dirty_scopes);
|
||||
log::trace!("succesffuly resolved scopes {:?}", mutations.dirty_scopes);
|
||||
|
||||
for scope in &mutations.dirty_scopes {
|
||||
self.dirty_scopes.remove(scope);
|
||||
}
|
||||
|
||||
committed_mutations.push(mutations);
|
||||
if !mutations.edits.is_empty() {
|
||||
committed_mutations.push(mutations);
|
||||
}
|
||||
|
||||
// todo: pause the diff machine
|
||||
// if diff_state.work(&mut deadline) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
struct_lit_width = 80
|
||||
@@ -1,53 +0,0 @@
|
||||
# Testing of Dioxus core
|
||||
|
||||
|
||||
NodeFactory
|
||||
- [] rsx, html, NodeFactory generate the same structures
|
||||
|
||||
Diffing
|
||||
- [x] create elements
|
||||
- [x] create text
|
||||
- [x] create fragments
|
||||
- [x] create empty fragments (placeholders)
|
||||
- [x] diff elements
|
||||
- [x] diff from element/text to fragment
|
||||
- [x] diff from element/text to empty fragment
|
||||
- [x] diff to element with children works too
|
||||
- [x] replace with works forward
|
||||
- [x] replace with works backward
|
||||
- [x] un-keyed diffing
|
||||
- [x] keyed diffing
|
||||
- [x] keyed diffing out of order
|
||||
- [x] keyed diffing with prefix/suffix
|
||||
- [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
|
||||
- [] unwrapping shared context if it doesn't exist works too
|
||||
|
||||
Suspense
|
||||
- [] use_suspense generates suspended nodes
|
||||
|
||||
|
||||
Hooks
|
||||
- [] Drop order is maintained
|
||||
- [] Shared hook state is okay
|
||||
- [] use_hook works
|
||||
- [] use_ref works
|
||||
- [] use_noderef works
|
||||
- [] use_provide_state
|
||||
- [] use_consume_state
|
||||
|
||||
|
||||
VirtualDOM API
|
||||
- [] work
|
||||
- [] rebuild
|
||||
- [] change props
|
||||
@@ -1,49 +0,0 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[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,217 +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::*, DomEdit};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
mod test_logging;
|
||||
use DomEdit::*;
|
||||
|
||||
fn new_dom<P: 'static + Send>(app: Component<P>, props: P) -> VirtualDom {
|
||||
const IS_LOGGING_ENABLED: bool = false;
|
||||
test_logging::set_up_logging(IS_LOGGING_ENABLED);
|
||||
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,733 +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::*, DomEdit};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
mod test_logging;
|
||||
|
||||
fn new_dom() -> VirtualDom {
|
||||
const IS_LOGGING_ENABLED: bool = false;
|
||||
test_logging::set_up_logging(IS_LOGGING_ENABLED);
|
||||
VirtualDom::new(|cx| rsx!(cx, "hi"))
|
||||
}
|
||||
|
||||
use 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);
|
||||
log::debug!("{:#?}", &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);
|
||||
log::debug!("{:?}", &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);
|
||||
log::debug!("{:?}", 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);
|
||||
log::debug!("{:#?}", 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);
|
||||
log::debug!("{:#?}", &changes);
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
// move 4 to after 6
|
||||
PushRoot { root: 1 },
|
||||
InsertAfter { n: 1, root: 3 },
|
||||
// remove 7
|
||||
|
||||
// create 9 and insert before 6
|
||||
CreateElement { root: 5, tag: "div" },
|
||||
InsertBefore { n: 1, root: 3 },
|
||||
// create 0 and insert before 5
|
||||
CreateElement { root: 6, 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);
|
||||
log::debug!("{:#?}", &changes);
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
CreateElement { root: 6, 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 },
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -1,68 +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::*, DomEdit, ScopeId};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
mod test_logging;
|
||||
use DomEdit::*;
|
||||
|
||||
const IS_LOGGING_ENABLED: bool = false;
|
||||
|
||||
fn new_dom<P: 'static + Send>(app: Component<P>, props: P) -> VirtualDom {
|
||||
test_logging::set_up_logging(IS_LOGGING_ENABLED);
|
||||
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;
|
||||
}
|
||||
|
||||
rsx!(cx, 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,269 +0,0 @@
|
||||
#![allow(unused, non_upper_case_globals)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
//! Tests for the lifecycle of components.
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core::DomEdit::*;
|
||||
use dioxus_core::ScopeId;
|
||||
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
mod test_logging;
|
||||
|
||||
const IS_LOGGING_ENABLED: bool = true;
|
||||
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();
|
||||
|
||||
log::debug!("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 {
|
||||
log::debug!("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,420 +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::*, SchedulerMsg, ScopeId};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
mod test_logging;
|
||||
|
||||
const IS_LOGGING_ENABLED: bool = false;
|
||||
|
||||
fn new_dom<P: 'static + Send>(app: Component<P>, props: P) -> VirtualDom {
|
||||
test_logging::set_up_logging(IS_LOGGING_ENABLED);
|
||||
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()
|
||||
borrowed_child(na: name)
|
||||
borrowed_child(na: name)
|
||||
borrowed_child(na: name)
|
||||
borrowed_child(na: name)
|
||||
borrowed_child(na: name)
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct BorrowedProps<'a> {
|
||||
na: &'a str,
|
||||
}
|
||||
|
||||
fn borrowed_child<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element {
|
||||
rsx!(cx, div {
|
||||
"goodbye {cx.props.na}"
|
||||
child()
|
||||
child()
|
||||
})
|
||||
}
|
||||
|
||||
fn child(cx: Scope) -> Element {
|
||||
rsx!(cx, 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 {
|
||||
rsx!(cx, 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 {
|
||||
rsx!(cx, "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");
|
||||
rsx!(cx, "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")));
|
||||
rsx!(cx, 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 {
|
||||
rsx!(cx, div { child(a: "abcdef".to_string()) })
|
||||
} else {
|
||||
rsx!(cx, 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);
|
||||
rsx!(cx, 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 {
|
||||
rsx!(cx, 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);
|
||||
rsx!(cx, 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 {
|
||||
rsx!(cx, 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 {
|
||||
rsx!(cx,
|
||||
NavBrand {}
|
||||
div {
|
||||
NavStart {}
|
||||
NavEnd {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn NavBrand(cx: Scope) -> Element {
|
||||
rsx!(cx, div {})
|
||||
}
|
||||
|
||||
fn NavStart(cx: Scope) -> Element {
|
||||
rsx!(cx, div {})
|
||||
}
|
||||
|
||||
fn NavEnd(cx: Scope) -> Element {
|
||||
rsx!(cx, 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,107 +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::*, DomEdit, ScopeId};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
mod test_logging;
|
||||
|
||||
fn new_dom() -> VirtualDom {
|
||||
const IS_LOGGING_ENABLED: bool = false;
|
||||
test_logging::set_up_logging(IS_LOGGING_ENABLED);
|
||||
VirtualDom::new(|cx| rsx!(cx, "hi"))
|
||||
}
|
||||
|
||||
use 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,130 +0,0 @@
|
||||
#![allow(unused, non_upper_case_globals, non_snake_case)]
|
||||
|
||||
use dioxus::{prelude::*, DomEdit, Mutations, SchedulerMsg, ScopeId};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
use DomEdit::*;
|
||||
|
||||
mod test_logging;
|
||||
|
||||
#[test]
|
||||
fn shared_state_test() {
|
||||
struct MySharedState(&'static str);
|
||||
|
||||
static App: Component = |cx| {
|
||||
cx.provide_context(MySharedState("world!"));
|
||||
cx.render(rsx!(Child {}))
|
||||
};
|
||||
|
||||
static Child: Component = |cx| {
|
||||
let shared = cx.consume_context::<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(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::<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::<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,51 +0,0 @@
|
||||
pub fn set_up_logging(enabled: bool) {
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
|
||||
if !enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
// configure colors for the whole line
|
||||
let colors_line = ColoredLevelConfig::new()
|
||||
.error(Color::Red)
|
||||
.warn(Color::Yellow)
|
||||
// we actually don't need to specify the color for debug and info, they are white by default
|
||||
.info(Color::White)
|
||||
.debug(Color::White)
|
||||
// depending on the terminals color scheme, this is the same as the background color
|
||||
.trace(Color::BrightBlack);
|
||||
|
||||
// configure colors for the name of the level.
|
||||
// since almost all of them are the same as the color for the whole line, we
|
||||
// just clone `colors_line` and overwrite our changes
|
||||
let colors_level = colors_line.info(Color::Green);
|
||||
// here we set up our fern Dispatch
|
||||
|
||||
// when running tests in batch, the logger is re-used, so ignore the logger error
|
||||
let _ = fern::Dispatch::new()
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{color_line}[{level}{color_line}] {message}\x1B[0m",
|
||||
color_line = format_args!(
|
||||
"\x1B[{}m",
|
||||
colors_line.get_color(&record.level()).to_fg_str()
|
||||
),
|
||||
level = colors_level.color(record.level()),
|
||||
message = message,
|
||||
));
|
||||
})
|
||||
// set the default log level. to filter out verbose log messages from dependencies, set
|
||||
// this to Warn and overwrite the log level for your crate.
|
||||
.level(log::LevelFilter::Debug)
|
||||
// .level(log::LevelFilter::Warn)
|
||||
// change log levels for individual modules. Note: This looks for the record's target
|
||||
// field which defaults to the module path but can be overwritten with the `target`
|
||||
// parameter:
|
||||
// `info!(target="special_target", "This log message is about special_target");`
|
||||
// .level_for("dioxus", log::LevelFilter::Debug)
|
||||
// .level_for("dioxus", log::LevelFilter::Info)
|
||||
// .level_for("pretty_colored", log::LevelFilter::Trace)
|
||||
// output to stdout
|
||||
.chain(std::io::stdout())
|
||||
.apply();
|
||||
}
|
||||
@@ -1,99 +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::DomEdit;
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use DomEdit::*;
|
||||
|
||||
#[test]
|
||||
fn app_runs() {
|
||||
static App: Component = |cx| rsx!(cx, 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);
|
||||
}
|
||||
+25
-15
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-desktop"
|
||||
version = "0.1.6"
|
||||
version = "0.2.2"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Dioxus VirtualDOM renderer for a remote webview instance"
|
||||
@@ -12,32 +12,42 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] }
|
||||
argh = "0.1.4"
|
||||
serde = "1.0.120"
|
||||
serde_json = "1.0.61"
|
||||
thiserror = "1.0.23"
|
||||
log = "0.4.13"
|
||||
html-escape = "0.2.9"
|
||||
wry = "0.12.2"
|
||||
futures-channel = "0.3"
|
||||
tokio = { version = "1.12.0", features = [
|
||||
dioxus-core = { path = "../core", version = "^0.2.0", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.0" }
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.0" }
|
||||
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
thiserror = "1.0.30"
|
||||
log = "0.4.14"
|
||||
wry = { version = "0.13.1" }
|
||||
futures-channel = "0.3.21"
|
||||
tokio = { version = "1.16.1", features = [
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"rt",
|
||||
"time",
|
||||
], optional = true, default-features = false }
|
||||
dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" }
|
||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" }
|
||||
webbrowser = "0.5.5"
|
||||
webbrowser = "0.6.0"
|
||||
mime_guess = "2.0.3"
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" }
|
||||
dunce = "1.0.2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9.3"
|
||||
|
||||
[features]
|
||||
default = ["tokio_runtime"]
|
||||
tokio_runtime = ["tokio"]
|
||||
|
||||
devtool = ["wry/devtool"]
|
||||
fullscreen = ["wry/fullscreen"]
|
||||
transparent = ["wry/transparent"]
|
||||
|
||||
tray = ["wry/tray"]
|
||||
ayatana = ["wry/ayatana"]
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
dioxus-hooks = { path = "../hooks" }
|
||||
# image = "0.24.0" # enable this when generating a new desktop image
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
@@ -1,3 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use wry::application::window::Icon;
|
||||
use wry::{
|
||||
application::{
|
||||
event_loop::EventLoop,
|
||||
@@ -11,14 +14,18 @@ use wry::{
|
||||
pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
|
||||
|
||||
pub struct DesktopConfig {
|
||||
pub window: WindowBuilder,
|
||||
pub file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
|
||||
pub protocos: Vec<WryProtocl>,
|
||||
pub(crate) window: WindowBuilder,
|
||||
pub(crate) file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
|
||||
pub(crate) protocols: Vec<WryProtocol>,
|
||||
pub(crate) pre_rendered: Option<String>,
|
||||
pub(crate) event_handler: Option<Box<DynEventHandlerFn>>,
|
||||
pub(crate) disable_context_menu: bool,
|
||||
pub(crate) resource_dir: Option<PathBuf>,
|
||||
pub(crate) custom_head: Option<String>,
|
||||
pub(crate) custom_index: Option<String>,
|
||||
}
|
||||
|
||||
pub type WryProtocl = (
|
||||
pub(crate) type WryProtocol = (
|
||||
String,
|
||||
Box<dyn Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static>,
|
||||
);
|
||||
@@ -28,15 +35,33 @@ impl DesktopConfig {
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
let window = WindowBuilder::new().with_title("Dioxus app");
|
||||
|
||||
Self {
|
||||
event_handler: None,
|
||||
window,
|
||||
protocos: Vec::new(),
|
||||
protocols: Vec::new(),
|
||||
file_drop_handler: None,
|
||||
pre_rendered: None,
|
||||
disable_context_menu: !cfg!(debug_assertions),
|
||||
resource_dir: None,
|
||||
custom_head: None,
|
||||
custom_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// set the directory from which assets will be searched in release mode
|
||||
pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
|
||||
self.resource_dir = Some(path.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether or not the right-click context menu should be disabled.
|
||||
pub fn with_disable_context_menu(&mut self, disable: bool) -> &mut Self {
|
||||
self.disable_context_menu = disable;
|
||||
self
|
||||
}
|
||||
|
||||
/// With pre-rendered HTML content
|
||||
pub fn with_prerendered(&mut self, content: String) -> &mut Self {
|
||||
self.pre_rendered = Some(content);
|
||||
self
|
||||
@@ -75,7 +100,41 @@ impl DesktopConfig {
|
||||
where
|
||||
F: Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static,
|
||||
{
|
||||
self.protocos.push((name, Box::new(handler)));
|
||||
self.protocols.push((name, Box::new(handler)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a custom icon for this application
|
||||
pub fn with_icon(&mut self, icon: Icon) -> &mut Self {
|
||||
self.window.window.window_icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
/// Inject additional content into the document's HEAD.
|
||||
///
|
||||
/// This is useful for loading CSS libraries, JS libraries, etc.
|
||||
pub fn with_custom_head(&mut self, head: String) -> &mut Self {
|
||||
self.custom_head = Some(head);
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a custom index.html instead of the default Dioxus one.
|
||||
///
|
||||
/// Make sure your index.html is valid HTML.
|
||||
///
|
||||
/// Dioxus injects some loader code into the closing body tag. Your document
|
||||
/// must include a body element!
|
||||
pub fn with_custom_index(&mut self, index: String) -> &mut Self {
|
||||
self.custom_index = Some(index);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopConfig {
|
||||
pub(crate) fn with_default_icon(mut self) -> Self {
|
||||
let bin: &[u8] = include_bytes!("./assets/default_icon.bin");
|
||||
let rgba = Icon::from_rgba(bin.to_owned(), 460, 460).expect("image parse failed");
|
||||
self.window.window.window_icon = Some(rgba);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -85,3 +144,31 @@ impl Default for DesktopConfig {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// dirty trick, avoid introducing `image` at runtime
|
||||
// TODO: use serde when `Icon` impl serde
|
||||
//
|
||||
// This function should only be enabled when generating new icons.
|
||||
//
|
||||
// #[test]
|
||||
// #[ignore]
|
||||
// fn prepare_default_icon() {
|
||||
// use image::io::Reader as ImageReader;
|
||||
// use image::ImageFormat;
|
||||
// use std::fs::File;
|
||||
// use std::io::Cursor;
|
||||
// use std::io::Write;
|
||||
// use std::path::PathBuf;
|
||||
// let png: &[u8] = include_bytes!("default_icon.png");
|
||||
// let mut reader = ImageReader::new(Cursor::new(png));
|
||||
// reader.set_format(ImageFormat::Png);
|
||||
// let icon = reader.decode().unwrap();
|
||||
// let bin = PathBuf::from(file!())
|
||||
// .parent()
|
||||
// .unwrap()
|
||||
// .join("default_icon.bin");
|
||||
// println!("{:?}", bin);
|
||||
// let mut file = File::create(bin).unwrap();
|
||||
// file.write_all(icon.as_bytes()).unwrap();
|
||||
// println!("({}, {})", icon.width(), icon.height())
|
||||
// }
|
||||
@@ -0,0 +1,107 @@
|
||||
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
||||
use dioxus_core::*;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::Arc,
|
||||
sync::{atomic::AtomicBool, Mutex},
|
||||
};
|
||||
use wry::{
|
||||
self,
|
||||
application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId},
|
||||
webview::WebView,
|
||||
};
|
||||
|
||||
pub(super) struct DesktopController {
|
||||
pub(super) webviews: HashMap<WindowId, WebView>,
|
||||
pub(super) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
|
||||
pub(super) quit_app_on_close: bool,
|
||||
pub(super) is_ready: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl DesktopController {
|
||||
// Launch the virtualdom on its own thread managed by tokio
|
||||
// returns the desktop state
|
||||
pub(super) fn new_on_tokio<P: Send + 'static>(
|
||||
root: Component<P>,
|
||||
props: P,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> Self {
|
||||
let edit_queue = Arc::new(Mutex::new(Vec::new()));
|
||||
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
|
||||
|
||||
let pending_edits = edit_queue.clone();
|
||||
let return_sender = sender.clone();
|
||||
let desktop_context_proxy = proxy.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
let _ = proxy.send_event(UserWindowEvent::Update);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
pending_edits,
|
||||
sender: return_sender,
|
||||
webviews: HashMap::new(),
|
||||
is_ready: Arc::new(AtomicBool::new(false)),
|
||||
quit_app_on_close: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) {
|
||||
self.webviews.remove(&window_id);
|
||||
|
||||
if self.webviews.is_empty() && self.quit_app_on_close {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
|
||||
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 (_id, view) = self.webviews.iter_mut().next().unwrap();
|
||||
|
||||
for edit in queue.drain(..) {
|
||||
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,26 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::controller::DesktopController;
|
||||
use dioxus_core::ScopeState;
|
||||
use wry::application::event_loop::ControlFlow;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
use wry::application::window::Fullscreen as WryFullscreen;
|
||||
|
||||
use crate::UserWindowEvent;
|
||||
use UserWindowEvent::*;
|
||||
|
||||
type ProxyType = EventLoopProxy<UserWindowEvent>;
|
||||
pub type ProxyType = EventLoopProxy<UserWindowEvent>;
|
||||
|
||||
/// Desktop-Window handle api context
|
||||
/// Get an imperative handle to the current window
|
||||
pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
||||
cx.use_hook(|_| cx.consume_context::<DesktopContext>())
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// An imperative interface to the current window.
|
||||
///
|
||||
/// you can use this context control some window event
|
||||
/// To get a handle to the current window, use the [`use_window`] hook.
|
||||
///
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// you can use `cx.consume_context::<DesktopContext>` to get this context
|
||||
///
|
||||
@@ -18,7 +29,8 @@ type ProxyType = EventLoopProxy<UserWindowEvent>;
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct DesktopContext {
|
||||
proxy: ProxyType,
|
||||
/// The wry/tao proxy to the current window
|
||||
pub proxy: ProxyType,
|
||||
}
|
||||
|
||||
impl DesktopContext {
|
||||
@@ -35,81 +47,161 @@ impl DesktopContext {
|
||||
/// onmousedown: move |_| { desktop.drag_window(); }
|
||||
/// ```
|
||||
pub fn drag(&self) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::DragWindow);
|
||||
let _ = self.proxy.send_event(DragWindow);
|
||||
}
|
||||
|
||||
/// set window minimize state
|
||||
pub fn set_minimized(&self, minimized: bool) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized));
|
||||
let _ = self.proxy.send_event(Minimize(minimized));
|
||||
}
|
||||
|
||||
/// set window maximize state
|
||||
pub fn set_maximized(&self, maximized: bool) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized));
|
||||
let _ = self.proxy.send_event(Maximize(maximized));
|
||||
}
|
||||
|
||||
/// toggle window maximize state
|
||||
pub fn toggle_maximized(&self) {
|
||||
let _ = self.proxy.send_event(MaximizeToggle);
|
||||
}
|
||||
|
||||
/// set window visible or not
|
||||
pub fn set_visible(&self, visible: bool) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::Visible(visible));
|
||||
let _ = self.proxy.send_event(Visible(visible));
|
||||
}
|
||||
|
||||
/// close window
|
||||
pub fn close(&self) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::CloseWindow);
|
||||
let _ = self.proxy.send_event(CloseWindow);
|
||||
}
|
||||
|
||||
/// set window to focus
|
||||
pub fn focus(&self) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::FocusWindow);
|
||||
let _ = self.proxy.send_event(FocusWindow);
|
||||
}
|
||||
|
||||
/// change window to fullscreen
|
||||
pub fn set_fullscreen(&self, fullscreen: bool) {
|
||||
let _ = self
|
||||
.proxy
|
||||
.send_event(UserWindowEvent::Fullscreen(fullscreen));
|
||||
let _ = self.proxy.send_event(Fullscreen(fullscreen));
|
||||
}
|
||||
|
||||
/// set resizable state
|
||||
pub fn set_resizable(&self, resizable: bool) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable));
|
||||
let _ = self.proxy.send_event(Resizable(resizable));
|
||||
}
|
||||
|
||||
/// set the window always on top
|
||||
pub fn set_always_on_top(&self, top: bool) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top));
|
||||
let _ = self.proxy.send_event(AlwaysOnTop(top));
|
||||
}
|
||||
|
||||
// set cursor visible or not
|
||||
/// set cursor visible or not
|
||||
pub fn set_cursor_visible(&self, visible: bool) {
|
||||
let _ = self
|
||||
.proxy
|
||||
.send_event(UserWindowEvent::CursorVisible(visible));
|
||||
let _ = self.proxy.send_event(CursorVisible(visible));
|
||||
}
|
||||
|
||||
// set cursor grab
|
||||
/// set cursor grab
|
||||
pub fn set_cursor_grab(&self, grab: bool) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::CursorGrab(grab));
|
||||
let _ = self.proxy.send_event(CursorGrab(grab));
|
||||
}
|
||||
|
||||
/// set window title
|
||||
pub fn set_title(&self, title: &str) {
|
||||
let _ = self
|
||||
.proxy
|
||||
.send_event(UserWindowEvent::SetTitle(String::from(title)));
|
||||
let _ = self.proxy.send_event(SetTitle(String::from(title)));
|
||||
}
|
||||
|
||||
/// change window to borderless
|
||||
pub fn set_decorations(&self, decoration: bool) {
|
||||
let _ = self
|
||||
.proxy
|
||||
.send_event(UserWindowEvent::SetDecorations(decoration));
|
||||
let _ = self.proxy.send_event(SetDecorations(decoration));
|
||||
}
|
||||
|
||||
/// opens DevTool window
|
||||
pub fn devtool(&self) {
|
||||
let _ = self.proxy.send_event(DevTool);
|
||||
}
|
||||
|
||||
/// run (evaluate) a script in the WebView context
|
||||
pub fn eval(&self, script: impl std::string::ToString) {
|
||||
let _ = self.proxy.send_event(Eval(script.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
/// use this function can get the `DesktopContext` context.
|
||||
pub fn use_window(cx: &ScopeState) -> &Rc<DesktopContext> {
|
||||
cx.use_hook(|_| cx.consume_context::<DesktopContext>())
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
#[derive(Debug)]
|
||||
pub enum UserWindowEvent {
|
||||
Update,
|
||||
|
||||
CloseWindow,
|
||||
DragWindow,
|
||||
FocusWindow,
|
||||
|
||||
Visible(bool),
|
||||
Minimize(bool),
|
||||
Maximize(bool),
|
||||
MaximizeToggle,
|
||||
Resizable(bool),
|
||||
AlwaysOnTop(bool),
|
||||
Fullscreen(bool),
|
||||
|
||||
CursorVisible(bool),
|
||||
CursorGrab(bool),
|
||||
|
||||
SetTitle(String),
|
||||
SetDecorations(bool),
|
||||
|
||||
DevTool,
|
||||
|
||||
Eval(String),
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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))));
|
||||
}
|
||||
}
|
||||
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),
|
||||
|
||||
DevTool => webview.devtool(),
|
||||
|
||||
Eval(code) => webview
|
||||
.evaluate_script(code.as_str())
|
||||
.expect("eval shouldn't panic"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) {
|
||||
let desktop = use_window(cx).clone();
|
||||
|
||||
cx.use_hook(|_| move |script| desktop.eval(script))
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Convert a serialized event to an event Trigger
|
||||
//!
|
||||
//! Convert a serialized event to an event trigger
|
||||
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
@@ -7,27 +6,49 @@ use std::sync::Arc;
|
||||
use dioxus_core::{ElementId, EventPriority, UserEvent};
|
||||
use dioxus_html::on::*;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct IpcMessage {
|
||||
method: String,
|
||||
params: serde_json::Value,
|
||||
}
|
||||
|
||||
impl IpcMessage {
|
||||
pub(crate) fn method(&self) -> &str {
|
||||
self.method.as_str()
|
||||
}
|
||||
|
||||
pub(crate) fn params(self) -> serde_json::Value {
|
||||
self.params
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
|
||||
match serde_json::from_str(payload) {
|
||||
Ok(message) => Some(message),
|
||||
Err(e) => {
|
||||
log::error!("could not parse IPC message, error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct ImEvent {
|
||||
event: String,
|
||||
mounted_dom_id: u64,
|
||||
// scope: u64,
|
||||
contents: serde_json::Value,
|
||||
}
|
||||
|
||||
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
||||
let ims: Vec<ImEvent> = serde_json::from_value(val).unwrap();
|
||||
|
||||
let ImEvent {
|
||||
event,
|
||||
mounted_dom_id,
|
||||
contents,
|
||||
} = ims.into_iter().next().unwrap();
|
||||
} = serde_json::from_value(val).unwrap();
|
||||
|
||||
// let scope_id = ScopeId(scope as usize);
|
||||
let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
|
||||
|
||||
let name = event_name_from_typ(&event);
|
||||
let name = event_name_from_type(&event);
|
||||
let event = make_synthetic_event(&event, contents);
|
||||
|
||||
UserEvent {
|
||||
@@ -63,9 +84,9 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Sen
|
||||
Arc::new(serde_json::from_value::<FormData>(val).unwrap())
|
||||
}
|
||||
|
||||
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
|
||||
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
|
||||
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
|
||||
"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"
|
||||
@@ -105,7 +126,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Sen
|
||||
}
|
||||
}
|
||||
|
||||
fn event_name_from_typ(typ: &str) -> &'static str {
|
||||
fn event_name_from_type(typ: &str) -> &'static str {
|
||||
match typ {
|
||||
"copy" => "copy",
|
||||
"cut" => "cut",
|
||||
@@ -128,6 +149,7 @@ fn event_name_from_typ(typ: &str) -> &'static str {
|
||||
"click" => "click",
|
||||
"contextmenu" => "contextmenu",
|
||||
"doubleclick" => "doubleclick",
|
||||
"dblclick" => "dblclick",
|
||||
"drag" => "drag",
|
||||
"dragend" => "dragend",
|
||||
"dragenter" => "dragenter",
|
||||
@@ -188,8 +210,8 @@ fn event_name_from_typ(typ: &str) -> &'static str {
|
||||
"volumechange" => "volumechange",
|
||||
"waiting" => "waiting",
|
||||
"toggle" => "toggle",
|
||||
_ => {
|
||||
panic!("unsupported event type")
|
||||
a => {
|
||||
panic!("unsupported event type {:?}", a);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8" />
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="main">
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
import("./index.js").then(function (module) {
|
||||
module.main();
|
||||
});
|
||||
</script>
|
||||
|
||||
<head>
|
||||
<title>Dioxus app</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- CUSTOM HEAD -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<!-- MODULE LOADER -->
|
||||
</body>
|
||||
</html>
|
||||
+76
-359
@@ -1,81 +1,31 @@
|
||||
//! Dioxus Desktop Renderer
|
||||
//!
|
||||
//! Render the Dioxus VirtualDom using the platform's native WebView implementation.
|
||||
//!
|
||||
//! # Desktop
|
||||
//!
|
||||
//! One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
|
||||
//!
|
||||
//! Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
|
||||
//!
|
||||
//!
|
||||
//! ## Getting Set up
|
||||
//!
|
||||
//! Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project:
|
||||
//!
|
||||
//! ```shell
|
||||
//! $ cargo new --bin demo
|
||||
//! $ cd app
|
||||
//! ```
|
||||
//!
|
||||
//! Add Dioxus with the `desktop` feature:
|
||||
//!
|
||||
//! ```shell
|
||||
//! $ cargo add dioxus --features desktop
|
||||
//! ```
|
||||
//!
|
||||
//! Edit your `main.rs`:
|
||||
//!
|
||||
//! ```rust
|
||||
//! // main.rs
|
||||
//! use dioxus::prelude::*;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! dioxus::desktop::launch(app);
|
||||
//! }
|
||||
//!
|
||||
//! fn app(cx: Scope) -> Element {
|
||||
//! cx.render(rsx!{
|
||||
//! div {
|
||||
//! "hello world!"
|
||||
//! }
|
||||
//! })
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/).
|
||||
//!
|
||||
//! ## Future Steps
|
||||
//!
|
||||
//! Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't!
|
||||
#![doc = include_str!("readme.md")]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
pub mod cfg;
|
||||
pub mod desktop_context;
|
||||
pub mod escape;
|
||||
pub mod events;
|
||||
mod cfg;
|
||||
mod controller;
|
||||
mod desktop_context;
|
||||
mod escape;
|
||||
mod events;
|
||||
mod protocol;
|
||||
|
||||
use desktop_context::UserWindowEvent;
|
||||
pub use desktop_context::{use_eval, use_window, DesktopContext};
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
|
||||
use crate::events::trigger_from_serialized;
|
||||
use cfg::DesktopConfig;
|
||||
pub use desktop_context::use_window;
|
||||
use desktop_context::DesktopContext;
|
||||
use controller::DesktopController;
|
||||
use dioxus_core::*;
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::atomic::AtomicBool,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use events::parse_ipc_message;
|
||||
use tao::{
|
||||
event::{Event, StartCause, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::{
|
||||
application::{event_loop::EventLoopProxy, window::Fullscreen},
|
||||
webview::RpcRequest,
|
||||
webview::{WebView, WebViewBuilder},
|
||||
window::Window,
|
||||
};
|
||||
use wry::webview::WebViewBuilder;
|
||||
|
||||
/// Launch the WebView and run the event loop.
|
||||
///
|
||||
@@ -152,7 +102,7 @@ pub fn launch_with_props<P: 'static + Send>(
|
||||
props: P,
|
||||
builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig,
|
||||
) {
|
||||
let mut cfg = DesktopConfig::default();
|
||||
let mut cfg = DesktopConfig::default().with_default_icon();
|
||||
builder(&mut cfg);
|
||||
|
||||
let event_loop = EventLoop::with_user_event();
|
||||
@@ -173,30 +123,33 @@ pub fn launch_with_props<P: 'static + Send>(
|
||||
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_rpc_handler(move |_window: &Window, req: RpcRequest| {
|
||||
match req.method.as_str() {
|
||||
"user_event" => {
|
||||
let event = events::trigger_from_serialized(req.params.unwrap());
|
||||
log::trace!("User event: {:?}", event);
|
||||
sender.unbounded_send(SchedulerMsg::Event(event)).unwrap();
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let _ = proxy.send_event(UserWindowEvent::Update);
|
||||
}
|
||||
"browser_open" => {
|
||||
println!("browser_open");
|
||||
let data = req.params.unwrap();
|
||||
log::trace!("Open browser: {:?}", data);
|
||||
if let Some(arr) = data.as_array() {
|
||||
if let Some(temp) = arr[0].as_object() {
|
||||
.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();
|
||||
}
|
||||
"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) {
|
||||
@@ -205,54 +158,19 @@ pub fn launch_with_props<P: 'static + Send>(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
_ => (),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("invalid IPC message received");
|
||||
});
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |request| {
|
||||
// Any content that 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://", "");
|
||||
|
||||
// all assets shouldbe called from index.html
|
||||
let trimmed = path.trim_start_matches("index.html/");
|
||||
|
||||
if trimmed.is_empty() {
|
||||
wry::http::ResponseBuilder::new()
|
||||
.mimetype("text/html")
|
||||
.body(include_bytes!("./index.html").to_vec())
|
||||
} else if trimmed == "index.js" {
|
||||
wry::http::ResponseBuilder::new()
|
||||
.mimetype("text/javascript")
|
||||
.body(dioxus_interpreter_js::INTERPRTER_JS.as_bytes().to_vec())
|
||||
} else {
|
||||
// Read the file content from file path
|
||||
use std::fs::read;
|
||||
|
||||
let path_buf = std::path::Path::new(trimmed).canonicalize()?;
|
||||
let cur_path = std::path::Path::new(".").canonicalize()?;
|
||||
|
||||
if !path_buf.starts_with(cur_path) {
|
||||
return wry::http::ResponseBuilder::new()
|
||||
.status(wry::http::status::StatusCode::FORBIDDEN)
|
||||
.body(String::from("Forbidden").into_bytes());
|
||||
}
|
||||
|
||||
if !path_buf.exists() {
|
||||
return wry::http::ResponseBuilder::new()
|
||||
.status(wry::http::status::StatusCode::NOT_FOUND)
|
||||
.body(String::from("Not Found").into_bytes());
|
||||
}
|
||||
|
||||
let mime = mime_guess::from_path(&path_buf).first_or_octet_stream();
|
||||
|
||||
// do not let path searching to go two layers beyond the caller level
|
||||
let data = read(path_buf)?;
|
||||
let meta = format!("{}", mime);
|
||||
|
||||
wry::http::ResponseBuilder::new().mimetype(&meta).body(data)
|
||||
}
|
||||
.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
|
||||
@@ -261,10 +179,30 @@ pub fn launch_with_props<P: 'static + Send>(
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
for (name, handler) in cfg.protocos.drain(..) {
|
||||
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 (document.addEventListener) {
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
} else {
|
||||
document.attachEvent('oncontextmenu', function() {
|
||||
window.event.returnValue = false;
|
||||
});
|
||||
}
|
||||
"#,
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_dev_tool(true);
|
||||
}
|
||||
|
||||
desktop.webviews.insert(window_id, webview.build().unwrap());
|
||||
}
|
||||
|
||||
@@ -283,114 +221,8 @@ pub fn launch_with_props<P: 'static + Send>(
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::UserEvent(_evt) => {
|
||||
//
|
||||
match _evt {
|
||||
UserWindowEvent::Update => desktop.try_load_ready_webviews(),
|
||||
UserWindowEvent::DragWindow => {
|
||||
// this loop just run once, because dioxus-desktop is unsupport multi-window.
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
// start to drag the window.
|
||||
// if the drag_window have any err. we don't do anything.
|
||||
|
||||
if window.fullscreen().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = window.drag_window();
|
||||
}
|
||||
}
|
||||
UserWindowEvent::CloseWindow => {
|
||||
// close window
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
UserWindowEvent::Visible(state) => {
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
window.set_visible(state);
|
||||
}
|
||||
}
|
||||
UserWindowEvent::Minimize(state) => {
|
||||
// this loop just run once, because dioxus-desktop is unsupport multi-window.
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
// change window minimized state.
|
||||
window.set_minimized(state);
|
||||
}
|
||||
}
|
||||
UserWindowEvent::Maximize(state) => {
|
||||
// this loop just run once, because dioxus-desktop is unsupport multi-window.
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
// change window maximized state.
|
||||
window.set_maximized(state);
|
||||
}
|
||||
}
|
||||
UserWindowEvent::Fullscreen(state) => {
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
|
||||
let current_monitor = window.current_monitor();
|
||||
|
||||
if current_monitor.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let fullscreen = if state {
|
||||
Some(Fullscreen::Borderless(current_monitor))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
window.set_fullscreen(fullscreen);
|
||||
}
|
||||
}
|
||||
UserWindowEvent::FocusWindow => {
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
window.set_focus();
|
||||
}
|
||||
}
|
||||
UserWindowEvent::Resizable(state) => {
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
window.set_resizable(state);
|
||||
}
|
||||
}
|
||||
UserWindowEvent::AlwaysOnTop(state) => {
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
window.set_always_on_top(state);
|
||||
}
|
||||
}
|
||||
|
||||
UserWindowEvent::CursorVisible(state) => {
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
window.set_cursor_visible(state);
|
||||
}
|
||||
}
|
||||
UserWindowEvent::CursorGrab(state) => {
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
let _ = window.set_cursor_grab(state);
|
||||
}
|
||||
}
|
||||
|
||||
UserWindowEvent::SetTitle(content) => {
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
window.set_title(&content);
|
||||
}
|
||||
}
|
||||
UserWindowEvent::SetDecorations(state) => {
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
window.set_decorations(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::UserEvent(user_event) => {
|
||||
desktop_context::handler(user_event, &mut desktop, control_flow)
|
||||
}
|
||||
Event::MainEventsCleared => {}
|
||||
Event::Resumed => {}
|
||||
@@ -401,118 +233,3 @@ pub fn launch_with_props<P: 'static + Send>(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub enum UserWindowEvent {
|
||||
Update,
|
||||
DragWindow,
|
||||
CloseWindow,
|
||||
FocusWindow,
|
||||
Visible(bool),
|
||||
Minimize(bool),
|
||||
Maximize(bool),
|
||||
Resizable(bool),
|
||||
AlwaysOnTop(bool),
|
||||
Fullscreen(bool),
|
||||
|
||||
CursorVisible(bool),
|
||||
CursorGrab(bool),
|
||||
|
||||
SetTitle(String),
|
||||
SetDecorations(bool),
|
||||
}
|
||||
|
||||
pub struct DesktopController {
|
||||
pub proxy: EventLoopProxy<UserWindowEvent>,
|
||||
pub webviews: HashMap<WindowId, WebView>,
|
||||
pub sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pub pending_edits: Arc<RwLock<VecDeque<String>>>,
|
||||
pub quit_app_on_close: bool,
|
||||
pub is_ready: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl DesktopController {
|
||||
// Launch the virtualdom on its own thread managed by tokio
|
||||
// returns the desktop state
|
||||
pub fn new_on_tokio<P: Send + 'static>(
|
||||
root: Component<P>,
|
||||
props: P,
|
||||
evt: EventLoopProxy<UserWindowEvent>,
|
||||
) -> Self {
|
||||
let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
|
||||
let pending_edits = edit_queue.clone();
|
||||
|
||||
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
|
||||
let return_sender = sender.clone();
|
||||
let proxy = evt.clone();
|
||||
|
||||
let desktop_context_proxy = proxy.clone();
|
||||
std::thread::spawn(move || {
|
||||
// We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
|
||||
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);
|
||||
|
||||
let edits = dom.rebuild();
|
||||
|
||||
edit_queue
|
||||
.write()
|
||||
.unwrap()
|
||||
.push_front(serde_json::to_string(&edits.edits).unwrap());
|
||||
|
||||
loop {
|
||||
dom.wait_for_work().await;
|
||||
let mut muts = dom.work_with_deadline(|| false);
|
||||
|
||||
while let Some(edit) = muts.pop() {
|
||||
edit_queue
|
||||
.write()
|
||||
.unwrap()
|
||||
.push_front(serde_json::to_string(&edit.edits).unwrap());
|
||||
}
|
||||
|
||||
let _ = evt.send_event(UserWindowEvent::Update);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
pending_edits,
|
||||
sender: return_sender,
|
||||
proxy,
|
||||
webviews: HashMap::new(),
|
||||
is_ready: Arc::new(AtomicBool::new(false)),
|
||||
quit_app_on_close: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) {
|
||||
self.webviews.remove(&window_id);
|
||||
|
||||
if self.webviews.is_empty() && self.quit_app_on_close {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_load_ready_webviews(&mut self) {
|
||||
if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let mut queue = self.pending_edits.write().unwrap();
|
||||
let (_id, view) = self.webviews.iter_mut().next().unwrap();
|
||||
|
||||
while let Some(edit) = queue.pop_back() {
|
||||
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
println!("waiting for ready");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use wry::{
|
||||
http::{status::StatusCode, Request, Response, ResponseBuilder},
|
||||
Result,
|
||||
};
|
||||
|
||||
const MODULE_LOADER: &str = r#"
|
||||
<script>
|
||||
import("./index.js").then(function (module) {
|
||||
module.main();
|
||||
});
|
||||
</script>
|
||||
"#;
|
||||
|
||||
pub(super) fn desktop_handler(
|
||||
request: &Request,
|
||||
asset_root: Option<PathBuf>,
|
||||
custom_head: Option<String>,
|
||||
custom_index: Option<String>,
|
||||
) -> Result<Response> {
|
||||
// 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://", "");
|
||||
|
||||
// all assets should be called from index.html
|
||||
let trimmed = path.trim_start_matches("index.html/");
|
||||
|
||||
if trimmed.is_empty() {
|
||||
// If a custom index is provided, just defer to that, expecting the user to know what they're doing.
|
||||
// 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))
|
||||
.into_bytes();
|
||||
ResponseBuilder::new().mimetype("text/html").body(rendered)
|
||||
} 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);
|
||||
|
||||
ResponseBuilder::new()
|
||||
.mimetype("text/html")
|
||||
.body(template.into_bytes())
|
||||
}
|
||||
} else if trimmed == "index.js" {
|
||||
ResponseBuilder::new()
|
||||
.mimetype("text/javascript")
|
||||
.body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec())
|
||||
} else {
|
||||
let asset_root = asset_root
|
||||
.unwrap_or_else(|| get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf()))
|
||||
.canonicalize()?;
|
||||
|
||||
let asset = asset_root.join(trimmed).canonicalize()?;
|
||||
|
||||
if !asset.starts_with(asset_root) {
|
||||
return ResponseBuilder::new()
|
||||
.status(StatusCode::FORBIDDEN)
|
||||
.body(String::from("Forbidden").into_bytes());
|
||||
}
|
||||
|
||||
if !asset.exists() {
|
||||
return ResponseBuilder::new()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(String::from("Not Found").into_bytes());
|
||||
}
|
||||
|
||||
let mime = mime_guess::from_path(&asset).first_or_octet_stream();
|
||||
|
||||
// do not let path searching to go two layers beyond the caller level
|
||||
let data = std::fs::read(asset)?;
|
||||
let meta = format!("{}", mime);
|
||||
|
||||
ResponseBuilder::new().mimetype(&meta).body(data)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn get_asset_root() -> Option<PathBuf> {
|
||||
/*
|
||||
We're matching exactly how cargo-bundle works.
|
||||
|
||||
- [x] macOS
|
||||
- [ ] Windows
|
||||
- [ ] Linux (rpm)
|
||||
- [ ] Linux (deb)
|
||||
- [ ] iOS
|
||||
- [ ] Android
|
||||
|
||||
*/
|
||||
|
||||
if std::env::var_os("CARGO").is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// TODO: support for other platforms
|
||||
#[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()?);
|
||||
|
||||
return Some(canonical_resources_root);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
Dioxus Desktop Renderer
|
||||
|
||||
Render the Dioxus VirtualDom using the platform's native WebView implementation.
|
||||
|
||||
# Desktop
|
||||
|
||||
One of Dioxus' flagship features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
|
||||
|
||||
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao) directly. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
|
||||
|
||||
|
||||
## Getting Set up
|
||||
|
||||
Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project:
|
||||
|
||||
```shell
|
||||
$ cargo new --bin demo
|
||||
$ cd app
|
||||
```
|
||||
|
||||
Add Dioxus with the `desktop` feature:
|
||||
|
||||
```shell
|
||||
$ cargo add dioxus --features desktop
|
||||
```
|
||||
|
||||
Edit your `main.rs`:
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
"hello world!"
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/).
|
||||
|
||||
## Future Steps
|
||||
|
||||
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't!
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "fermi"
|
||||
version = "0.2.0"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Global state management for Dioxus"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.2.0" }
|
||||
im-rc = { version = "15.0.0", features = ["serde"] }
|
||||
log = "0.4.14"
|
||||
|
||||
[dev-dependencies]
|
||||
closure = "0.3.0"
|
||||
@@ -0,0 +1,92 @@
|
||||
|
||||
<div align="center">
|
||||
<h1>Fermi ⚛</h1>
|
||||
<p>
|
||||
<strong>Atom-based global state management solution for Dioxus</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div align="center">
|
||||
<!-- Crates version -->
|
||||
<a href="https://crates.io/crates/dioxus">
|
||||
<img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
|
||||
alt="Crates.io version" />
|
||||
</a>
|
||||
<!-- Downloads -->
|
||||
<a href="https://crates.io/crates/dioxus">
|
||||
<img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
|
||||
alt="Download" />
|
||||
</a>
|
||||
<!-- docs -->
|
||||
<a href="https://docs.rs/dioxus">
|
||||
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
|
||||
alt="docs.rs docs" />
|
||||
</a>
|
||||
<!-- CI -->
|
||||
<a href="https://github.com/jkelleyrtp/dioxus/actions">
|
||||
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
|
||||
alt="CI status" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
-----
|
||||
|
||||
Fermi is a global state management solution for Dioxus that's as easy as `use_state`.
|
||||
|
||||
Inspired by atom-based state management solutions, all state in Fermi starts as an `atom`:
|
||||
|
||||
```rust
|
||||
static NAME: Atom<&str> = |_| "Dioxus";
|
||||
```
|
||||
|
||||
From anywhere in our app, we can read our the value of our atom:
|
||||
|
||||
```rust
|
||||
fn NameCard(cx: Scope) -> Element {
|
||||
let name = use_read(&cx, NAME);
|
||||
cx.render(rsx!{ h1 { "Hello, {name}"} })
|
||||
}
|
||||
```
|
||||
|
||||
We can also set the value of our atom, also from anywhere in our app:
|
||||
|
||||
```rust
|
||||
fn NameCard(cx: Scope) -> Element {
|
||||
let set_name = use_set(&cx, NAME);
|
||||
cx.render(rsx!{
|
||||
button {
|
||||
onclick: move |_| set_name("Fermi"),
|
||||
"Set name to fermi"
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
It's that simple!
|
||||
|
||||
## Installation
|
||||
Fermi is currently under construction, so you have to use the `master` branch to get started.
|
||||
|
||||
```rust
|
||||
[depdencies]
|
||||
fermi = { git = "https://github.com/dioxuslabs/fermi" }
|
||||
```
|
||||
|
||||
|
||||
## Running examples
|
||||
|
||||
The examples here use Dioxus Desktop to showcase their functionality. To run an example, use
|
||||
```
|
||||
$ cargo run --example EXAMPLE
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
Broadly our feature set to required to be released includes:
|
||||
- [x] Support for Atoms
|
||||
- [x] Support for AtomRef (for values that aren't clone)
|
||||
- [ ] Support for Atom Families
|
||||
- [ ] Support for memoized Selectors
|
||||
- [ ] Support for memoized SelectorFamilies
|
||||
- [ ] Support for UseFermiCallback for access to fermi from async
|
||||
@@ -0,0 +1,28 @@
|
||||
use crate::{AtomId, AtomRoot, Readable, Writable};
|
||||
|
||||
pub type Atom<T> = fn(AtomBuilder) -> T;
|
||||
pub struct AtomBuilder;
|
||||
|
||||
impl<V> Readable<V> for Atom<V> {
|
||||
fn read(&self, _root: AtomRoot) -> Option<V> {
|
||||
todo!()
|
||||
}
|
||||
fn init(&self) -> V {
|
||||
(*self)(AtomBuilder)
|
||||
}
|
||||
fn unique_id(&self) -> AtomId {
|
||||
*self as *const ()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Writable<V> for Atom<V> {
|
||||
fn write(&self, _root: AtomRoot, _value: V) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn atom_compiles() {
|
||||
static TEST_ATOM: Atom<&str> = |_| "hello";
|
||||
dbg!(TEST_ATOM.init());
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::{AtomId, AtomRoot, Readable, Writable};
|
||||
use im_rc::HashMap as ImMap;
|
||||
|
||||
pub struct AtomFamilyBuilder;
|
||||
pub type AtomFamily<K, V> = fn(AtomFamilyBuilder) -> ImMap<K, V>;
|
||||
|
||||
impl<K, V> Readable<ImMap<K, V>> for AtomFamily<K, V> {
|
||||
fn read(&self, _root: AtomRoot) -> Option<ImMap<K, V>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn init(&self) -> ImMap<K, V> {
|
||||
(*self)(AtomFamilyBuilder)
|
||||
}
|
||||
|
||||
fn unique_id(&self) -> AtomId {
|
||||
*self as *const ()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Writable<ImMap<K, V>> for AtomFamily<K, V> {
|
||||
fn write(&self, _root: AtomRoot, _value: ImMap<K, V>) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::{AtomId, AtomRoot, Readable};
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub struct AtomRefBuilder;
|
||||
pub type AtomRef<T> = fn(AtomRefBuilder) -> T;
|
||||
|
||||
impl<V> Readable<RefCell<V>> for AtomRef<V> {
|
||||
fn read(&self, _root: AtomRoot) -> Option<RefCell<V>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn init(&self) -> RefCell<V> {
|
||||
RefCell::new((*self)(AtomRefBuilder))
|
||||
}
|
||||
|
||||
fn unique_id(&self) -> AtomId {
|
||||
*self as *const ()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn atom_compiles() {
|
||||
static TEST_ATOM: AtomRef<Vec<String>> = |_| vec![];
|
||||
dbg!(TEST_ATOM.init());
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#![allow(clippy::all, unused)]
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use dioxus_core::prelude::*;
|
||||
|
||||
use crate::{AtomRoot, Readable, Writable};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CallbackApi {
|
||||
root: Rc<AtomRoot>,
|
||||
}
|
||||
|
||||
impl CallbackApi {
|
||||
// get the current value of the atom
|
||||
pub fn get<V>(&self, atom: impl Readable<V>) -> &V {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// get the current value of the atom in its RC container
|
||||
pub fn get_rc<V>(&self, atom: impl Readable<V>) -> &Rc<V> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// set the current value of the atom
|
||||
pub fn set<V>(&self, atom: impl Writable<V>, value: V) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi {
|
||||
todo!()
|
||||
}
|
||||
|
||||
macro_rules! use_callback {
|
||||
(&$cx:ident, [$($cap:ident),*], move || $body:expr) => {
|
||||
move || {
|
||||
$(
|
||||
#[allow(unused_mut)]
|
||||
let mut $cap = $cap.to_owned();
|
||||
)*
|
||||
$cx.spawn($body);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! to_owned {
|
||||
($($es:ident),+) => {$(
|
||||
#[allow(unused_mut)]
|
||||
let mut $es = $es.to_owned();
|
||||
)*}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use crate::{use_atom_root, AtomId, AtomRef, AtomRoot, Readable};
|
||||
use dioxus_core::{ScopeId, ScopeState};
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn use_atom_ref<T: 'static>(cx: &ScopeState, atom: AtomRef<T>) -> &UseAtomRef<T> {
|
||||
let root = use_atom_root(cx);
|
||||
|
||||
cx.use_hook(|_| {
|
||||
root.initialize(atom);
|
||||
UseAtomRef {
|
||||
ptr: atom.unique_id(),
|
||||
root: root.clone(),
|
||||
scope_id: cx.scope_id(),
|
||||
value: root.register(atom, cx.scope_id()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub struct UseAtomRef<T> {
|
||||
ptr: AtomId,
|
||||
value: Rc<RefCell<T>>,
|
||||
root: Rc<AtomRoot>,
|
||||
scope_id: ScopeId,
|
||||
}
|
||||
|
||||
impl<T: 'static> UseAtomRef<T> {
|
||||
pub fn read(&self) -> Ref<T> {
|
||||
self.value.borrow()
|
||||
}
|
||||
|
||||
pub fn write(&self) -> RefMut<T> {
|
||||
self.root.force_update(self.ptr);
|
||||
self.value.borrow_mut()
|
||||
}
|
||||
|
||||
pub fn write_silent(&self) -> RefMut<T> {
|
||||
self.root.force_update(self.ptr);
|
||||
self.value.borrow_mut()
|
||||
}
|
||||
|
||||
pub fn set(&self, new: T) {
|
||||
self.root.force_update(self.ptr);
|
||||
self.root.set(self.ptr, new);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for UseAtomRef<T> {
|
||||
fn drop(&mut self) {
|
||||
self.root.unsubscribe(self.ptr, self.scope_id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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()))),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::AtomRoot;
|
||||
use dioxus_core::ScopeState;
|
||||
use std::rc::Rc;
|
||||
|
||||
// Initializes the atom root and retuns it;
|
||||
pub fn use_init_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
|
||||
cx.use_hook(|_| match cx.consume_context::<Rc<AtomRoot>>() {
|
||||
Some(ctx) => ctx,
|
||||
None => cx.provide_context(Rc::new(AtomRoot::new(cx.schedule_update_any()))),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use crate::{use_atom_root, AtomId, AtomRoot, Readable};
|
||||
use dioxus_core::{ScopeId, ScopeState};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub fn use_read<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable<V>) -> &'a V {
|
||||
use_read_rc(cx, f).as_ref()
|
||||
}
|
||||
|
||||
pub fn use_read_rc<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable<V>) -> &'a Rc<V> {
|
||||
let root = use_atom_root(cx);
|
||||
|
||||
struct UseReadInner<V> {
|
||||
root: Rc<AtomRoot>,
|
||||
id: AtomId,
|
||||
scope_id: ScopeId,
|
||||
value: Option<Rc<V>>,
|
||||
}
|
||||
|
||||
impl<V> Drop for UseReadInner<V> {
|
||||
fn drop(&mut self) {
|
||||
self.root.unsubscribe(self.id, self.scope_id)
|
||||
}
|
||||
}
|
||||
|
||||
let inner = cx.use_hook(|_| UseReadInner {
|
||||
value: None,
|
||||
root: root.clone(),
|
||||
scope_id: cx.scope_id(),
|
||||
id: f.unique_id(),
|
||||
});
|
||||
|
||||
let value = inner.root.register(f, cx.scope_id());
|
||||
|
||||
inner.value = Some(value);
|
||||
inner.value.as_ref().unwrap()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use crate::{use_atom_root, Writable};
|
||||
use dioxus_core::ScopeState;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub fn use_set<'a, T: 'static>(cx: &'a ScopeState, f: impl Writable<T>) -> &'a Rc<dyn Fn(T)> {
|
||||
let root = use_atom_root(cx);
|
||||
cx.use_hook(|_| {
|
||||
let id = f.unique_id();
|
||||
let root = root.clone();
|
||||
root.initialize(f);
|
||||
Rc::new(move |new| root.set(id, new)) as Rc<dyn Fn(T)>
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
use crate::{use_atom_root, AtomId, AtomRoot, Readable, Writable};
|
||||
use dioxus_core::{ScopeId, ScopeState};
|
||||
use std::{
|
||||
cell::{RefCell, RefMut},
|
||||
fmt::{Debug, Display},
|
||||
marker::PhantomData,
|
||||
ops::{Add, Div, Mul, Not, Sub},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// Store state between component renders.
|
||||
///
|
||||
/// ## Dioxus equivalent of AtomState, designed for Rust
|
||||
///
|
||||
/// The Dioxus version of `AtomState` for state management inside components. It allows you to ergonomically store and
|
||||
/// modify state between component renders. When the state is updated, the component will re-render.
|
||||
///
|
||||
///
|
||||
/// ```ignore
|
||||
/// static COUNT: Atom<u32> = |_| 0;
|
||||
///
|
||||
/// fn Example(cx: Scope) -> Element {
|
||||
/// let mut count = use_atom_state(&cx, COUNT);
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// div {
|
||||
/// h1 { "Count: {count}" }
|
||||
/// button { onclick: move |_| count += 1, "Increment" }
|
||||
/// button { onclick: move |_| count -= 1, "Decrement" }
|
||||
/// }
|
||||
/// ))
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_atom_state<'a, T: 'static>(cx: &'a ScopeState, f: impl Writable<T>) -> &'a AtomState<T> {
|
||||
let root = crate::use_atom_root(cx);
|
||||
|
||||
let inner = cx.use_hook(|_| AtomState {
|
||||
value: None,
|
||||
root: root.clone(),
|
||||
scope_id: cx.scope_id(),
|
||||
id: f.unique_id(),
|
||||
});
|
||||
|
||||
inner.value = Some(inner.root.register(f, cx.scope_id()));
|
||||
|
||||
inner
|
||||
}
|
||||
|
||||
pub struct AtomState<V: 'static> {
|
||||
root: Rc<AtomRoot>,
|
||||
id: AtomId,
|
||||
scope_id: ScopeId,
|
||||
value: Option<Rc<V>>,
|
||||
}
|
||||
|
||||
impl<V> Drop for AtomState<V> {
|
||||
fn drop(&mut self) {
|
||||
self.root.unsubscribe(self.id, self.scope_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> AtomState<T> {
|
||||
/// Set the state to a new value.
|
||||
pub fn set(&self, new: T) {
|
||||
self.root.set(self.id, new)
|
||||
}
|
||||
|
||||
/// Get the current value of the state by cloning its container Rc.
|
||||
///
|
||||
/// This is useful when you are dealing with state in async contexts but need
|
||||
/// to know the current value. You are not given a reference to the state.
|
||||
///
|
||||
/// # Examples
|
||||
/// An async context might need to know the current value:
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let count = use_state(&cx, || 0);
|
||||
/// cx.spawn({
|
||||
/// let set_count = count.to_owned();
|
||||
/// async move {
|
||||
/// let current = set_count.current();
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn current(&self) -> Rc<T> {
|
||||
self.value.as_ref().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Get the `setter` function directly without the `AtomState` wrapper.
|
||||
///
|
||||
/// This is useful for passing the setter function to other components.
|
||||
///
|
||||
/// However, for most cases, calling `to_owned` o`AtomState`te is the
|
||||
/// preferred way to get "anoth`set_state`tate handle.
|
||||
///
|
||||
///
|
||||
/// # Examples
|
||||
/// A component might require an `Rc<dyn Fn(T)>` as an input to set a value.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let value = use_state(&cx, || 0);
|
||||
///
|
||||
/// rsx!{
|
||||
/// Component {
|
||||
/// handler: value.setter()
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn setter(&self) -> Rc<dyn Fn(T)> {
|
||||
let root = self.root.clone();
|
||||
let id = self.id;
|
||||
Rc::new(move |new_val| root.set(id, new_val))
|
||||
}
|
||||
|
||||
/// Set the state to a new value, using the current state value as a reference.
|
||||
///
|
||||
/// This is similar to passing a closure to React's `set_value` function.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic usage:
|
||||
/// ```rust
|
||||
/// # use dioxus_core::prelude::*;
|
||||
/// # use dioxus_hooks::*;
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let value = use_state(&cx, || 0);
|
||||
///
|
||||
/// // to increment the value
|
||||
/// value.modify(|v| v + 1);
|
||||
///
|
||||
/// // usage in async
|
||||
/// cx.spawn({
|
||||
/// let value = value.to_owned();
|
||||
/// async move {
|
||||
/// value.modify(|v| v + 1);
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// # todo!()
|
||||
/// }
|
||||
/// ```
|
||||
pub fn modify(&self, f: impl FnOnce(&T) -> T) {
|
||||
self.root.clone().set(self.id, {
|
||||
let current = self.value.as_ref().unwrap();
|
||||
f(current.as_ref())
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the value of the state when this handle was created.
|
||||
///
|
||||
/// This method is useful when you want an `Rc` around the data to cheaply
|
||||
/// pass it around your app.
|
||||
///
|
||||
/// ## Warning
|
||||
///
|
||||
/// This will return a stale value if used within async contexts.
|
||||
///
|
||||
/// Try `current` to get the real current value of the state.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// # use dioxus_core::prelude::*;
|
||||
/// # use dioxus_hooks::*;
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let value = use_state(&cx, || 0);
|
||||
///
|
||||
/// let as_rc = value.get();
|
||||
/// assert_eq!(as_rc.as_ref(), &0);
|
||||
///
|
||||
/// # todo!()
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn get(&self) -> &T {
|
||||
self.value.as_ref().unwrap()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_rc(&self) -> &Rc<T> {
|
||||
self.value.as_ref().unwrap()
|
||||
}
|
||||
|
||||
/// Mark all consumers of this atom to re-render
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let count = use_state(&cx, || 0);
|
||||
/// cx.spawn({
|
||||
/// let count = count.to_owned();
|
||||
/// async move {
|
||||
/// // for the component to re-render
|
||||
/// count.needs_update();
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn needs_update(&self) {
|
||||
self.root.force_update(self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> AtomState<T> {
|
||||
/// Get a mutable handle to the value by calling `ToOwned::to_owned` on the
|
||||
/// current value.
|
||||
///
|
||||
/// This is essentially cloning the underlying value and then setting it,
|
||||
/// giving you a mutable handle in the process. This method is intended for
|
||||
/// types that are cheaply cloneable.
|
||||
///
|
||||
/// If you are comfortable dealing with `RefMut`, then you can use `make_mut` to get
|
||||
/// the underlying slot. However, be careful with `RefMut` since you might panic
|
||||
/// if the `RefCell` is left open.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let val = use_state(&cx, || 0);
|
||||
///
|
||||
/// val.with_mut(|v| *v = 1);
|
||||
/// ```
|
||||
pub fn with_mut(&self, apply: impl FnOnce(&mut T)) {
|
||||
let mut new_val = self.value.as_ref().unwrap().as_ref().to_owned();
|
||||
apply(&mut new_val);
|
||||
self.set(new_val);
|
||||
}
|
||||
|
||||
/// Get a mutable handle to the value by calling `ToOwned::to_owned` on the
|
||||
/// current value.
|
||||
///
|
||||
/// This is essentially cloning the underlying value and then setting it,
|
||||
/// giving you a mutable handle in the process. This method is intended for
|
||||
/// types that are cheaply cloneable.
|
||||
///
|
||||
/// # Warning
|
||||
/// Be careful with `RefMut` since you might panic if the `RefCell` is left open!
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let val = use_state(&cx, || 0);
|
||||
///
|
||||
/// *val.make_mut() += 1;
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn make_mut(&self) -> RefMut<T> {
|
||||
todo!("make mut not support for atom values yet")
|
||||
// let mut slot = self.value.as_ref().unwrap();
|
||||
|
||||
// self.needs_update();
|
||||
|
||||
// if Rc::strong_count(&*slot) > 0 {
|
||||
// *slot = Rc::new(slot.as_ref().to_owned());
|
||||
// }
|
||||
|
||||
// RefMut::map(slot, |rc| Rc::get_mut(rc).expect("the hard count to be 0"))
|
||||
}
|
||||
|
||||
/// Convert this handle to a tuple of the value and the handle itself.
|
||||
#[must_use]
|
||||
pub fn split(&self) -> (&T, &Self) {
|
||||
(self.value.as_ref().unwrap(), self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Clone for AtomState<T> {
|
||||
fn clone(&self) -> Self {
|
||||
AtomState {
|
||||
root: self.root.clone(),
|
||||
id: self.id,
|
||||
scope_id: self.scope_id,
|
||||
value: self.value.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'static + Display> std::fmt::Display for AtomState<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.value.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: std::fmt::Binary> std::fmt::Binary for AtomState<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:b}", self.value.as_ref().unwrap().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq<T> for AtomState<T> {
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
self.value.as_ref().unwrap().as_ref() == other
|
||||
}
|
||||
}
|
||||
|
||||
// todo: this but for more interesting conrete types
|
||||
impl PartialEq<bool> for &AtomState<bool> {
|
||||
fn eq(&self, other: &bool) -> bool {
|
||||
self.value.as_ref().unwrap().as_ref() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq<AtomState<T>> for AtomState<T> {
|
||||
fn eq(&self, other: &AtomState<T>) -> bool {
|
||||
Rc::ptr_eq(self.value.as_ref().unwrap(), other.value.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for AtomState<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self.value.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> std::ops::Deref for AtomState<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.value.as_ref().unwrap().as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Not + Copy> std::ops::Not for &AtomState<T> {
|
||||
type Output = <T as std::ops::Not>::Output;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
self.value.as_ref().unwrap().not()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Not + Copy> std::ops::Not for AtomState<T> {
|
||||
type Output = <T as std::ops::Not>::Output;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
self.value.as_ref().unwrap().not()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::ops::Add + Copy> std::ops::Add<T> for &AtomState<T> {
|
||||
type Output = <T as std::ops::Add>::Output;
|
||||
|
||||
fn add(self, other: T) -> Self::Output {
|
||||
*self.value.as_ref().unwrap().as_ref() + other
|
||||
}
|
||||
}
|
||||
impl<T: std::ops::Sub + Copy> std::ops::Sub<T> for &AtomState<T> {
|
||||
type Output = <T as std::ops::Sub>::Output;
|
||||
|
||||
fn sub(self, other: T) -> Self::Output {
|
||||
*self.value.as_ref().unwrap().as_ref() - other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::ops::Div + Copy> std::ops::Div<T> for &AtomState<T> {
|
||||
type Output = <T as std::ops::Div>::Output;
|
||||
|
||||
fn div(self, other: T) -> Self::Output {
|
||||
*self.value.as_ref().unwrap().as_ref() / other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::ops::Mul + Copy> std::ops::Mul<T> for &AtomState<T> {
|
||||
type Output = <T as std::ops::Mul>::Output;
|
||||
|
||||
fn mul(self, other: T) -> Self::Output {
|
||||
*self.value.as_ref().unwrap().as_ref() * other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Add<Output = T> + Copy> std::ops::AddAssign<T> for &AtomState<T> {
|
||||
fn add_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) + rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Sub<Output = T> + Copy> std::ops::SubAssign<T> for &AtomState<T> {
|
||||
fn sub_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) - rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Mul<Output = T> + Copy> std::ops::MulAssign<T> for &AtomState<T> {
|
||||
fn mul_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) * rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Div<Output = T> + Copy> std::ops::DivAssign<T> for &AtomState<T> {
|
||||
fn div_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) / rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Add<Output = T> + Copy> std::ops::AddAssign<T> for AtomState<T> {
|
||||
fn add_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) + rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Sub<Output = T> + Copy> std::ops::SubAssign<T> for AtomState<T> {
|
||||
fn sub_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) - rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Mul<Output = T> + Copy> std::ops::MulAssign<T> for AtomState<T> {
|
||||
fn mul_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) * rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Div<Output = T> + Copy> std::ops::DivAssign<T> for AtomState<T> {
|
||||
fn div_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) / rhs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::*;
|
||||
}
|
||||
|
||||
mod root;
|
||||
|
||||
pub use atoms::*;
|
||||
pub use hooks::*;
|
||||
pub use root::*;
|
||||
|
||||
mod atoms {
|
||||
mod atom;
|
||||
mod atomfamily;
|
||||
mod atomref;
|
||||
mod selector;
|
||||
mod selectorfamily;
|
||||
|
||||
pub use atom::*;
|
||||
pub use atomfamily::*;
|
||||
pub use atomref::*;
|
||||
pub use selector::*;
|
||||
pub use selectorfamily::*;
|
||||
}
|
||||
|
||||
pub mod hooks {
|
||||
mod atom_ref;
|
||||
mod atom_root;
|
||||
mod init_atom_root;
|
||||
mod read;
|
||||
mod set;
|
||||
mod state;
|
||||
pub use atom_ref::*;
|
||||
pub use atom_root::*;
|
||||
pub use init_atom_root::*;
|
||||
pub use read::*;
|
||||
pub use set::*;
|
||||
pub use state::*;
|
||||
}
|
||||
|
||||
/// All Atoms are `Readable` - they support reading their value.
|
||||
///
|
||||
/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors.
|
||||
/// It is not very useful for your own code, but could be used to build new Atom primitives.
|
||||
pub trait Readable<V> {
|
||||
fn read(&self, root: AtomRoot) -> Option<V>;
|
||||
fn init(&self) -> V;
|
||||
fn unique_id(&self) -> AtomId;
|
||||
}
|
||||
|
||||
/// All Atoms are `Writable` - they support writing their value.
|
||||
///
|
||||
/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors.
|
||||
/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors
|
||||
pub trait Writable<V>: Readable<V> {
|
||||
fn write(&self, root: AtomRoot, value: V);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
||||
|
||||
use dioxus_core::ScopeId;
|
||||
use im_rc::HashSet;
|
||||
|
||||
use crate::Readable;
|
||||
|
||||
pub type AtomId = *const ();
|
||||
|
||||
pub struct AtomRoot {
|
||||
pub atoms: RefCell<HashMap<AtomId, Slot>>,
|
||||
pub update_any: Arc<dyn Fn(ScopeId)>,
|
||||
}
|
||||
|
||||
pub struct Slot {
|
||||
pub value: Rc<dyn Any>,
|
||||
pub subscribers: HashSet<ScopeId>,
|
||||
}
|
||||
|
||||
impl AtomRoot {
|
||||
pub fn new(update_any: Arc<dyn Fn(ScopeId)>) -> Self {
|
||||
Self {
|
||||
update_any,
|
||||
atoms: RefCell::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize<V: 'static>(&self, f: impl Readable<V>) {
|
||||
let id = f.unique_id();
|
||||
if self.atoms.borrow().get(&id).is_none() {
|
||||
self.atoms.borrow_mut().insert(
|
||||
id,
|
||||
Slot {
|
||||
value: Rc::new(f.init()),
|
||||
subscribers: HashSet::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<V: 'static>(&self, f: impl Readable<V>, scope: ScopeId) -> Rc<V> {
|
||||
let mut atoms = self.atoms.borrow_mut();
|
||||
|
||||
// initialize the value if it's not already initialized
|
||||
if let Some(slot) = atoms.get_mut(&f.unique_id()) {
|
||||
slot.subscribers.insert(scope);
|
||||
slot.value.clone().downcast().unwrap()
|
||||
} else {
|
||||
let value = Rc::new(f.init());
|
||||
let mut subscribers = HashSet::new();
|
||||
subscribers.insert(scope);
|
||||
|
||||
atoms.insert(
|
||||
f.unique_id(),
|
||||
Slot {
|
||||
value: value.clone(),
|
||||
subscribers,
|
||||
},
|
||||
);
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set<V: 'static>(&self, ptr: AtomId, value: V) {
|
||||
let mut atoms = self.atoms.borrow_mut();
|
||||
|
||||
if let Some(slot) = atoms.get_mut(&ptr) {
|
||||
slot.value = Rc::new(value);
|
||||
log::trace!("found item with subscribers {:?}", slot.subscribers);
|
||||
|
||||
for scope in &slot.subscribers {
|
||||
log::trace!("updating subcsriber");
|
||||
(self.update_any)(*scope);
|
||||
}
|
||||
} else {
|
||||
log::trace!("no atoms found for {:?}", ptr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unsubscribe(&self, ptr: AtomId, scope: ScopeId) {
|
||||
let mut atoms = self.atoms.borrow_mut();
|
||||
|
||||
if let Some(slot) = atoms.get_mut(&ptr) {
|
||||
slot.subscribers.remove(&scope);
|
||||
}
|
||||
}
|
||||
|
||||
// force update of all subscribers
|
||||
pub fn force_update(&self, ptr: AtomId) {
|
||||
if let Some(slot) = self.atoms.borrow_mut().get(&ptr) {
|
||||
for scope in slot.subscribers.iter() {
|
||||
log::trace!("updating subcsriber");
|
||||
(self.update_any)(*scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read<V: 'static>(&self, f: impl Readable<V>) -> Rc<V> {
|
||||
let mut atoms = self.atoms.borrow_mut();
|
||||
|
||||
// initialize the value if it's not already initialized
|
||||
if let Some(slot) = atoms.get_mut(&f.unique_id()) {
|
||||
slot.value.clone().downcast().unwrap()
|
||||
} else {
|
||||
let value = Rc::new(f.init());
|
||||
atoms.insert(
|
||||
f.unique_id(),
|
||||
Slot {
|
||||
value: value.clone(),
|
||||
subscribers: HashSet::new(),
|
||||
},
|
||||
);
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-hooks"
|
||||
version = "0.1.7"
|
||||
version = "0.2.0"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Dioxus VirtualDOM renderer for a remote webview instance"
|
||||
@@ -12,4 +12,11 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../../packages/core", version = "^0.1.9" }
|
||||
dioxus-core = { path = "../../packages/core", version = "^0.2.0" }
|
||||
futures-channel = "0.3.21"
|
||||
log = { version = "0.4" }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
dioxus-core = { path = "../../packages/core", version = "^0.2.0" }
|
||||
@@ -1,3 +1,6 @@
|
||||
// #![deny(missing_docs)]
|
||||
//! Useful foundational hooks for Dioxus
|
||||
|
||||
mod usestate;
|
||||
pub use usestate::{use_state, UseState};
|
||||
|
||||
@@ -13,28 +16,5 @@ pub use usecoroutine::*;
|
||||
mod usefuture;
|
||||
pub use usefuture::*;
|
||||
|
||||
mod usesuspense;
|
||||
pub use usesuspense::*;
|
||||
|
||||
#[macro_export]
|
||||
/// A helper macro for using hooks in async environements.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
///
|
||||
/// ```
|
||||
/// 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 usesuspense;
|
||||
// pub use usesuspense::*;
|
||||
@@ -3,14 +3,15 @@ use std::{
|
||||
cell::{Cell, Ref, RefCell, RefMut},
|
||||
collections::HashSet,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
type ProvidedState<T> = RefCell<ProvidedStateInner<T>>;
|
||||
type ProvidedState<T> = Rc<RefCell<ProvidedStateInner<T>>>;
|
||||
|
||||
// Tracks all the subscribers to a shared State
|
||||
pub struct ProvidedStateInner<T> {
|
||||
value: Rc<RefCell<T>>,
|
||||
notify_any: Rc<dyn Fn(ScopeId)>,
|
||||
notify_any: Arc<dyn Fn(ScopeId)>,
|
||||
consumers: HashSet<ScopeId>,
|
||||
}
|
||||
|
||||
@@ -90,7 +91,7 @@ pub fn use_context<'a, T: 'static>(cx: &'a ScopeState) -> Option<UseSharedState<
|
||||
}
|
||||
|
||||
struct SharedStateInner<T: 'static> {
|
||||
root: Option<Rc<ProvidedState<T>>>,
|
||||
root: Option<ProvidedState<T>>,
|
||||
value: Option<Rc<RefCell<T>>>,
|
||||
scope_id: ScopeId,
|
||||
needs_notification: Cell<bool>,
|
||||
@@ -173,11 +174,11 @@ where
|
||||
///
|
||||
pub fn use_context_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) {
|
||||
cx.use_hook(|_| {
|
||||
let state: ProvidedState<T> = RefCell::new(ProvidedStateInner {
|
||||
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)
|
||||
});
|
||||
}
|
||||
+131
-101
@@ -1,122 +1,152 @@
|
||||
use dioxus_core::{ScopeState, TaskId};
|
||||
pub use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use std::future::Future;
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
/*
|
||||
|
||||
|
||||
|
||||
let g = use_coroutine(&cx, || {
|
||||
// clone the items in
|
||||
async move {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
*/
|
||||
pub fn use_coroutine<F>(cx: &ScopeState, create_future: impl FnOnce() -> F) -> CoroutineHandle<'_>
|
||||
/// Maintain a handle over a future that can be paused, resumed, and canceled.
|
||||
///
|
||||
/// This is an upgraded form of [`use_future`] with an integrated channel system.
|
||||
/// Specifically, the coroutine generated here comes with an [`UnboundedChannel`]
|
||||
/// built into it - saving you the hassle of building your own.
|
||||
///
|
||||
/// Addititionally, coroutines are automatically injected as shared contexts, so
|
||||
/// downstream components can tap into a coroutine's channel and send messages
|
||||
/// into a singular async event loop.
|
||||
///
|
||||
/// This makes it effective for apps that need to interact with an event loop or
|
||||
/// some asynchronous code without thinking too hard about state.
|
||||
///
|
||||
/// ## Global State
|
||||
///
|
||||
/// Typically, writing apps that handle concurrency properly can be difficult,
|
||||
/// so the intention of this hook is to make it easy to join and poll async tasks
|
||||
/// concurrently in a centralized place. You'll find that you can have much better
|
||||
/// control over your app's state if you centralize your async actions, even under
|
||||
/// the same concurrent context. This makes it easier to prevent undeseriable
|
||||
/// states in your UI while various async tasks are already running.
|
||||
///
|
||||
/// This hook is especially powerful when combined with Fermi. We can store important
|
||||
/// global data in a coroutine, and then access display-level values from the rest
|
||||
/// of our app through atoms.
|
||||
///
|
||||
/// ## UseCallback instead
|
||||
///
|
||||
/// However, you must plan out your own concurrency and synchronization. If you
|
||||
/// don't care about actions in your app being synchronized, you can use [`use_callback`]
|
||||
/// hook to spawn multiple tasks and run them concurrently.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust
|
||||
/// enum Action {
|
||||
/// Start,
|
||||
/// Stop,
|
||||
/// }
|
||||
///
|
||||
/// let chat_client = use_coroutine(&cx, |rx: UnboundedReceiver<Action>| async move {
|
||||
/// while let Some(action) = rx.next().await {
|
||||
/// match action {
|
||||
/// Action::Start => {}
|
||||
/// Action::Stop => {},
|
||||
/// }
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
///
|
||||
/// cx.render(rsx!{
|
||||
/// button {
|
||||
/// onclick: move |_| chat_client.send(Action::Start),
|
||||
/// "Start Chat Service"
|
||||
/// }
|
||||
/// })
|
||||
/// ```
|
||||
pub fn use_coroutine<M, G, F>(cx: &ScopeState, init: G) -> &CoroutineHandle<M>
|
||||
where
|
||||
M: 'static,
|
||||
G: FnOnce(UnboundedReceiver<M>) -> F,
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
let state = cx.use_hook(move |_| {
|
||||
let f = create_future();
|
||||
let id = cx.push_future(f);
|
||||
State {
|
||||
running: Default::default(),
|
||||
_id: id
|
||||
// pending_fut: Default::default(),
|
||||
// running_fut: Default::default(),
|
||||
}
|
||||
});
|
||||
|
||||
// state.pending_fut.set(Some(Box::pin(f)));
|
||||
|
||||
// if let Some(fut) = state.running_fut.as_mut() {
|
||||
// cx.push_future(fut);
|
||||
// }
|
||||
|
||||
// if let Some(fut) = state.running_fut.take() {
|
||||
// state.running.set(true);
|
||||
// fut.resume();
|
||||
// }
|
||||
|
||||
// let submit: Box<dyn FnOnce() + 'a> = Box::new(move || {
|
||||
// let g = async move {
|
||||
// running.set(true);
|
||||
// create_future().await;
|
||||
// running.set(false);
|
||||
// };
|
||||
// let p: Pin<Box<dyn Future<Output = ()>>> = Box::pin(g);
|
||||
// fut_slot
|
||||
// .borrow_mut()
|
||||
// .replace(unsafe { std::mem::transmute(p) });
|
||||
// });
|
||||
|
||||
// let submit = unsafe { std::mem::transmute(submit) };
|
||||
// state.submit.get_mut().replace(submit);
|
||||
|
||||
// if state.running.get() {
|
||||
// // let mut fut = state.fut.borrow_mut();
|
||||
// // cx.push_task(|| fut.as_mut().unwrap().as_mut());
|
||||
// } else {
|
||||
// // make sure to drop the old future
|
||||
// if let Some(fut) = state.fut.borrow_mut().take() {
|
||||
// drop(fut);
|
||||
// }
|
||||
// }
|
||||
CoroutineHandle { cx, inner: state }
|
||||
cx.use_hook(|_| {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
let task = cx.push_future(init(rx));
|
||||
cx.provide_context(CoroutineHandle { tx, task })
|
||||
})
|
||||
}
|
||||
|
||||
struct State {
|
||||
running: Rc<Cell<bool>>,
|
||||
_id: TaskId,
|
||||
// the way this is structure, you can toggle the coroutine without re-rendering the comppnent
|
||||
// this means every render *generates* the future, which is a bit of a waste
|
||||
// todo: allocate pending futures in the bump allocator and then have a true promotion
|
||||
// pending_fut: Cell<Option<Pin<Box<dyn Future<Output = ()> + 'static>>>>,
|
||||
// running_fut: Option<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||
// running_fut: Rc<RefCell<Option<Pin<Box<dyn Future<Output = ()> + 'static>>>>>
|
||||
/// Get a handle to a coroutine higher in the tree
|
||||
///
|
||||
/// See the docs for [`use_coroutine`] for more details.
|
||||
pub fn use_coroutine_handle<M: 'static>(cx: &ScopeState) -> Option<&CoroutineHandle<M>> {
|
||||
cx.use_hook(|_| cx.consume_context::<CoroutineHandle<M>>())
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub struct CoroutineHandle<'a> {
|
||||
cx: &'a ScopeState,
|
||||
inner: &'a State,
|
||||
pub struct CoroutineHandle<T> {
|
||||
tx: UnboundedSender<T>,
|
||||
task: TaskId,
|
||||
}
|
||||
|
||||
impl Clone for CoroutineHandle<'_> {
|
||||
impl<T> Clone for CoroutineHandle<T> {
|
||||
fn clone(&self) -> Self {
|
||||
CoroutineHandle {
|
||||
cx: self.cx,
|
||||
inner: self.inner,
|
||||
Self {
|
||||
tx: self.tx.clone(),
|
||||
task: self.task,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Copy for CoroutineHandle<'_> {}
|
||||
|
||||
impl<'a> CoroutineHandle<'a> {
|
||||
#[allow(clippy::needless_return)]
|
||||
pub fn start(&self) {
|
||||
if self.is_running() {
|
||||
return;
|
||||
}
|
||||
|
||||
// if let Some(submit) = self.inner.pending_fut.take() {
|
||||
// submit();
|
||||
// let inner = self.inner;
|
||||
// self.cx.push_task(submit());
|
||||
// }
|
||||
impl<T> CoroutineHandle<T> {
|
||||
/// Get the ID of this coroutine
|
||||
#[must_use]
|
||||
pub fn task_id(&self) -> TaskId {
|
||||
self.task
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.inner.running.get()
|
||||
/// Send a message to the coroutine
|
||||
pub fn send(&self, msg: T) {
|
||||
let _ = self.tx.unbounded_send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(unused)]
|
||||
|
||||
use super::*;
|
||||
use dioxus_core::exports::futures_channel::mpsc::unbounded;
|
||||
use dioxus_core::prelude::*;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
fn app(cx: Scope, name: String) -> Element {
|
||||
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 task3 = use_coroutine(&cx, |rx| complex_task(rx, 10));
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn view_task(mut rx: UnboundedReceiver<i32>) {
|
||||
while let Some(msg) = rx.next().await {
|
||||
println!("got message: {}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
enum Actions {
|
||||
CloseAll,
|
||||
OpenAll,
|
||||
}
|
||||
|
||||
async fn complex_task(mut rx: UnboundedReceiver<Actions>, name: i32) {
|
||||
while let Some(msg) = rx.next().await {
|
||||
match msg {
|
||||
Actions::CloseAll => todo!(),
|
||||
Actions::OpenAll => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resume(&self) {
|
||||
// self.cx.push_task(fut)
|
||||
}
|
||||
|
||||
pub fn stop(&self) {}
|
||||
|
||||
pub fn restart(&self) {}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use dioxus_core::{ScopeState, TaskId};
|
||||
use std::{any::Any, cell::Cell, future::Future};
|
||||
|
||||
use crate::UseFutureDep;
|
||||
|
||||
/// A hook that provides a future that executes after the hooks have been applied
|
||||
///
|
||||
/// Whenever the hooks dependencies change, the future will be re-evaluated.
|
||||
/// If a future is pending when the dependencies change, the previous future
|
||||
/// will be allowed to continue
|
||||
///
|
||||
/// - dependencies: a tuple of references to values that are PartialEq + Clone
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust, ignore
|
||||
///
|
||||
/// #[inline_props]
|
||||
/// fn app(cx: Scope, name: &str) -> Element {
|
||||
/// use_effect(&cx, (name,), |(name,)| async move {
|
||||
/// set_title(name);
|
||||
/// }))
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_effect<T, F, D>(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F)
|
||||
where
|
||||
T: 'static,
|
||||
F: Future<Output = T> + 'static,
|
||||
D: UseFutureDep,
|
||||
{
|
||||
struct UseEffect {
|
||||
needs_regen: bool,
|
||||
task: Cell<Option<TaskId>>,
|
||||
dependencies: Vec<Box<dyn Any>>,
|
||||
}
|
||||
|
||||
let state = cx.use_hook(move |_| UseEffect {
|
||||
needs_regen: true,
|
||||
task: Cell::new(None),
|
||||
dependencies: Vec::new(),
|
||||
});
|
||||
|
||||
if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen {
|
||||
// We don't need regen anymore
|
||||
state.needs_regen = false;
|
||||
|
||||
// Create the new future
|
||||
let fut = future(dependencies.out());
|
||||
|
||||
state.task.set(Some(cx.push_future(async move {
|
||||
fut.await;
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[allow(unused)]
|
||||
#[test]
|
||||
fn test_use_future() {
|
||||
use dioxus_core::prelude::*;
|
||||
|
||||
struct MyProps {
|
||||
a: String,
|
||||
b: i32,
|
||||
c: i32,
|
||||
d: i32,
|
||||
e: i32,
|
||||
}
|
||||
|
||||
fn app(cx: Scope<MyProps>) -> Element {
|
||||
// should only ever run once
|
||||
use_effect(&cx, (), |_| async move {
|
||||
//
|
||||
});
|
||||
|
||||
// runs when a is changed
|
||||
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 {
|
||||
//
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
+226
-23
@@ -1,61 +1,108 @@
|
||||
#![allow(missing_docs)]
|
||||
use dioxus_core::{ScopeState, TaskId};
|
||||
use std::{cell::Cell, future::Future, rc::Rc};
|
||||
use std::{any::Any, cell::Cell, future::Future, rc::Rc, sync::Arc};
|
||||
|
||||
pub fn use_future<'a, T: 'static, F: Future<Output = T> + 'static>(
|
||||
cx: &'a ScopeState,
|
||||
new_fut: impl FnOnce() -> F,
|
||||
) -> &'a UseFuture<T> {
|
||||
let state = cx.use_hook(move |_| {
|
||||
//
|
||||
UseFuture {
|
||||
update: cx.schedule_update(),
|
||||
needs_regen: Cell::new(true),
|
||||
slot: Rc::new(Cell::new(None)),
|
||||
value: None,
|
||||
task: None,
|
||||
}
|
||||
/// A future that resolves to a value.
|
||||
///
|
||||
/// This runs the future only once - though the future may be regenerated
|
||||
/// through the [`UseFuture::restart`] method.
|
||||
///
|
||||
/// This is commonly used for components that cannot be rendered until some
|
||||
/// asynchronous operation has completed.
|
||||
///
|
||||
/// Whenever the hooks dependencies change, the future will be re-evaluated.
|
||||
/// If a future is pending when the dependencies change, the previous future
|
||||
/// will be allowed to continue
|
||||
///
|
||||
/// - dependencies: a tuple of references to values that are PartialEq + Clone
|
||||
pub fn use_future<T, F, D>(
|
||||
cx: &ScopeState,
|
||||
dependencies: D,
|
||||
future: impl FnOnce(D::Out) -> F,
|
||||
) -> &UseFuture<T>
|
||||
where
|
||||
T: 'static,
|
||||
F: Future<Output = T> + 'static,
|
||||
D: UseFutureDep,
|
||||
{
|
||||
let state = cx.use_hook(move |_| UseFuture {
|
||||
update: cx.schedule_update(),
|
||||
needs_regen: Cell::new(true),
|
||||
slot: Rc::new(Cell::new(None)),
|
||||
value: None,
|
||||
task: Cell::new(None),
|
||||
dependencies: Vec::new(),
|
||||
});
|
||||
|
||||
if let Some(value) = state.slot.take() {
|
||||
state.value = Some(value);
|
||||
state.task = None;
|
||||
state.task.set(None);
|
||||
}
|
||||
|
||||
if state.needs_regen.get() {
|
||||
if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() {
|
||||
// We don't need regen anymore
|
||||
state.needs_regen.set(false);
|
||||
|
||||
// Create the new future
|
||||
let fut = new_fut();
|
||||
let fut = future(dependencies.out());
|
||||
|
||||
// Clone in our cells
|
||||
let slot = state.slot.clone();
|
||||
let updater = state.update.clone();
|
||||
let schedule_update = state.update.clone();
|
||||
|
||||
state.task = Some(cx.push_future(async move {
|
||||
// Cancel the current future
|
||||
if let Some(current) = state.task.take() {
|
||||
cx.remove_future(current);
|
||||
}
|
||||
|
||||
state.task.set(Some(cx.push_future(async move {
|
||||
let res = fut.await;
|
||||
slot.set(Some(res));
|
||||
updater();
|
||||
}));
|
||||
schedule_update();
|
||||
})));
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
pub enum FutureState<'a, T> {
|
||||
Pending,
|
||||
Complete(&'a T),
|
||||
Regenerating(&'a T), // the old value
|
||||
}
|
||||
|
||||
pub struct UseFuture<T> {
|
||||
update: Rc<dyn Fn()>,
|
||||
update: Arc<dyn Fn()>,
|
||||
needs_regen: Cell<bool>,
|
||||
value: Option<T>,
|
||||
slot: Rc<Cell<Option<T>>>,
|
||||
task: Option<TaskId>,
|
||||
task: Cell<Option<TaskId>>,
|
||||
dependencies: Vec<Box<dyn Any>>,
|
||||
}
|
||||
|
||||
pub enum UseFutureState<'a, T> {
|
||||
Pending,
|
||||
Complete(&'a T),
|
||||
Reloading(&'a T),
|
||||
}
|
||||
|
||||
impl<T> UseFuture<T> {
|
||||
/// Restart the future with new dependencies.
|
||||
///
|
||||
/// Will not cancel the previous future, but will ignore any values that it
|
||||
/// generates.
|
||||
pub fn restart(&self) {
|
||||
self.needs_regen.set(true);
|
||||
(self.update)();
|
||||
}
|
||||
|
||||
/// Forcefully cancel a future
|
||||
pub fn cancel(&self, cx: &ScopeState) {
|
||||
if let Some(task) = self.task.take() {
|
||||
cx.remove_future(task);
|
||||
}
|
||||
}
|
||||
|
||||
// clears the value in the future slot without starting the future over
|
||||
pub fn clear(&self) -> Option<T> {
|
||||
(self.update)();
|
||||
@@ -69,7 +116,163 @@ impl<T> UseFuture<T> {
|
||||
(self.update)();
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Get the ID of the future in Dioxus' internal scheduler
|
||||
pub fn task(&self) -> Option<TaskId> {
|
||||
self.task.get()
|
||||
}
|
||||
|
||||
/// 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),
|
||||
|
||||
// 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,
|
||||
|
||||
// Task, no value - we're still pending
|
||||
(Some(_), None) => UseFutureState::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UseFutureDep: Sized + Clone {
|
||||
type Out;
|
||||
fn out(&self) -> Self::Out;
|
||||
fn apply(self, state: &mut Vec<Box<dyn Any>>) -> bool;
|
||||
}
|
||||
|
||||
impl UseFutureDep for () {
|
||||
type Out = ();
|
||||
fn out(&self) -> Self::Out {}
|
||||
fn apply(self, _state: &mut Vec<Box<dyn Any>>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Dep: 'static + PartialEq + Clone {}
|
||||
impl<T> Dep for T where T: 'static + PartialEq + Clone {}
|
||||
|
||||
impl<A: Dep> UseFutureDep for &A {
|
||||
type Out = A;
|
||||
fn out(&self) -> Self::Out {
|
||||
(*self).clone()
|
||||
}
|
||||
fn apply(self, state: &mut Vec<Box<dyn Any>>) -> bool {
|
||||
match state.get_mut(0).and_then(|f| f.downcast_mut::<A>()) {
|
||||
Some(val) => {
|
||||
if *val != *self {
|
||||
*val = self.clone();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
state.push(Box::new(self.clone()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_dep {
|
||||
(
|
||||
$($el:ident=$name:ident,)*
|
||||
) => {
|
||||
impl< $($el),* > UseFutureDep for ($(&$el,)*)
|
||||
where
|
||||
$(
|
||||
$el: Dep
|
||||
),*
|
||||
{
|
||||
type Out = ($($el,)*);
|
||||
|
||||
fn out(&self) -> Self::Out {
|
||||
let ($($name,)*) = self;
|
||||
($((*$name).clone(),)*)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn apply(self, state: &mut Vec<Box<dyn Any>>) -> bool {
|
||||
let ($($name,)*) = self;
|
||||
let mut idx = 0;
|
||||
let mut needs_regen = false;
|
||||
|
||||
$(
|
||||
match state.get_mut(idx).map(|f| f.downcast_mut::<$el>()).flatten() {
|
||||
Some(val) => {
|
||||
if *val != *$name {
|
||||
*val = $name.clone();
|
||||
needs_regen = true;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
state.push(Box::new($name.clone()));
|
||||
needs_regen = true;
|
||||
}
|
||||
}
|
||||
idx += 1;
|
||||
)*
|
||||
|
||||
needs_regen
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_dep!(A = a,);
|
||||
impl_dep!(A = a, B = b,);
|
||||
impl_dep!(A = a, B = b, C = c,);
|
||||
impl_dep!(A = a, B = b, C = c, D = d,);
|
||||
impl_dep!(A = a, B = b, C = c, D = d, E = e,);
|
||||
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,);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[allow(unused)]
|
||||
#[test]
|
||||
fn test_use_future() {
|
||||
use dioxus_core::prelude::*;
|
||||
|
||||
struct MyProps {
|
||||
a: String,
|
||||
b: i32,
|
||||
c: i32,
|
||||
d: i32,
|
||||
e: i32,
|
||||
}
|
||||
|
||||
fn app(cx: Scope<MyProps>) -> Element {
|
||||
// should only ever run once
|
||||
let fut = use_future(&cx, (), |_| async move {
|
||||
//
|
||||
});
|
||||
|
||||
// runs when a is changed
|
||||
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 {
|
||||
//
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
+236
-39
@@ -1,57 +1,254 @@
|
||||
use dioxus_core::ScopeState;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
cell::{Cell, Ref, RefCell, RefMut},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use dioxus_core::ScopeState;
|
||||
/// `use_ref` is a key foundational hook for storing state in Dioxus.
|
||||
///
|
||||
/// It is different that `use_state` in that the value stored is not "immutable".
|
||||
/// Instead, UseRef is designed to store larger values that will be mutated at will.
|
||||
///
|
||||
/// ## Writing Values
|
||||
///
|
||||
/// Generally, `use_ref` is just a wrapper around a RefCell that tracks mutable
|
||||
/// writes through the `write` method. Whenever `write` is called, the component
|
||||
/// that initialized the hook will be marked as "dirty".
|
||||
///
|
||||
/// ```rust
|
||||
/// let val = use_ref(|| HashMap::<u32, String>::new());
|
||||
///
|
||||
/// // using `write` will give us a `RefMut` to the inner value, which we can call methods on
|
||||
/// // This marks the component as "dirty"
|
||||
/// val.write().insert(1, "hello".to_string());
|
||||
/// ```
|
||||
///
|
||||
/// You can avoid this defualt behavior with `write_silent`
|
||||
///
|
||||
/// ```
|
||||
/// // with `write_silent`, the component will not be re-rendered
|
||||
/// val.write_silent().insert(2, "goodbye".to_string());
|
||||
/// ```
|
||||
///
|
||||
/// ## Reading Values
|
||||
///
|
||||
/// To read values out of the refcell, you can use the `read` method which will retrun a `Ref`.
|
||||
///
|
||||
/// ```rust
|
||||
/// let map: Ref<_> = val.read();
|
||||
///
|
||||
/// let item = map.get(&1);
|
||||
/// ```
|
||||
///
|
||||
/// To get an &T out of the RefCell, you need to "reborrow" through the Ref:
|
||||
///
|
||||
/// ```rust
|
||||
/// let read = val.read();
|
||||
/// let map = &*read;
|
||||
/// ```
|
||||
///
|
||||
/// ## Collections and iteration
|
||||
///
|
||||
/// A common usecase for `use_ref` is to store a large amount of data in a component.
|
||||
/// Typically this will be a collection like a HashMap or a Vec. To create new
|
||||
/// elements from the collection, we can use `read()` directly in our rsx!.
|
||||
///
|
||||
/// ```rust
|
||||
/// rsx!{
|
||||
/// val.read().iter().map(|(k, v)| {
|
||||
/// rsx!{ key: "{k}", value: "{v}" }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If you are generating elements outside of `rsx!` then you might need to call
|
||||
/// "render" inside the iterator. For some cases you might need to collect into
|
||||
/// a temporary Vec.
|
||||
///
|
||||
/// ```rust
|
||||
/// let items = val.read().iter().map(|(k, v)| {
|
||||
/// cx.render(rsx!{ key: "{k}", value: "{v}" })
|
||||
/// });
|
||||
///
|
||||
/// // collect into a Vec
|
||||
///
|
||||
/// let items: Vec<Element> = items.collect();
|
||||
/// ```
|
||||
///
|
||||
/// ## Use in Async
|
||||
///
|
||||
/// To access values from a `UseRef` in an async context, you need to detach it
|
||||
/// from the current scope's lifetime, making it a `'static` value. This is done
|
||||
/// by simply calling `ToOnwed` or `Clone`.
|
||||
///
|
||||
/// ```rust
|
||||
/// let val = use_ref(|| HashMap::<u32, String>::new());
|
||||
///
|
||||
/// cx.spawn({
|
||||
/// let val = val.clone();
|
||||
/// async move {
|
||||
/// some_work().await;
|
||||
/// val.write().insert(1, "hello".to_string());
|
||||
/// }
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// If you're working with lots of values like UseState and UseRef, you can use the
|
||||
/// `clone!` macro to make it easier to write the above code.
|
||||
///
|
||||
/// ```rust
|
||||
/// let val1 = use_ref(|| HashMap::<u32, String>::new());
|
||||
/// let val2 = use_ref(|| HashMap::<u32, String>::new());
|
||||
/// let val3 = use_ref(|| HashMap::<u32, String>::new());
|
||||
///
|
||||
/// cx.spawn({
|
||||
/// clone![val1, val2, val3];
|
||||
/// async move {
|
||||
/// some_work().await;
|
||||
/// val.write().insert(1, "hello".to_string());
|
||||
/// }
|
||||
/// })
|
||||
/// ```
|
||||
pub fn use_ref<'a, T: 'static>(
|
||||
cx: &'a ScopeState,
|
||||
initialize_refcell: impl FnOnce() -> T,
|
||||
) -> &'a UseRef<T> {
|
||||
let hook = cx.use_hook(|_| UseRef {
|
||||
update: cx.schedule_update(),
|
||||
value: Rc::new(RefCell::new(initialize_refcell())),
|
||||
dirty: Rc::new(Cell::new(false)),
|
||||
gen: 0,
|
||||
});
|
||||
|
||||
pub fn use_ref<'a, T: 'static>(cx: &'a ScopeState, f: impl FnOnce() -> T) -> &'a UseRef<T> {
|
||||
cx.use_hook(|_| UseRef {
|
||||
update_callback: cx.schedule_update(),
|
||||
value: Rc::new(RefCell::new(f())),
|
||||
})
|
||||
if hook.dirty.get() {
|
||||
hook.gen += 1;
|
||||
hook.dirty.set(false);
|
||||
}
|
||||
|
||||
hook
|
||||
}
|
||||
|
||||
/// A type created by the [`use_ref`] hook. See its documentation for more details.
|
||||
pub struct UseRef<T> {
|
||||
update_callback: Rc<dyn Fn()>,
|
||||
update: Arc<dyn Fn()>,
|
||||
value: Rc<RefCell<T>>,
|
||||
}
|
||||
|
||||
impl<T> UseRef<T> {
|
||||
pub fn read(&self) -> Ref<'_, T> {
|
||||
self.value.borrow()
|
||||
}
|
||||
|
||||
pub fn set(&self, new: T) {
|
||||
*self.value.borrow_mut() = new;
|
||||
self.needs_update();
|
||||
}
|
||||
|
||||
pub fn read_write(&self) -> (Ref<'_, T>, &Self) {
|
||||
(self.read(), self)
|
||||
}
|
||||
|
||||
/// Calling "write" will force the component to re-render
|
||||
pub fn write(&self) -> RefMut<'_, T> {
|
||||
self.needs_update();
|
||||
self.value.borrow_mut()
|
||||
}
|
||||
|
||||
/// Allows the ability to write the value without forcing a re-render
|
||||
pub fn write_silent(&self) -> RefMut<'_, T> {
|
||||
self.value.borrow_mut()
|
||||
}
|
||||
|
||||
pub fn needs_update(&self) {
|
||||
(self.update_callback)();
|
||||
}
|
||||
dirty: Rc<Cell<bool>>,
|
||||
gen: usize,
|
||||
}
|
||||
|
||||
impl<T> Clone for UseRef<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
update_callback: self.update_callback.clone(),
|
||||
update: self.update.clone(),
|
||||
value: self.value.clone(),
|
||||
dirty: self.dirty.clone(),
|
||||
gen: self.gen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UseRef<T> {
|
||||
/// Read the value in the RefCell into a `Ref`. If this method is called
|
||||
/// while other values are still being `read` or `write`, then your app will crash.
|
||||
///
|
||||
/// Be very careful when working with this method. If you can, consider using
|
||||
/// the `with` and `with_mut` methods instead, choosing to render Elements
|
||||
/// during the read calls.
|
||||
pub fn read(&self) -> Ref<'_, T> {
|
||||
self.value.borrow()
|
||||
}
|
||||
|
||||
/// Mutably unlock the value in the RefCell. This will mark the component as "dirty"
|
||||
///
|
||||
/// Uses to `write` should be as short as possible.
|
||||
///
|
||||
/// Be very careful when working with this method. If you can, consider using
|
||||
/// the `with` and `with_mut` methods instead, choosing to render Elements
|
||||
/// during the read and write calls.
|
||||
pub fn write(&self) -> RefMut<'_, T> {
|
||||
self.needs_update();
|
||||
self.value.borrow_mut()
|
||||
}
|
||||
|
||||
/// Set the curernt value to `new_value`. This will mark the component as "dirty"
|
||||
///
|
||||
/// This change will propogate immediately, so any other contexts that are
|
||||
/// using this RefCell will also be affected. If called during an async context,
|
||||
/// the component will not be re-rendered until the next `.await` call.
|
||||
pub fn set(&self, new: T) {
|
||||
*self.value.borrow_mut() = new;
|
||||
self.needs_update();
|
||||
}
|
||||
|
||||
/// Mutably unlock the value in the RefCell. This will not mark the component as dirty.
|
||||
/// This is useful if you want to do some work without causing the component to re-render.
|
||||
///
|
||||
/// Uses to `write` should be as short as possible.
|
||||
///
|
||||
/// Be very careful when working with this method. If you can, consider using
|
||||
/// the `with` and `with_mut` methods instead, choosing to render Elements
|
||||
pub fn write_silent(&self) -> RefMut<'_, T> {
|
||||
self.value.borrow_mut()
|
||||
}
|
||||
|
||||
/// Take a reference to the inner value termporarily and produce a new value
|
||||
///
|
||||
/// Note: You can always "reborrow" the value through the RefCell.
|
||||
/// This method just does it for you automatically.
|
||||
///
|
||||
/// ```rust
|
||||
/// let val = use_ref(|| HashMap::<u32, String>::new());
|
||||
///
|
||||
///
|
||||
/// // use reborrowing
|
||||
/// let inner = &*val.read();
|
||||
///
|
||||
/// // or, be safer and use `with`
|
||||
/// val.with(|i| println!("{:?}", i));
|
||||
/// ```
|
||||
pub fn with<O>(&self, immutable_callback: impl FnOnce(&T) -> O) -> O {
|
||||
immutable_callback(&*self.read())
|
||||
}
|
||||
|
||||
/// Take a reference to the inner value termporarily and produce a new value,
|
||||
/// modifying the original in place.
|
||||
///
|
||||
/// Note: You can always "reborrow" the value through the RefCell.
|
||||
/// This method just does it for you automatically.
|
||||
///
|
||||
/// ```rust
|
||||
/// let val = use_ref(|| HashMap::<u32, String>::new());
|
||||
///
|
||||
///
|
||||
/// // use reborrowing
|
||||
/// let inner = &mut *val.write();
|
||||
///
|
||||
/// // or, be safer and use `with`
|
||||
/// val.with_mut(|i| i.insert(1, "hi"));
|
||||
/// ```
|
||||
pub fn with_mut<O>(&self, mutable_callback: impl FnOnce(&mut T) -> O) -> O {
|
||||
mutable_callback(&mut *self.write())
|
||||
}
|
||||
|
||||
/// Call the inner callback to mark the originator component as dirty.
|
||||
///
|
||||
/// This will cause the component to be re-rendered after the current scope
|
||||
/// has ended or the current async task has been yielded through await.
|
||||
pub fn needs_update(&self) {
|
||||
self.dirty.set(true);
|
||||
(self.update)();
|
||||
}
|
||||
}
|
||||
|
||||
// UseRef memoizes not on value but on cell
|
||||
// Memoizes on "generation" - so it will cause a re-render if the value changes
|
||||
impl<T> PartialEq for UseRef<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if Rc::ptr_eq(&self.value, &other.value) {
|
||||
self.gen == other.gen
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
+170
-39
@@ -4,7 +4,9 @@ use dioxus_core::prelude::*;
|
||||
use std::{
|
||||
cell::{RefCell, RefMut},
|
||||
fmt::{Debug, Display},
|
||||
ops::{Add, Div, Mul, Not, Sub},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// Store state between component renders.
|
||||
@@ -17,13 +19,13 @@ use std::{
|
||||
///
|
||||
/// ```ignore
|
||||
/// const Example: Component = |cx| {
|
||||
/// let (count, set_count) = use_state(&cx, || 0);
|
||||
/// let count = use_state(&cx, || 0);
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// div {
|
||||
/// h1 { "Count: {count}" }
|
||||
/// button { onclick: move |_| set_count(a - 1), "Increment" }
|
||||
/// button { onclick: move |_| set_count(a + 1), "Decrement" }
|
||||
/// button { onclick: move |_| *count.modify() += 1, "Increment" }
|
||||
/// button { onclick: move |_| *count.modify() -= 1, "Decrement" }
|
||||
/// }
|
||||
/// ))
|
||||
/// }
|
||||
@@ -31,13 +33,13 @@ use std::{
|
||||
pub fn use_state<'a, T: 'static>(
|
||||
cx: &'a ScopeState,
|
||||
initial_state_fn: impl FnOnce() -> T,
|
||||
) -> (&'a T, &'a UseState<T>) {
|
||||
) -> &'a UseState<T> {
|
||||
let hook = cx.use_hook(move |_| {
|
||||
let current_val = Rc::new(initial_state_fn());
|
||||
let update_callback = cx.schedule_update();
|
||||
let slot = Rc::new(RefCell::new(current_val.clone()));
|
||||
let setter = Rc::new({
|
||||
crate::to_owned![update_callback, slot];
|
||||
dioxus_core::to_owned![update_callback, slot];
|
||||
move |new| {
|
||||
{
|
||||
let mut slot = slot.borrow_mut();
|
||||
@@ -64,17 +66,22 @@ pub fn use_state<'a, T: 'static>(
|
||||
|
||||
hook.current_val = hook.slot.borrow().clone();
|
||||
|
||||
(hook.current_val.as_ref(), hook)
|
||||
hook
|
||||
}
|
||||
|
||||
pub struct UseState<T: 'static> {
|
||||
pub(crate) current_val: Rc<T>,
|
||||
pub(crate) update_callback: Rc<dyn Fn()>,
|
||||
pub(crate) update_callback: Arc<dyn Fn()>,
|
||||
pub(crate) setter: Rc<dyn Fn(T)>,
|
||||
pub(crate) slot: Rc<RefCell<Rc<T>>>,
|
||||
}
|
||||
|
||||
impl<T: 'static> UseState<T> {
|
||||
/// Set the state to a new value.
|
||||
pub fn set(&self, new: T) {
|
||||
(self.setter)(new);
|
||||
}
|
||||
|
||||
/// Get the current value of the state by cloning its container Rc.
|
||||
///
|
||||
/// This is useful when you are dealing with state in async contexts but need
|
||||
@@ -85,9 +92,9 @@ impl<T: 'static> UseState<T> {
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let (count, set_count) = use_state(&cx, || 0);
|
||||
/// let count = use_state(&cx, || 0);
|
||||
/// cx.spawn({
|
||||
/// let set_count = set_count.to_owned();
|
||||
/// let set_count = count.to_owned();
|
||||
/// async move {
|
||||
/// let current = set_count.current();
|
||||
/// }
|
||||
@@ -112,11 +119,11 @@ impl<T: 'static> UseState<T> {
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let (value, set_value) = use_state(&cx, || 0);
|
||||
/// let value = use_state(&cx, || 0);
|
||||
///
|
||||
/// rsx!{
|
||||
/// Component {
|
||||
/// handler: set_val.setter()
|
||||
/// handler: value.setter()
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
@@ -137,16 +144,16 @@ impl<T: 'static> UseState<T> {
|
||||
/// # use dioxus_core::prelude::*;
|
||||
/// # use dioxus_hooks::*;
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let (value, set_value) = use_state(&cx, || 0);
|
||||
/// let value = use_state(&cx, || 0);
|
||||
///
|
||||
/// // to increment the value
|
||||
/// set_value.modify(|v| v + 1);
|
||||
/// value.modify(|v| v + 1);
|
||||
///
|
||||
/// // usage in async
|
||||
/// cx.spawn({
|
||||
/// let set_value = set_value.to_owned();
|
||||
/// let value = value.to_owned();
|
||||
/// async move {
|
||||
/// set_value.modify(|v| v + 1);
|
||||
/// value.modify(|v| v + 1);
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
@@ -178,16 +185,21 @@ impl<T: 'static> UseState<T> {
|
||||
/// # use dioxus_core::prelude::*;
|
||||
/// # use dioxus_hooks::*;
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let (value, set_value) = use_state(&cx, || 0);
|
||||
/// let value = use_state(&cx, || 0);
|
||||
///
|
||||
/// let as_rc = set_value.get();
|
||||
/// let as_rc = value.get();
|
||||
/// assert_eq!(as_rc.as_ref(), &0);
|
||||
///
|
||||
/// # todo!()
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn get(&self) -> &Rc<T> {
|
||||
pub fn get(&self) -> &T {
|
||||
&self.current_val
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_rc(&self) -> &Rc<T> {
|
||||
&self.current_val
|
||||
}
|
||||
|
||||
@@ -195,12 +207,12 @@ impl<T: 'static> UseState<T> {
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// fn component(cx: Scope) -> Element {
|
||||
/// let (count, set_count) = use_state(&cx, || 0);
|
||||
/// let count = use_state(&cx, || 0);
|
||||
/// cx.spawn({
|
||||
/// let set_count = set_count.to_owned();
|
||||
/// let count = count.to_owned();
|
||||
/// async move {
|
||||
/// // for the component to re-render
|
||||
/// set_count.needs_update();
|
||||
/// count.needs_update();
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
@@ -225,9 +237,9 @@ impl<T: Clone> UseState<T> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let (val, set_val) = use_state(&cx, || 0);
|
||||
/// let val = use_state(&cx, || 0);
|
||||
///
|
||||
/// set_val.with_mut(|v| *v = 1);
|
||||
/// val.with_mut(|v| *v = 1);
|
||||
/// ```
|
||||
pub fn with_mut(&self, apply: impl FnOnce(&mut T)) {
|
||||
let mut slot = self.slot.borrow_mut();
|
||||
@@ -257,9 +269,9 @@ impl<T: Clone> UseState<T> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let (val, set_val) = use_state(&cx, || 0);
|
||||
/// let val = use_state(&cx, || 0);
|
||||
///
|
||||
/// set_val.with_mut(|v| *v = 1);
|
||||
/// *val.make_mut() += 1;
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn make_mut(&self) -> RefMut<T> {
|
||||
@@ -273,6 +285,12 @@ impl<T: Clone> UseState<T> {
|
||||
|
||||
RefMut::map(slot, |rc| Rc::get_mut(rc).expect("the hard count to be 0"))
|
||||
}
|
||||
|
||||
/// Convert this handle to a tuple of the value and the handle itself.
|
||||
#[must_use]
|
||||
pub fn split(&self) -> (&T, &Self) {
|
||||
(&self.current_val, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Clone for UseState<T> {
|
||||
@@ -292,10 +310,28 @@ impl<'a, T: 'static + Display> std::fmt::Display for UseState<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<UseState<T>> for UseState<T> {
|
||||
impl<'a, T: std::fmt::Binary> std::fmt::Binary for UseState<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:b}", self.current_val.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq<T> for UseState<T> {
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
self.current_val.as_ref() == other
|
||||
}
|
||||
}
|
||||
|
||||
// todo: this but for more interesting conrete types
|
||||
impl PartialEq<bool> for &UseState<bool> {
|
||||
fn eq(&self, other: &bool) -> bool {
|
||||
self.current_val.as_ref() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq<UseState<T>> for UseState<T> {
|
||||
fn eq(&self, other: &UseState<T>) -> bool {
|
||||
// some level of memoization for UseState
|
||||
Rc::ptr_eq(&self.slot, &other.slot)
|
||||
Rc::ptr_eq(&self.current_val, &other.current_val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,10 +342,105 @@ impl<T: Debug> Debug for UseState<T> {
|
||||
}
|
||||
|
||||
impl<'a, T> std::ops::Deref for UseState<T> {
|
||||
type Target = Rc<dyn Fn(T)>;
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.setter
|
||||
self.current_val.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Not + Copy> std::ops::Not for &UseState<T> {
|
||||
type Output = <T as std::ops::Not>::Output;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
self.current_val.not()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Not + Copy> std::ops::Not for UseState<T> {
|
||||
type Output = <T as std::ops::Not>::Output;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
self.current_val.not()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::ops::Add + Copy> std::ops::Add<T> for &UseState<T> {
|
||||
type Output = <T as std::ops::Add>::Output;
|
||||
|
||||
fn add(self, other: T) -> Self::Output {
|
||||
*self.current_val.as_ref() + other
|
||||
}
|
||||
}
|
||||
impl<T: std::ops::Sub + Copy> std::ops::Sub<T> for &UseState<T> {
|
||||
type Output = <T as std::ops::Sub>::Output;
|
||||
|
||||
fn sub(self, other: T) -> Self::Output {
|
||||
*self.current_val.as_ref() - other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::ops::Div + Copy> std::ops::Div<T> for &UseState<T> {
|
||||
type Output = <T as std::ops::Div>::Output;
|
||||
|
||||
fn div(self, other: T) -> Self::Output {
|
||||
*self.current_val.as_ref() / other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::ops::Mul + Copy> std::ops::Mul<T> for &UseState<T> {
|
||||
type Output = <T as std::ops::Mul>::Output;
|
||||
|
||||
fn mul(self, other: T) -> Self::Output {
|
||||
*self.current_val.as_ref() * other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Add<Output = T> + Copy> std::ops::AddAssign<T> for &UseState<T> {
|
||||
fn add_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) + rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Sub<Output = T> + Copy> std::ops::SubAssign<T> for &UseState<T> {
|
||||
fn sub_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) - rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Mul<Output = T> + Copy> std::ops::MulAssign<T> for &UseState<T> {
|
||||
fn mul_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) * rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Div<Output = T> + Copy> std::ops::DivAssign<T> for &UseState<T> {
|
||||
fn div_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) / rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Add<Output = T> + Copy> std::ops::AddAssign<T> for UseState<T> {
|
||||
fn add_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) + rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Sub<Output = T> + Copy> std::ops::SubAssign<T> for UseState<T> {
|
||||
fn sub_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) - rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Mul<Output = T> + Copy> std::ops::MulAssign<T> for UseState<T> {
|
||||
fn mul_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) * rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Div<Output = T> + Copy> std::ops::DivAssign<T> for UseState<T> {
|
||||
fn div_assign(&mut self, rhs: T) {
|
||||
self.set((*self.current()) / rhs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,16 +448,16 @@ impl<'a, T> std::ops::Deref for UseState<T> {
|
||||
fn api_makes_sense() {
|
||||
#[allow(unused)]
|
||||
fn app(cx: Scope) -> Element {
|
||||
let (val, set_val) = use_state(&cx, || 0);
|
||||
let val = use_state(&cx, || 0);
|
||||
|
||||
set_val(0);
|
||||
set_val.modify(|v| v + 1);
|
||||
let real_current = set_val.current();
|
||||
val.set(0);
|
||||
val.modify(|v| v + 1);
|
||||
let real_current = val.current();
|
||||
|
||||
match val {
|
||||
match val.get() {
|
||||
10 => {
|
||||
set_val(20);
|
||||
set_val.modify(|v| v + 1);
|
||||
val.set(20);
|
||||
val.modify(|v| v + 1);
|
||||
}
|
||||
20 => {}
|
||||
_ => {
|
||||
@@ -335,9 +466,9 @@ fn api_makes_sense() {
|
||||
}
|
||||
|
||||
cx.spawn({
|
||||
crate::to_owned![set_val];
|
||||
dioxus_core::to_owned![val];
|
||||
async move {
|
||||
set_val.modify(|f| f + 1);
|
||||
val.modify(|f| f + 1);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-html"
|
||||
version = "0.1.6"
|
||||
version = "0.2.0"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
@@ -11,7 +11,7 @@ documentation = "https://docs.rs/dioxus"
|
||||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.1.9" }
|
||||
dioxus-core = { path = "../core", version = "^0.2.0" }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
serde_repr = { version = "0.1", optional = true }
|
||||
wasm-bindgen = { version = "0.2.79", optional = true }
|
||||
|
||||
@@ -193,6 +193,9 @@ pub mod on {
|
||||
/// ondoubleclick
|
||||
ondoubleclick
|
||||
|
||||
/// ondoubleclick
|
||||
ondblclick
|
||||
|
||||
/// ondrag
|
||||
ondrag
|
||||
|
||||
@@ -608,7 +611,7 @@ pub mod on {
|
||||
feature = "serialize",
|
||||
derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr)
|
||||
)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyCode {
|
||||
// That key has no keycode, = 0
|
||||
|
||||
@@ -118,524 +118,411 @@ pub trait GlobalAttributes {
|
||||
//
|
||||
// This roughly follows the html spec
|
||||
style_trait_methods! {
|
||||
/// Specifies the alignment of flexible container's items within the flex container.
|
||||
align_content: "align-content",
|
||||
|
||||
/// Specifies the default alignment for items within the flex container.
|
||||
align_items: "align-items",
|
||||
|
||||
/// Specifies the alignment for selected items within the flex container.
|
||||
align_self: "align-self",
|
||||
|
||||
/// Specifies the keyframe_based animations.
|
||||
alignment_adjust: "alignment-adjust",
|
||||
alignment_baseline: "alignment-baseline",
|
||||
all: "all",
|
||||
alt: "alt",
|
||||
animation: "animation",
|
||||
|
||||
/// Specifies when the animation will start.
|
||||
animation_delay: "animation-delay",
|
||||
|
||||
/// Specifies whether the animation should play in reverse on alternate cycles or not.
|
||||
animation_direction: "animation-direction",
|
||||
|
||||
/// Specifies the number of seconds or milliseconds an animation should take to complete one cycle
|
||||
animation_duration: "animation-duration",
|
||||
|
||||
/// Specifies how a CSS animation should apply styles to its target before and after it is executing
|
||||
animation_fill_mode: "animation-fill-mode",
|
||||
|
||||
/// Specifies the number of times an animation cycle should be played before stopping.
|
||||
animation_iteration_count: "animation-iteration-count",
|
||||
|
||||
/// Specifies the name of @keyframes defined animations that should be applied to the selected element
|
||||
animation_name: "animation-name",
|
||||
|
||||
/// Specifies whether the animation is running or paused.
|
||||
animation_play_state: "animation-play-state",
|
||||
|
||||
/// Specifies how a CSS animation should progress over the duration of each cycle.
|
||||
animation_timing_function: "animation-timing-function",
|
||||
|
||||
/// Specifies whether or not the "back" side of a transformed element is visible when facing the user.
|
||||
azimuth: "azimuth",
|
||||
backface_visibility: "backface-visibility",
|
||||
|
||||
/// Defines a variety of background properties within one declaration.
|
||||
background: "background",
|
||||
|
||||
/// Specify whether the background image is fixed in the viewport or scrolls.
|
||||
background_attachment: "background-attachment",
|
||||
|
||||
/// Specifies the painting area of the background.
|
||||
background_clip: "background-clip",
|
||||
|
||||
/// Defines an element's background color.
|
||||
background_color: "background-color",
|
||||
|
||||
/// Defines an element's background image.
|
||||
background_image: "background-image",
|
||||
|
||||
/// Specifies the positioning area of the background images.
|
||||
background_origin: "background-origin",
|
||||
|
||||
/// Defines the origin of a background image.
|
||||
background_position: "background-position",
|
||||
|
||||
/// Specify whether/how the background image is tiled.
|
||||
background_repeat: "background-repeat",
|
||||
|
||||
/// Specifies the size of the background images.
|
||||
background_size: "background-size",
|
||||
|
||||
/// Sets the width, style, and color for all four sides of an element's border.
|
||||
background_blend_mode: "background-blend-mode",
|
||||
baseline_shift: "baseline-shift",
|
||||
bleed: "bleed",
|
||||
bookmark_label: "bookmark-label",
|
||||
bookmark_level: "bookmark-level",
|
||||
bookmark_state: "bookmark-state",
|
||||
border: "border",
|
||||
|
||||
/// Sets the width, style, and color of the bottom border of an element.
|
||||
border_bottom: "border-bottom",
|
||||
|
||||
/// Sets the color of the bottom border of an element.
|
||||
border_bottom_color: "border-bottom-color",
|
||||
|
||||
/// Defines the shape of the bottom_left border corner of an element.
|
||||
border_bottom_left_radius: "border-bottom-left-radius",
|
||||
|
||||
/// Defines the shape of the bottom_right border corner of an element.
|
||||
border_bottom_right_radius: "border-bottom-right-radius",
|
||||
|
||||
/// Sets the style of the bottom border of an element.
|
||||
border_bottom_style: "border-bottom-style",
|
||||
|
||||
/// Sets the width of the bottom border of an element.
|
||||
border_bottom_width: "border-bottom-width",
|
||||
|
||||
/// Specifies whether table cell borders are connected or separated.
|
||||
border_collapse: "border-collapse",
|
||||
|
||||
/// Sets the color of the border on all the four sides of an element.
|
||||
border_color: "border-color",
|
||||
|
||||
/// Specifies how an image is to be used in place of the border styles.
|
||||
border_image: "border-image",
|
||||
|
||||
/// Specifies the amount by which the border image area extends beyond the border box.
|
||||
border_image_outset: "border-image-outset",
|
||||
|
||||
/// Specifies whether the image_border should be repeated, rounded or stretched.
|
||||
border_image_repeat: "border-image-repeat",
|
||||
|
||||
/// Specifies the inward offsets of the image_border.
|
||||
border_image_slice: "border-image-slice",
|
||||
|
||||
/// Specifies the location of the image to be used as a border.
|
||||
border_image_source: "border-image-source",
|
||||
|
||||
/// Specifies the width of the image_border.
|
||||
border_image_width: "border-image-width",
|
||||
|
||||
/// Sets the width, style, and color of the left border of an element.
|
||||
border_left: "border-left",
|
||||
|
||||
/// Sets the color of the left border of an element.
|
||||
border_left_color: "border-left-color",
|
||||
|
||||
/// Sets the style of the left border of an element.
|
||||
border_left_style: "border-left-style",
|
||||
|
||||
/// Sets the width of the left border of an element.
|
||||
border_left_width: "border-left-width",
|
||||
|
||||
/// Defines the shape of the border corners of an element.
|
||||
border_radius: "border-radius",
|
||||
|
||||
/// Sets the width, style, and color of the right border of an element.
|
||||
border_right: "border-right",
|
||||
|
||||
/// Sets the color of the right border of an element.
|
||||
border_right_color: "border-right-color",
|
||||
|
||||
/// Sets the style of the right border of an element.
|
||||
border_right_style: "border-right-style",
|
||||
|
||||
/// Sets the width of the right border of an element.
|
||||
border_right_width: "border-right-width",
|
||||
|
||||
/// Sets the spacing between the borders of adjacent table cells.
|
||||
border_spacing: "border-spacing",
|
||||
|
||||
/// Sets the style of the border on all the four sides of an element.
|
||||
border_style: "border-style",
|
||||
|
||||
/// Sets the width, style, and color of the top border of an element.
|
||||
border_top: "border-top",
|
||||
|
||||
/// Sets the color of the top border of an element.
|
||||
border_top_color: "border-top-color",
|
||||
|
||||
/// Defines the shape of the top_left border corner of an element.
|
||||
border_top_left_radius: "border-top-left-radius",
|
||||
|
||||
/// Defines the shape of the top_right border corner of an element.
|
||||
border_top_right_radius: "border-top-right-radius",
|
||||
|
||||
/// Sets the style of the top border of an element.
|
||||
border_top_style: "border-top-style",
|
||||
|
||||
/// Sets the width of the top border of an element.
|
||||
border_top_width: "border-top-width",
|
||||
|
||||
/// Sets the width of the border on all the four sides of an element.
|
||||
border_width: "border-width",
|
||||
|
||||
/// Specify the location of the bottom edge of the positioned element.
|
||||
border_bottom: "border-bottom",
|
||||
border_bottom_color: "border-bottom-color",
|
||||
border_bottom_style: "border-bottom-style",
|
||||
border_bottom_width: "border-bottom-width",
|
||||
border_left: "border-left",
|
||||
border_left_color: "border-left-color",
|
||||
border_left_style: "border-left-style",
|
||||
border_left_width: "border-left-width",
|
||||
border_right: "border-right",
|
||||
border_right_color: "border-right-color",
|
||||
border_right_style: "border-right-style",
|
||||
border_right_width: "border-right-width",
|
||||
border_top: "border-top",
|
||||
border_top_color: "border-top-color",
|
||||
border_top_style: "border-top-style",
|
||||
border_top_width: "border-top-width",
|
||||
border_collapse: "border-collapse",
|
||||
border_image: "border-image",
|
||||
border_image_outset: "border-image-outset",
|
||||
border_image_repeat: "border-image-repeat",
|
||||
border_image_slice: "border-image-slice",
|
||||
border_image_source: "border-image-source",
|
||||
border_image_width: "border-image-width",
|
||||
border_radius: "border-radius",
|
||||
border_bottom_left_radius: "border-bottom-left-radius",
|
||||
border_bottom_right_radius: "border-bottom-right-radius",
|
||||
border_top_left_radius: "border-top-left-radius",
|
||||
border_top_right_radius: "border-top-right-radius",
|
||||
border_spacing: "border-spacing",
|
||||
bottom: "bottom",
|
||||
|
||||
/// Applies one or more drop_shadows to the element's box.
|
||||
box_decoration_break: "box-decoration-break",
|
||||
box_shadow: "box-shadow",
|
||||
|
||||
/// Alter the default CSS box model.
|
||||
box_sizing: "box-sizing",
|
||||
|
||||
/// Specify the position of table's caption.
|
||||
box_snap: "box-snap",
|
||||
break_after: "break-after",
|
||||
break_before: "break-before",
|
||||
break_inside: "break-inside",
|
||||
buffered_rendering: "buffered-rendering",
|
||||
caption_side: "caption-side",
|
||||
|
||||
/// Specifies the placement of an element in relation to floating elements.
|
||||
clear: "clear",
|
||||
|
||||
/// Defines the clipping region.
|
||||
clear_side: "clear-side",
|
||||
clip: "clip",
|
||||
|
||||
/// Specify the color of the text of an element.
|
||||
clip_path: "clip-path",
|
||||
clip_rule: "clip-rule",
|
||||
color: "color",
|
||||
|
||||
/// Specifies the number of columns in a multi_column element.
|
||||
column_count: "column-count",
|
||||
|
||||
/// Specifies how columns will be filled.
|
||||
color_adjust: "color-adjust",
|
||||
color_correction: "color-correction",
|
||||
color_interpolation: "color-interpolation",
|
||||
color_interpolation_filters: "color-interpolation-filters",
|
||||
color_profile: "color-profile",
|
||||
color_rendering: "color-rendering",
|
||||
column_fill: "column-fill",
|
||||
|
||||
/// Specifies the gap between the columns in a multi_column element.
|
||||
column_gap: "column-gap",
|
||||
|
||||
/// Specifies a straight line, or "rule", to be drawn between each column in a multi_column element.
|
||||
column_rule: "column-rule",
|
||||
|
||||
/// Specifies the color of the rules drawn between columns in a multi_column layout.
|
||||
column_rule_color: "column-rule-color",
|
||||
|
||||
/// Specifies the style of the rule drawn between the columns in a multi_column layout.
|
||||
column_rule_style: "column-rule-style",
|
||||
|
||||
/// Specifies the width of the rule drawn between the columns in a multi_column layout.
|
||||
column_rule_width: "column-rule-width",
|
||||
|
||||
/// Specifies how many columns an element spans across in a multi_column layout.
|
||||
column_span: "column-span",
|
||||
|
||||
/// Specifies the optimal width of the columns in a multi_column element.
|
||||
column_width: "column-width",
|
||||
|
||||
/// A shorthand property for setting column_width and column_count properties.
|
||||
columns: "columns",
|
||||
|
||||
/// Inserts generated content.
|
||||
column_count: "column-count",
|
||||
column_width: "column-width",
|
||||
contain: "contain",
|
||||
content: "content",
|
||||
|
||||
/// Increments one or more counter values.
|
||||
counter_increment: "counter-increment",
|
||||
|
||||
/// Creates or resets one or more counters.
|
||||
counter_reset: "counter-reset",
|
||||
|
||||
/// Specify the type of cursor.
|
||||
counter_set: "counter-set",
|
||||
cue: "cue",
|
||||
cue_after: "cue-after",
|
||||
cue_before: "cue-before",
|
||||
cursor: "cursor",
|
||||
|
||||
/// Define the text direction/writing direction.
|
||||
direction: "direction",
|
||||
|
||||
/// Specifies how an element is displayed onscreen.
|
||||
display: "display",
|
||||
|
||||
/// Show or hide borders and backgrounds of empty table cells.
|
||||
display_inside: "display-inside",
|
||||
display_outside: "display-outside",
|
||||
display_extras: "display-extras",
|
||||
display_box: "display-box",
|
||||
dominant_baseline: "dominant-baseline",
|
||||
elevation: "elevation",
|
||||
empty_cells: "empty-cells",
|
||||
|
||||
/// Specifies the components of a flexible length.
|
||||
flex: "flex",
|
||||
|
||||
/// Specifies the initial main size of the flex item.
|
||||
flex_basis: "flex-basis",
|
||||
|
||||
/// Specifies the direction of the flexible items.
|
||||
flex_direction: "flex-direction",
|
||||
|
||||
/// A shorthand property for the flex_direction and the flex_wrap properties.
|
||||
flex_flow: "flex-flow",
|
||||
|
||||
/// Specifies how the flex item will grow relative to the other items inside the flex container.
|
||||
flex_grow: "flex-grow",
|
||||
|
||||
/// Specifies how the flex item will shrink relative to the other items inside the flex container
|
||||
flex_shrink: "flex-shrink",
|
||||
|
||||
/// Specifies whether the flexible items should wrap or not.
|
||||
flex_wrap: "flex-wrap",
|
||||
|
||||
/// Specifies whether or not a box should float.
|
||||
enable_background: "enable-background",
|
||||
fill: "fill",
|
||||
fill_opacity: "fill-opacity",
|
||||
fill_rule: "fill-rule",
|
||||
filter: "filter",
|
||||
float: "float",
|
||||
|
||||
/// Defines a variety of font properties within one declaration.
|
||||
float_defer_column: "float-defer-column",
|
||||
float_defer_page: "float-defer-page",
|
||||
float_offset: "float-offset",
|
||||
float_wrap: "float-wrap",
|
||||
flow_into: "flow-into",
|
||||
flow_from: "flow-from",
|
||||
flex: "flex",
|
||||
flex_basis: "flex-basis",
|
||||
flex_grow: "flex-grow",
|
||||
flex_shrink: "flex-shrink",
|
||||
flex_flow: "flex-flow",
|
||||
flex_direction: "flex-direction",
|
||||
flex_wrap: "flex-wrap",
|
||||
flood_color: "flood-color",
|
||||
flood_opacity: "flood-opacity",
|
||||
font: "font",
|
||||
|
||||
/// Defines a list of fonts for element.
|
||||
font_family: "font-family",
|
||||
|
||||
/// Defines the font size for the text.
|
||||
font_size: "font-size",
|
||||
|
||||
/// Preserves the readability of text when font fallback occurs.
|
||||
font_size_adjust: "font-size-adjust",
|
||||
|
||||
/// Selects a normal, condensed, or expanded face from a font.
|
||||
font_stretch: "font-stretch",
|
||||
|
||||
/// Defines the font style for the text.
|
||||
font_style: "font-style",
|
||||
|
||||
/// Specify the font variant.
|
||||
font_variant: "font-variant",
|
||||
|
||||
/// Specify the font weight of the text.
|
||||
font_weight: "font-weight",
|
||||
|
||||
/// Sets gaps (gutters) between rows and columns. Shorthand for row_gap and column_gap.
|
||||
gap: "gap",
|
||||
|
||||
/// Specify the height of an element.
|
||||
font_feature_settings: "font-feature-settings",
|
||||
font_kerning: "font-kerning",
|
||||
font_language_override: "font-language-override",
|
||||
font_size_adjust: "font-size-adjust",
|
||||
font_synthesis: "font-synthesis",
|
||||
font_variant: "font-variant",
|
||||
font_variant_alternates: "font-variant-alternates",
|
||||
font_variant_caps: "font-variant-caps",
|
||||
font_variant_east_asian: "font-variant-east-asian",
|
||||
font_variant_ligatures: "font-variant-ligatures",
|
||||
font_variant_numeric: "font-variant-numeric",
|
||||
font_variant_position: "font-variant-position",
|
||||
footnote_policy: "footnote-policy",
|
||||
glyph_orientation_horizontal: "glyph-orientation-horizontal",
|
||||
glyph_orientation_vertical: "glyph-orientation-vertical",
|
||||
grid: "grid",
|
||||
grid_auto_flow: "grid-auto-flow",
|
||||
grid_auto_columns: "grid-auto-columns",
|
||||
grid_auto_rows: "grid-auto-rows",
|
||||
grid_template: "grid-template",
|
||||
grid_template_areas: "grid-template-areas",
|
||||
grid_template_columns: "grid-template-columns",
|
||||
grid_template_rows: "grid-template-rows",
|
||||
grid_area: "grid-area",
|
||||
grid_column: "grid-column",
|
||||
grid_column_start: "grid-column-start",
|
||||
grid_column_end: "grid-column-end",
|
||||
grid_row: "grid-row",
|
||||
grid_row_start: "grid-row-start",
|
||||
grid_row_end: "grid-row-end",
|
||||
hanging_punctuation: "hanging-punctuation",
|
||||
height: "height",
|
||||
|
||||
/// Specifies how flex items are aligned along the main axis of the flex container after any flexible lengths and auto margins have been resolved.
|
||||
hyphenate_character: "hyphenate-character",
|
||||
hyphenate_limit_chars: "hyphenate-limit-chars",
|
||||
hyphenate_limit_last: "hyphenate-limit-last",
|
||||
hyphenate_limit_lines: "hyphenate-limit-lines",
|
||||
hyphenate_limit_zone: "hyphenate-limit-zone",
|
||||
hyphens: "hyphens",
|
||||
icon: "icon",
|
||||
image_orientation: "image-orientation",
|
||||
image_resolution: "image-resolution",
|
||||
image_rendering: "image-rendering",
|
||||
ime: "ime",
|
||||
ime_align: "ime-align",
|
||||
ime_mode: "ime-mode",
|
||||
ime_offset: "ime-offset",
|
||||
ime_width: "ime-width",
|
||||
initial_letters: "initial-letters",
|
||||
inline_box_align: "inline-box-align",
|
||||
isolation: "isolation",
|
||||
justify_content: "justify-content",
|
||||
|
||||
/// Specify the location of the left edge of the positioned element.
|
||||
justify_items: "justify-items",
|
||||
justify_self: "justify-self",
|
||||
kerning: "kerning",
|
||||
left: "left",
|
||||
|
||||
/// Sets the extra spacing between letters.
|
||||
letter_spacing: "letter-spacing",
|
||||
|
||||
/// Sets the height between lines of text.
|
||||
lighting_color: "lighting-color",
|
||||
line_box_contain: "line-box-contain",
|
||||
line_break: "line-break",
|
||||
line_grid: "line-grid",
|
||||
line_height: "line-height",
|
||||
|
||||
/// Defines the display style for a list and list elements.
|
||||
line_slack: "line-slack",
|
||||
line_snap: "line-snap",
|
||||
list_style: "list-style",
|
||||
|
||||
/// Specifies the image to be used as a list_item marker.
|
||||
list_style_image: "list-style-image",
|
||||
|
||||
/// Specifies the position of the list_item marker.
|
||||
list_style_position: "list-style-position",
|
||||
|
||||
/// Specifies the marker style for a list_item.
|
||||
list_styler_type: "list-style-type",
|
||||
|
||||
/// Sets the margin on all four sides of the element.
|
||||
list_style_type: "list-style-type",
|
||||
margin: "margin",
|
||||
|
||||
/// Sets the bottom margin of the element.
|
||||
margin_bottom: "margin-bottom",
|
||||
|
||||
/// Sets the left margin of the element.
|
||||
margin_left: "margin-left",
|
||||
|
||||
/// Sets the right margin of the element.
|
||||
margin_right: "margin-right",
|
||||
|
||||
/// Sets the top margin of the element.
|
||||
margin_top: "margin-top",
|
||||
|
||||
/// Specify the maximum height of an element.
|
||||
marker: "marker",
|
||||
marker_end: "marker-end",
|
||||
marker_mid: "marker-mid",
|
||||
marker_pattern: "marker-pattern",
|
||||
marker_segment: "marker-segment",
|
||||
marker_start: "marker-start",
|
||||
marker_knockout_left: "marker-knockout-left",
|
||||
marker_knockout_right: "marker-knockout-right",
|
||||
marker_side: "marker-side",
|
||||
marks: "marks",
|
||||
marquee_direction: "marquee-direction",
|
||||
marquee_play_count: "marquee-play-count",
|
||||
marquee_speed: "marquee-speed",
|
||||
marquee_style: "marquee-style",
|
||||
mask: "mask",
|
||||
mask_image: "mask-image",
|
||||
mask_repeat: "mask-repeat",
|
||||
mask_position: "mask-position",
|
||||
mask_clip: "mask-clip",
|
||||
mask_origin: "mask-origin",
|
||||
mask_size: "mask-size",
|
||||
mask_box: "mask-box",
|
||||
mask_box_outset: "mask-box-outset",
|
||||
mask_box_repeat: "mask-box-repeat",
|
||||
mask_box_slice: "mask-box-slice",
|
||||
mask_box_source: "mask-box-source",
|
||||
mask_box_width: "mask-box-width",
|
||||
mask_type: "mask-type",
|
||||
max_height: "max-height",
|
||||
|
||||
/// Specify the maximum width of an element.
|
||||
max_lines: "max-lines",
|
||||
max_width: "max-width",
|
||||
|
||||
/// Specify the minimum height of an element.
|
||||
min_height: "min-height",
|
||||
|
||||
/// Specify the minimum width of an element.
|
||||
min_width: "min-width",
|
||||
|
||||
/// Specifies the transparency of an element.
|
||||
mix_blend_mode: "mix-blend-mode",
|
||||
nav_down: "nav-down",
|
||||
nav_index: "nav-index",
|
||||
nav_left: "nav-left",
|
||||
nav_right: "nav-right",
|
||||
nav_up: "nav-up",
|
||||
object_fit: "object-fit",
|
||||
object_position: "object-position",
|
||||
offset_after: "offset-after",
|
||||
offset_before: "offset-before",
|
||||
offset_end: "offset-end",
|
||||
offset_start: "offset-start",
|
||||
opacity: "opacity",
|
||||
|
||||
/// Specifies the order in which a flex items are displayed and laid out within a flex container.
|
||||
order: "order",
|
||||
|
||||
/// Sets the width, style, and color for all four sides of an element's outline.
|
||||
orphans: "orphans",
|
||||
outline: "outline",
|
||||
|
||||
/// Sets the color of the outline.
|
||||
outline_color: "outline-color",
|
||||
|
||||
/// Set the space between an outline and the border edge of an element.
|
||||
outline_offset: "outline-offset",
|
||||
|
||||
/// Sets a style for an outline.
|
||||
outline_style: "outline-style",
|
||||
|
||||
/// Sets the width of the outline.
|
||||
outline_width: "outline-width",
|
||||
|
||||
/// Specifies the treatment of content that overflows the element's box.
|
||||
outline_offset: "outline-offset",
|
||||
overflow: "overflow",
|
||||
|
||||
/// Specifies the treatment of content that overflows the element's box horizontally.
|
||||
overflow_x: "overflow-x",
|
||||
|
||||
/// Specifies the treatment of content that overflows the element's box vertically.
|
||||
overflow_y: "overflow-y",
|
||||
|
||||
/// Sets the padding on all four sides of the element.
|
||||
overflow_style: "overflow-style",
|
||||
overflow_wrap: "overflow-wrap",
|
||||
padding: "padding",
|
||||
|
||||
/// Sets the padding to the bottom side of an element.
|
||||
padding_bottom: "padding-bottom",
|
||||
|
||||
/// Sets the padding to the left side of an element.
|
||||
padding_left: "padding-left",
|
||||
|
||||
/// Sets the padding to the right side of an element.
|
||||
padding_right: "padding-right",
|
||||
|
||||
/// Sets the padding to the top side of an element.
|
||||
padding_top: "padding-top",
|
||||
|
||||
/// Insert a page breaks after an element.
|
||||
page: "page",
|
||||
page_break_after: "page-break-after",
|
||||
|
||||
/// Insert a page breaks before an element.
|
||||
page_break_before: "page-break-before",
|
||||
|
||||
/// Insert a page breaks inside an element.
|
||||
page_break_inside: "page-break-inside",
|
||||
|
||||
/// Defines the perspective from which all child elements of the object are viewed.
|
||||
paint_order: "paint-order",
|
||||
pause: "pause",
|
||||
pause_after: "pause-after",
|
||||
pause_before: "pause-before",
|
||||
perspective: "perspective",
|
||||
|
||||
/// Defines the origin (the vanishing point for the 3D space) for the perspective property.
|
||||
perspective_origin: "perspective-origin",
|
||||
|
||||
/// Specifies how an element is positioned.
|
||||
position: "position",
|
||||
|
||||
/// The pointer-events CSS property sets under what circumstances (if any) a particular graphic element can
|
||||
/// become the target of pointer events.
|
||||
///
|
||||
/// MDN: [`pointer_events`](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events)
|
||||
pitch: "pitch",
|
||||
pitch_range: "pitch-range",
|
||||
play_during: "play-during",
|
||||
pointer_events: "pointer-events",
|
||||
|
||||
/// Specifies quotation marks for embedded quotations.
|
||||
position: "position",
|
||||
quotes: "quotes",
|
||||
|
||||
/// Specifies whether or not an element is resizable by the user.
|
||||
region_fragment: "region-fragment",
|
||||
resize: "resize",
|
||||
|
||||
/// Specify the location of the right edge of the positioned element.
|
||||
rest: "rest",
|
||||
rest_after: "rest-after",
|
||||
rest_before: "rest-before",
|
||||
richness: "richness",
|
||||
right: "right",
|
||||
|
||||
/// Specifies the gap between the rows in a multi_column element.
|
||||
row_gap: "row-gap",
|
||||
|
||||
/// Specifies the length of the tab character.
|
||||
ruby_align: "ruby-align",
|
||||
ruby_merge: "ruby-merge",
|
||||
ruby_position: "ruby-position",
|
||||
scroll_behavior: "scroll-behavior",
|
||||
scroll_snap_coordinate: "scroll-snap-coordinate",
|
||||
scroll_snap_destination: "scroll-snap-destination",
|
||||
scroll_snap_points_x: "scroll-snap-points-x",
|
||||
scroll_snap_points_y: "scroll-snap-points-y",
|
||||
scroll_snap_type: "scroll-snap-type",
|
||||
shape_image_threshold: "shape-image-threshold",
|
||||
shape_inside: "shape-inside",
|
||||
shape_margin: "shape-margin",
|
||||
shape_outside: "shape-outside",
|
||||
shape_padding: "shape-padding",
|
||||
shape_rendering: "shape-rendering",
|
||||
size: "size",
|
||||
speak: "speak",
|
||||
speak_as: "speak-as",
|
||||
speak_header: "speak-header",
|
||||
speak_numeral: "speak-numeral",
|
||||
speak_punctuation: "speak-punctuation",
|
||||
speech_rate: "speech-rate",
|
||||
stop_color: "stop-color",
|
||||
stop_opacity: "stop-opacity",
|
||||
stress: "stress",
|
||||
string_set: "string-set",
|
||||
stroke: "stroke",
|
||||
stroke_dasharray: "stroke-dasharray",
|
||||
stroke_dashoffset: "stroke-dashoffset",
|
||||
stroke_linecap: "stroke-linecap",
|
||||
stroke_linejoin: "stroke-linejoin",
|
||||
stroke_miterlimit: "stroke-miterlimit",
|
||||
stroke_opacity: "stroke-opacity",
|
||||
stroke_width: "stroke-width",
|
||||
tab_size: "tab-size",
|
||||
|
||||
/// Specifies a table layout algorithm.
|
||||
table_layout: "table-layout",
|
||||
|
||||
/// Sets the horizontal alignment of inline content.
|
||||
text_align: "text-align",
|
||||
|
||||
/// Specifies how the last line of a block or a line right before a forced line break is aligned when is justify.",
|
||||
text_align_all: "text-align-all",
|
||||
text_align_last: "text-align-last",
|
||||
|
||||
/// Specifies the decoration added to text.
|
||||
text_anchor: "text-anchor",
|
||||
text_combine_upright: "text-combine-upright",
|
||||
text_decoration: "text-decoration",
|
||||
|
||||
/// Specifies the color of the text_decoration_line.
|
||||
text_decoration_color: "text-decoration-color",
|
||||
|
||||
/// Specifies what kind of line decorations are added to the element.
|
||||
text_decoration_line: "text-decoration-line",
|
||||
|
||||
/// Specifies the style of the lines specified by the text_decoration_line property
|
||||
text_decoration_style: "text-decoration-style",
|
||||
|
||||
/// Indent the first line of text.
|
||||
text_decoration_skip: "text-decoration-skip",
|
||||
text_emphasis: "text-emphasis",
|
||||
text_emphasis_color: "text-emphasis-color",
|
||||
text_emphasis_style: "text-emphasis-style",
|
||||
text_emphasis_position: "text-emphasis-position",
|
||||
text_emphasis_skip: "text-emphasis-skip",
|
||||
text_height: "text-height",
|
||||
text_indent: "text-indent",
|
||||
|
||||
/// Specifies the justification method to use when the text_align property is set to justify.
|
||||
text_justify: "text-justify",
|
||||
|
||||
/// Specifies how the text content will be displayed, when it overflows the block containers.
|
||||
text_orientation: "text-orientation",
|
||||
text_overflow: "text-overflow",
|
||||
|
||||
/// Applies one or more shadows to the text content of an element.
|
||||
text_rendering: "text-rendering",
|
||||
text_shadow: "text-shadow",
|
||||
|
||||
/// Transforms the case of the text.
|
||||
text_size_adjust: "text-size-adjust",
|
||||
text_space_collapse: "text-space-collapse",
|
||||
text_spacing: "text-spacing",
|
||||
text_transform: "text-transform",
|
||||
|
||||
/// Specify the location of the top edge of the positioned element.
|
||||
text_underline_position: "text-underline-position",
|
||||
text_wrap: "text-wrap",
|
||||
top: "top",
|
||||
|
||||
/// Applies a 2D or 3D transformation to an element.
|
||||
touch_action: "touch-action",
|
||||
transform: "transform",
|
||||
|
||||
/// Defines the origin of transformation for an element.
|
||||
transform_box: "transform-box",
|
||||
transform_origin: "transform-origin",
|
||||
|
||||
/// Specifies how nested elements are rendered in 3D space.
|
||||
transform_style: "transform-style",
|
||||
|
||||
/// Defines the transition between two states of an element.
|
||||
transition: "transition",
|
||||
|
||||
/// Specifies when the transition effect will start.
|
||||
transition_delay: "transition-delay",
|
||||
|
||||
/// Specifies the number of seconds or milliseconds a transition effect should take to complete.
|
||||
transition_duration: "transition-duration",
|
||||
|
||||
/// Specifies the names of the CSS properties to which a transition effect should be applied.
|
||||
transition_property: "transition-property",
|
||||
|
||||
/// Specifies the speed curve of the transition effect.
|
||||
unicode_bidi: "unicode-bidi",
|
||||
vector_effect: "vector-effect",
|
||||
vertical_align: "vertical-align",
|
||||
visibility: "visibility",
|
||||
voice_balance: "voice-balance",
|
||||
voice_duration: "voice-duration",
|
||||
voice_family: "voice-family",
|
||||
voice_pitch: "voice-pitch",
|
||||
voice_range: "voice-range",
|
||||
voice_rate: "voice-rate",
|
||||
voice_stress: "voice-stress",
|
||||
voice_volumn: "voice-volumn",
|
||||
volume: "volume",
|
||||
white_space: "white-space",
|
||||
widows: "widows",
|
||||
width: "width",
|
||||
will_change: "will-change",
|
||||
word_break: "word-break",
|
||||
word_spacing: "word-spacing",
|
||||
word_wrap: "word-wrap",
|
||||
wrap_flow: "wrap-flow",
|
||||
wrap_through: "wrap-through",
|
||||
writing_mode: "writing-mode",
|
||||
gap: "gap",
|
||||
list_styler_type: "list-style-type",
|
||||
row_gap: "row-gap",
|
||||
transition_timing_function: "transition-timing-function",
|
||||
|
||||
/// The user-select CSS property controls whether the user can select text.
|
||||
/// This doesn't have any effect on content loaded as part of a browser's user interface (its chrome), except in textboxes.
|
||||
user_select: "user-select",
|
||||
webkit_user_select: "-webkit-user-select",
|
||||
|
||||
/// Sets the vertical positioning of an element relative to the current text baseline.
|
||||
vertical_align: "vertical-align",
|
||||
|
||||
/// Specifies whether or not an element is visible.
|
||||
visibility: "visibility",
|
||||
|
||||
/// Specifies how white space inside the element is handled.
|
||||
white_space: "white-space",
|
||||
|
||||
/// Specify the width of an element.
|
||||
width: "width",
|
||||
|
||||
/// Specifies how to break lines within words.
|
||||
word_break: "word-break",
|
||||
|
||||
/// Sets the spacing between words.
|
||||
word_spacing: "word-spacing",
|
||||
|
||||
/// Specifies whether to break words when the content overflows the boundaries of its container.
|
||||
word_wrap: "word-wrap",
|
||||
|
||||
/// Specifies a layering or stacking order for positioned elements.
|
||||
z_index : "z-index",
|
||||
|
||||
z_index : "z-index",
|
||||
}
|
||||
aria_trait_methods! {
|
||||
aria_current: "aria-current",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-interpreter-js"
|
||||
version = "0.0.0"
|
||||
version = "0.2.0"
|
||||
edition = "2018"
|
||||
authors = ["Jonathan Kelley"]
|
||||
description = "JS Intepreter for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
|
||||
@@ -12,11 +12,14 @@ extern "C" {
|
||||
pub fn new(arg: Element) -> Interpreter;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn set_node(this: &Interpreter, id: usize, node: Node);
|
||||
pub fn SetNode(this: &Interpreter, id: usize, node: Node);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn PushRoot(this: &Interpreter, root: u64);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn PopRoot(this: &Interpreter);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn AppendChildren(this: &Interpreter, many: u32);
|
||||
|
||||
@@ -63,5 +66,5 @@ extern "C" {
|
||||
);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str);
|
||||
pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str, ns: Option<&str>);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ export function main() {
|
||||
let root = window.document.getElementById("main");
|
||||
if (root != null) {
|
||||
window.interpreter = new Interpreter(root);
|
||||
window.rpc.call("initialize");
|
||||
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
||||
}
|
||||
}
|
||||
export class Interpreter {
|
||||
@@ -20,10 +20,16 @@ export class Interpreter {
|
||||
pop() {
|
||||
return this.stack.pop();
|
||||
}
|
||||
SetNode(id, node) {
|
||||
this.nodes[id] = node;
|
||||
}
|
||||
PushRoot(root) {
|
||||
const node = this.nodes[root];
|
||||
this.stack.push(node);
|
||||
}
|
||||
PopRoot() {
|
||||
this.stack.pop();
|
||||
}
|
||||
AppendChildren(many) {
|
||||
let root = this.stack[this.stack.length - (1 + many)];
|
||||
let to_add = this.stack.splice(this.stack.length - many);
|
||||
@@ -53,14 +59,12 @@ export class Interpreter {
|
||||
}
|
||||
}
|
||||
CreateTextNode(text, root) {
|
||||
// todo: make it so the types are okay
|
||||
const node = document.createTextNode(text);
|
||||
this.nodes[root] = node;
|
||||
this.stack.push(node);
|
||||
}
|
||||
CreateElement(tag, root) {
|
||||
const el = document.createElement(tag);
|
||||
// el.setAttribute("data-dioxus-id", `${root}`);
|
||||
this.nodes[root] = el;
|
||||
this.stack.push(el);
|
||||
}
|
||||
@@ -79,7 +83,7 @@ export class Interpreter {
|
||||
const element = this.nodes[root];
|
||||
element.setAttribute("data-dioxus-id", `${root}`);
|
||||
if (this.listeners[event_name] === undefined) {
|
||||
this.listeners[event_name] = 0;
|
||||
this.listeners[event_name] = 1;
|
||||
this.handlers[event_name] = handler;
|
||||
this.root.addEventListener(event_name, handler);
|
||||
} else {
|
||||
@@ -102,7 +106,7 @@ export class Interpreter {
|
||||
SetAttribute(root, field, value, ns) {
|
||||
const name = field;
|
||||
const node = this.nodes[root];
|
||||
if (ns == "style") {
|
||||
if (ns === "style") {
|
||||
// @ts-ignore
|
||||
node.style[name] = value;
|
||||
} else if (ns != null || ns != undefined) {
|
||||
@@ -110,7 +114,7 @@ export class Interpreter {
|
||||
} else {
|
||||
switch (name) {
|
||||
case "value":
|
||||
if (value != node.value) {
|
||||
if (value !== node.value) {
|
||||
node.value = value;
|
||||
}
|
||||
break;
|
||||
@@ -125,7 +129,7 @@ export class Interpreter {
|
||||
break;
|
||||
default:
|
||||
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
|
||||
if (value == "false" && bool_attrs.hasOwnProperty(name)) {
|
||||
if (value === "false" && bool_attrs.hasOwnProperty(name)) {
|
||||
node.removeAttribute(name);
|
||||
} else {
|
||||
node.setAttribute(name, value);
|
||||
@@ -133,16 +137,18 @@ export class Interpreter {
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoveAttribute(root, name) {
|
||||
RemoveAttribute(root, field, ns) {
|
||||
const name = field;
|
||||
const node = this.nodes[root];
|
||||
|
||||
if (name === "value") {
|
||||
if (ns !== null || ns !== undefined) {
|
||||
node.removeAttributeNS(ns, name);
|
||||
} else if (name === "value") {
|
||||
node.value = "";
|
||||
} else if (name === "checked") {
|
||||
node.checked = false;
|
||||
} else if (name === "selected") {
|
||||
node.selected = false;
|
||||
} else if (name == "dangerous_inner_html") {
|
||||
} else if (name === "dangerous_inner_html") {
|
||||
node.innerHTML = "";
|
||||
} else {
|
||||
node.removeAttribute(name);
|
||||
@@ -190,9 +196,13 @@ export class Interpreter {
|
||||
this.RemoveEventListener(edit.root, edit.event_name);
|
||||
break;
|
||||
case "NewEventListener":
|
||||
console.log(this.listeners);
|
||||
|
||||
// this handler is only provided on desktop implementations since this
|
||||
// method is not used by the web implementation
|
||||
let handler = (event) => {
|
||||
console.log(event);
|
||||
|
||||
let target = event.target;
|
||||
if (target != null) {
|
||||
let realId = target.getAttribute(`data-dioxus-id`);
|
||||
@@ -200,20 +210,22 @@ export class Interpreter {
|
||||
`dioxus-prevent-default`
|
||||
);
|
||||
|
||||
if (event.type == "click") {
|
||||
if (event.type === "click") {
|
||||
// todo call prevent default if it's the right type of event
|
||||
if (shouldPreventDefault !== `onclick`) {
|
||||
if (target.tagName == "A") {
|
||||
if (target.tagName === "A") {
|
||||
event.preventDefault();
|
||||
const href = target.getAttribute("href");
|
||||
if (href !== "" && href !== null && href !== undefined) {
|
||||
window.rpc.call("browser_open", { href });
|
||||
window.ipc.postMessage(
|
||||
serializeIpcMessage("browser_open", { href })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// also prevent buttons from submitting
|
||||
if (target.tagName == "BUTTON") {
|
||||
if (target.tagName === "BUTTON" && event.type == "submit") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
@@ -237,18 +249,26 @@ export class Interpreter {
|
||||
if (shouldPreventDefault === `on${event.type}`) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (event.type == "submit") {
|
||||
|
||||
if (event.type === "submit") {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (target.tagName == "FORM") {
|
||||
if (
|
||||
target.tagName === "FORM" &&
|
||||
(event.type === "submit" || event.type === "input")
|
||||
) {
|
||||
for (let x = 0; x < target.elements.length; x++) {
|
||||
let element = target.elements[x];
|
||||
let name = element.getAttribute("name");
|
||||
if (name != null) {
|
||||
if (element.getAttribute("type") == "checkbox") {
|
||||
if (element.getAttribute("type") === "checkbox") {
|
||||
// @ts-ignore
|
||||
contents.values[name] = element.checked ? "true" : "false";
|
||||
} else if (element.getAttribute("type") === "radio") {
|
||||
if (element.checked) {
|
||||
contents.values[name] = element.value;
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore
|
||||
contents.values[name] =
|
||||
@@ -258,14 +278,16 @@ export class Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
if (realId == null) {
|
||||
if (realId === null) {
|
||||
return;
|
||||
}
|
||||
window.rpc.call("user_event", {
|
||||
event: edit.event_name,
|
||||
mounted_dom_id: parseInt(realId),
|
||||
contents: contents,
|
||||
});
|
||||
window.ipc.postMessage(
|
||||
serializeIpcMessage("user_event", {
|
||||
event: edit.event_name,
|
||||
mounted_dom_id: parseInt(realId),
|
||||
contents: contents,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
this.NewEventListener(edit.event_name, edit.root, handler);
|
||||
@@ -277,7 +299,7 @@ export class Interpreter {
|
||||
this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
|
||||
break;
|
||||
case "RemoveAttribute":
|
||||
this.RemoveAttribute(edit.root, edit.name);
|
||||
this.RemoveAttribute(edit.root, edit.name, edit.ns);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -341,6 +363,7 @@ export function serialize_event(event) {
|
||||
}
|
||||
return {
|
||||
value: value,
|
||||
values: {},
|
||||
};
|
||||
}
|
||||
case "input":
|
||||
@@ -349,9 +372,11 @@ export function serialize_event(event) {
|
||||
case "submit": {
|
||||
let target = event.target;
|
||||
let value = target.value ?? target.textContent;
|
||||
if (target.type == "checkbox") {
|
||||
|
||||
if (target.type === "checkbox") {
|
||||
value = target.checked ? "true" : "false";
|
||||
}
|
||||
|
||||
return {
|
||||
value: value,
|
||||
values: {},
|
||||
@@ -360,6 +385,7 @@ export function serialize_event(event) {
|
||||
case "click":
|
||||
case "contextmenu":
|
||||
case "doubleclick":
|
||||
case "dblclick":
|
||||
case "drag":
|
||||
case "dragend":
|
||||
case "dragenter":
|
||||
@@ -544,6 +570,9 @@ export function serialize_event(event) {
|
||||
}
|
||||
}
|
||||
}
|
||||
function serializeIpcMessage(method, params = {}) {
|
||||
return JSON.stringify({ method, params });
|
||||
}
|
||||
const bool_attrs = {
|
||||
allowfullscreen: true,
|
||||
allowpaymentrequest: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub static INTERPRTER_JS: &str = include_str!("./interpreter.js");
|
||||
pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
mod bindings;
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"rust-analyzer.cargo.features": [
|
||||
"warp"
|
||||
]
|
||||
}
|
||||
@@ -6,17 +6,29 @@ repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.4.2", optional = true, features = ["ws", "headers"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
futures-util = { version = "0.3", default-features = false, features = [
|
||||
"sink",
|
||||
] }
|
||||
futures-channel = { version = "0.3.17", features = ["sink"] }
|
||||
pretty_env_logger = "0.4"
|
||||
tokio-stream = { version = "0.1.1", features = ["net"] }
|
||||
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.79"
|
||||
tokio-util = { version = "0.7.0", features = ["full"] }
|
||||
|
||||
dioxus-html = { path = "../html", features = ["serialize"] }
|
||||
dioxus-core = { path = "../core", features = ["serialize"] }
|
||||
|
||||
|
||||
# warp
|
||||
warp = { version = "0.3", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["axum"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.14.0", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tower-http = { version = "0.2.0", features = ["fs", "trace"] }
|
||||
headers = "0.3"
|
||||
default = []
|
||||
@@ -1,96 +0,0 @@
|
||||
//! Example websocket server.
|
||||
//!
|
||||
//! Run with
|
||||
//!
|
||||
//! ```not_rust
|
||||
//! cargo run -p example-websockets
|
||||
//! ```
|
||||
|
||||
use axum::{
|
||||
extract::{
|
||||
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
TypedHeader,
|
||||
},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, get_service},
|
||||
Router,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use tower_http::{
|
||||
services::ServeDir,
|
||||
trace::{DefaultMakeSpan, TraceLayer},
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Set the RUST_LOG, if it hasn't been explicitly defined
|
||||
if std::env::var_os("RUST_LOG").is_none() {
|
||||
std::env::set_var("RUST_LOG", "example_websockets=debug,tower_http=debug")
|
||||
}
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// build our application with some routes
|
||||
let app = Router::new()
|
||||
.fallback(
|
||||
get_service(
|
||||
ServeDir::new("examples/axum_assets").append_index_html_on_directories(true),
|
||||
)
|
||||
.handle_error(|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
}),
|
||||
)
|
||||
// routes are matched from bottom to top, so we have to put `nest` at the
|
||||
// top since it matches all routes
|
||||
.route("/ws", get(ws_handler))
|
||||
// logging so we can see whats going on
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(DefaultMakeSpan::default().include_headers(true)),
|
||||
);
|
||||
|
||||
// run it with hyper
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
tracing::debug!("listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
) -> impl IntoResponse {
|
||||
if let Some(TypedHeader(user_agent)) = user_agent {
|
||||
println!("`{}` connected", user_agent.as_str());
|
||||
}
|
||||
|
||||
ws.on_upgrade(handle_socket)
|
||||
}
|
||||
|
||||
async fn handle_socket(mut socket: WebSocket) {
|
||||
if let Some(msg) = socket.recv().await {
|
||||
if let Ok(msg) = msg {
|
||||
println!("Client says: {:?}", msg);
|
||||
} else {
|
||||
println!("client disconnected");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
if socket
|
||||
.send(Message::Text(String::from("Hi!")))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
println!("client disconnected");
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<script src='script.js'></script>
|
||||
@@ -1,9 +0,0 @@
|
||||
const socket = new WebSocket('ws://localhost:3000/ws');
|
||||
|
||||
socket.addEventListener('open', function (event) {
|
||||
socket.send('Hello Server!');
|
||||
});
|
||||
|
||||
socket.addEventListener('message', function (event) {
|
||||
console.log('Message from server ', event.data);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
#![cfg(feature = "warp")]
|
||||
|
||||
use dioxus_core::{Element, LazyNodes, Scope};
|
||||
use dioxus_liveview as liveview;
|
||||
use warp::ws::Ws;
|
||||
use warp::Filter;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let addr = ([127, 0, 0, 1], 3030);
|
||||
|
||||
// todo: compactify this routing under one liveview::app method
|
||||
let view = liveview::new(addr);
|
||||
let body = view.body("<title>Dioxus LiveView</title>");
|
||||
|
||||
let routes = warp::path::end()
|
||||
.map(move || warp::reply::html(body.clone()))
|
||||
.or(warp::path("app")
|
||||
.and(warp::ws())
|
||||
.and(warp::any().map(move || view.clone()))
|
||||
.map(|ws: Ws, view: liveview::Liveview| {
|
||||
ws.on_upgrade(|socket| async move {
|
||||
view.upgrade(socket, app).await;
|
||||
})
|
||||
}));
|
||||
|
||||
warp::serve(routes).run(addr).await;
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
use crate::events;
|
||||
use dioxus_core::prelude::*;
|
||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
use warp::ws::{Message, WebSocket};
|
||||
|
||||
impl crate::Liveview {
|
||||
pub async fn upgrade(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) {
|
||||
connect(ws, self.pool.clone(), app).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(ws: WebSocket, pool: LocalPoolHandle, app: fn(Scope) -> Element) {
|
||||
// Use a counter to assign a new unique ID for this user.
|
||||
|
||||
// Split the socket into a sender and receive of messages.
|
||||
let (mut user_ws_tx, mut user_ws_rx) = ws.split();
|
||||
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
let (edits_tx, edits_rx) = mpsc::unbounded_channel();
|
||||
|
||||
let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
|
||||
let mut event_rx = UnboundedReceiverStream::new(event_rx);
|
||||
|
||||
let vdom_fut = pool.spawn_pinned(move || async move {
|
||||
let mut vdom = VirtualDom::new(app);
|
||||
|
||||
let edits = vdom.rebuild();
|
||||
|
||||
let serialized = serde_json::to_string(&edits.edits).unwrap();
|
||||
edits_tx.send(serialized).unwrap();
|
||||
|
||||
loop {
|
||||
use futures_util::future::{select, Either};
|
||||
|
||||
let new_event = {
|
||||
let vdom_fut = vdom.wait_for_work();
|
||||
|
||||
pin_mut!(vdom_fut);
|
||||
|
||||
match select(event_rx.next(), vdom_fut).await {
|
||||
Either::Left((l, _)) => l,
|
||||
Either::Right((_, _)) => None,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(new_event) = new_event {
|
||||
vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event));
|
||||
} else {
|
||||
let mutations = vdom.work_with_deadline(|| false);
|
||||
for mutation in mutations {
|
||||
let edits = serde_json::to_string(&mutation.edits).unwrap();
|
||||
edits_tx.send(edits).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
use futures_util::future::{select, Either};
|
||||
|
||||
match select(user_ws_rx.next(), edits_rx.next()).await {
|
||||
Either::Left((l, _)) => {
|
||||
if let Some(Ok(msg)) = l {
|
||||
if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) {
|
||||
if msg.method == "user_event" {
|
||||
let user_event = events::trigger_from_serialized(msg.params);
|
||||
event_tx.send(user_event).unwrap();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Either::Right((edits, _)) => {
|
||||
if let Some(edits) = edits {
|
||||
// send the edits to the client
|
||||
if user_ws_tx.send(Message::text(edits)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vdom_fut.abort();
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
//! 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::on::*;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct IpcMessage {
|
||||
pub method: String,
|
||||
pub params: serde_json::Value,
|
||||
}
|
||||
|
||||
pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
|
||||
match serde_json::from_str(payload) {
|
||||
Ok(message) => Some(message),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct ImEvent {
|
||||
event: String,
|
||||
mounted_dom_id: u64,
|
||||
contents: serde_json::Value,
|
||||
}
|
||||
|
||||
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
||||
let ImEvent {
|
||||
event,
|
||||
mounted_dom_id,
|
||||
contents,
|
||||
} = serde_json::from_value(val).unwrap();
|
||||
|
||||
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,
|
||||
data: event,
|
||||
}
|
||||
}
|
||||
|
||||
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 {})
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
"click" | "contextmenu" | "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",
|
||||
"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",
|
||||
_ => {
|
||||
panic!("unsupported event type")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Dioxus app</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script>
|
||||
import("./index.js").then(function (module) {
|
||||
module.main();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,612 @@
|
||||
function main() {
|
||||
let root = window.document.getElementById("main");
|
||||
|
||||
if (root != null) {
|
||||
// create a new ipc
|
||||
window.ipc = new IPC(root);
|
||||
|
||||
window.ipc.send(serializeIpcMessage("initialize"));
|
||||
}
|
||||
}
|
||||
|
||||
class IPC {
|
||||
constructor(root) {
|
||||
// connect to the websocket
|
||||
window.interpreter = new Interpreter(root);
|
||||
|
||||
this.ws = new WebSocket(WS_ADDR);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log("Connected to the websocket");
|
||||
};
|
||||
|
||||
this.ws.onerror = (err) => {
|
||||
console.error("Error: ", err);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
let edits = JSON.parse(event.data);
|
||||
window.interpreter.handleEdits(edits);
|
||||
};
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
this.ws.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
class Interpreter {
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
this.stack = [root];
|
||||
this.listeners = {};
|
||||
this.handlers = {};
|
||||
this.lastNodeWasText = false;
|
||||
this.nodes = [root];
|
||||
}
|
||||
top() {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
pop() {
|
||||
return this.stack.pop();
|
||||
}
|
||||
PushRoot(root) {
|
||||
const node = this.nodes[root];
|
||||
this.stack.push(node);
|
||||
}
|
||||
AppendChildren(many) {
|
||||
let root = this.stack[this.stack.length - (1 + many)];
|
||||
let to_add = this.stack.splice(this.stack.length - many);
|
||||
for (let i = 0; i < many; i++) {
|
||||
root.appendChild(to_add[i]);
|
||||
}
|
||||
}
|
||||
ReplaceWith(root_id, m) {
|
||||
let root = this.nodes[root_id];
|
||||
let els = this.stack.splice(this.stack.length - m);
|
||||
root.replaceWith(...els);
|
||||
}
|
||||
InsertAfter(root, n) {
|
||||
let old = this.nodes[root];
|
||||
let new_nodes = this.stack.splice(this.stack.length - n);
|
||||
old.after(...new_nodes);
|
||||
}
|
||||
InsertBefore(root, n) {
|
||||
let old = this.nodes[root];
|
||||
let new_nodes = this.stack.splice(this.stack.length - n);
|
||||
old.before(...new_nodes);
|
||||
}
|
||||
Remove(root) {
|
||||
let node = this.nodes[root];
|
||||
if (node !== undefined) {
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
CreateTextNode(text, root) {
|
||||
// todo: make it so the types are okay
|
||||
const node = document.createTextNode(text);
|
||||
this.nodes[root] = node;
|
||||
this.stack.push(node);
|
||||
}
|
||||
CreateElement(tag, root) {
|
||||
const el = document.createElement(tag);
|
||||
// el.setAttribute("data-dioxus-id", `${root}`);
|
||||
this.nodes[root] = el;
|
||||
this.stack.push(el);
|
||||
}
|
||||
CreateElementNs(tag, root, ns) {
|
||||
let el = document.createElementNS(ns, tag);
|
||||
this.stack.push(el);
|
||||
this.nodes[root] = el;
|
||||
}
|
||||
CreatePlaceholder(root) {
|
||||
let el = document.createElement("pre");
|
||||
el.hidden = true;
|
||||
this.stack.push(el);
|
||||
this.nodes[root] = el;
|
||||
}
|
||||
NewEventListener(event_name, root, handler) {
|
||||
const element = this.nodes[root];
|
||||
element.setAttribute("data-dioxus-id", `${root}`);
|
||||
if (this.listeners[event_name] === undefined) {
|
||||
this.listeners[event_name] = 0;
|
||||
this.handlers[event_name] = handler;
|
||||
this.root.addEventListener(event_name, handler);
|
||||
} else {
|
||||
this.listeners[event_name]++;
|
||||
}
|
||||
}
|
||||
RemoveEventListener(root, event_name) {
|
||||
const element = this.nodes[root];
|
||||
element.removeAttribute(`data-dioxus-id`);
|
||||
this.listeners[event_name]--;
|
||||
if (this.listeners[event_name] === 0) {
|
||||
this.root.removeEventListener(event_name, this.handlers[event_name]);
|
||||
delete this.listeners[event_name];
|
||||
delete this.handlers[event_name];
|
||||
}
|
||||
}
|
||||
SetText(root, text) {
|
||||
this.nodes[root].textContent = text;
|
||||
}
|
||||
SetAttribute(root, field, value, ns) {
|
||||
const name = field;
|
||||
const node = this.nodes[root];
|
||||
if (ns === "style") {
|
||||
// @ts-ignore
|
||||
node.style[name] = value;
|
||||
} else if (ns != null || ns != undefined) {
|
||||
node.setAttributeNS(ns, name, value);
|
||||
} else {
|
||||
switch (name) {
|
||||
case "value":
|
||||
if (value !== node.value) {
|
||||
node.value = value;
|
||||
}
|
||||
break;
|
||||
case "checked":
|
||||
node.checked = value === "true";
|
||||
break;
|
||||
case "selected":
|
||||
node.selected = value === "true";
|
||||
break;
|
||||
case "dangerous_inner_html":
|
||||
node.innerHTML = value;
|
||||
break;
|
||||
default:
|
||||
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
|
||||
if (value === "false" && bool_attrs.hasOwnProperty(name)) {
|
||||
node.removeAttribute(name);
|
||||
} else {
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoveAttribute(root, name) {
|
||||
const node = this.nodes[root];
|
||||
|
||||
if (name === "value") {
|
||||
node.value = "";
|
||||
} else if (name === "checked") {
|
||||
node.checked = false;
|
||||
} else if (name === "selected") {
|
||||
node.selected = false;
|
||||
} else if (name === "dangerous_inner_html") {
|
||||
node.innerHTML = "";
|
||||
} else {
|
||||
node.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
handleEdits(edits) {
|
||||
this.stack.push(this.root);
|
||||
for (let edit of edits) {
|
||||
this.handleEdit(edit);
|
||||
}
|
||||
}
|
||||
handleEdit(edit) {
|
||||
switch (edit.type) {
|
||||
case "PushRoot":
|
||||
this.PushRoot(edit.root);
|
||||
break;
|
||||
case "AppendChildren":
|
||||
this.AppendChildren(edit.many);
|
||||
break;
|
||||
case "ReplaceWith":
|
||||
this.ReplaceWith(edit.root, edit.m);
|
||||
break;
|
||||
case "InsertAfter":
|
||||
this.InsertAfter(edit.root, edit.n);
|
||||
break;
|
||||
case "InsertBefore":
|
||||
this.InsertBefore(edit.root, edit.n);
|
||||
break;
|
||||
case "Remove":
|
||||
this.Remove(edit.root);
|
||||
break;
|
||||
case "CreateTextNode":
|
||||
this.CreateTextNode(edit.text, edit.root);
|
||||
break;
|
||||
case "CreateElement":
|
||||
this.CreateElement(edit.tag, edit.root);
|
||||
break;
|
||||
case "CreateElementNs":
|
||||
this.CreateElementNs(edit.tag, edit.root, edit.ns);
|
||||
break;
|
||||
case "CreatePlaceholder":
|
||||
this.CreatePlaceholder(edit.root);
|
||||
break;
|
||||
case "RemoveEventListener":
|
||||
this.RemoveEventListener(edit.root, edit.event_name);
|
||||
break;
|
||||
case "NewEventListener":
|
||||
// this handler is only provided on desktop implementations since this
|
||||
// method is not used by the web implementation
|
||||
let handler = (event) => {
|
||||
let target = event.target;
|
||||
if (target != null) {
|
||||
let realId = target.getAttribute(`data-dioxus-id`);
|
||||
let shouldPreventDefault = target.getAttribute(
|
||||
`dioxus-prevent-default`
|
||||
);
|
||||
|
||||
if (event.type === "click") {
|
||||
// todo call prevent default if it's the right type of event
|
||||
if (shouldPreventDefault !== `onclick`) {
|
||||
if (target.tagName === "A") {
|
||||
event.preventDefault();
|
||||
const href = target.getAttribute("href");
|
||||
if (href !== "" && href !== null && href !== undefined) {
|
||||
window.ipc.send(
|
||||
serializeIpcMessage("browser_open", { href })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// also prevent buttons from submitting
|
||||
if (target.tagName === "BUTTON") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
// walk the tree to find the real element
|
||||
while (realId == null) {
|
||||
// we've reached the root we don't want to send an event
|
||||
if (target.parentElement === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
target = target.parentElement;
|
||||
realId = target.getAttribute(`data-dioxus-id`);
|
||||
}
|
||||
|
||||
shouldPreventDefault = target.getAttribute(
|
||||
`dioxus-prevent-default`
|
||||
);
|
||||
|
||||
let contents = serialize_event(event);
|
||||
|
||||
if (shouldPreventDefault === `on${event.type}`) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (event.type === "submit") {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (target.tagName === "FORM") {
|
||||
for (let x = 0; x < target.elements.length; x++) {
|
||||
let element = target.elements[x];
|
||||
let name = element.getAttribute("name");
|
||||
if (name != null) {
|
||||
if (element.getAttribute("type") === "checkbox") {
|
||||
// @ts-ignore
|
||||
contents.values[name] = element.checked ? "true" : "false";
|
||||
} else {
|
||||
// @ts-ignore
|
||||
contents.values[name] =
|
||||
element.value ?? element.textContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (realId == null) {
|
||||
return;
|
||||
}
|
||||
window.ipc.send(
|
||||
serializeIpcMessage("user_event", {
|
||||
event: edit.event_name,
|
||||
mounted_dom_id: parseInt(realId),
|
||||
contents: contents,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
this.NewEventListener(edit.event_name, edit.root, handler);
|
||||
break;
|
||||
case "SetText":
|
||||
this.SetText(edit.root, edit.text);
|
||||
break;
|
||||
case "SetAttribute":
|
||||
this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
|
||||
break;
|
||||
case "RemoveAttribute":
|
||||
this.RemoveAttribute(edit.root, edit.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function serialize_event(event) {
|
||||
switch (event.type) {
|
||||
case "copy":
|
||||
case "cut":
|
||||
case "past": {
|
||||
return {};
|
||||
}
|
||||
case "compositionend":
|
||||
case "compositionstart":
|
||||
case "compositionupdate": {
|
||||
let { data } = event;
|
||||
return {
|
||||
data,
|
||||
};
|
||||
}
|
||||
case "keydown":
|
||||
case "keypress":
|
||||
case "keyup": {
|
||||
let {
|
||||
charCode,
|
||||
key,
|
||||
altKey,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
keyCode,
|
||||
shiftKey,
|
||||
location,
|
||||
repeat,
|
||||
which,
|
||||
} = event;
|
||||
return {
|
||||
char_code: charCode,
|
||||
key: key,
|
||||
alt_key: altKey,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
key_code: keyCode,
|
||||
shift_key: shiftKey,
|
||||
location: location,
|
||||
repeat: repeat,
|
||||
which: which,
|
||||
locale: "locale",
|
||||
};
|
||||
}
|
||||
case "focus":
|
||||
case "blur": {
|
||||
return {};
|
||||
}
|
||||
case "change": {
|
||||
let target = event.target;
|
||||
let value;
|
||||
if (target.type === "checkbox" || target.type === "radio") {
|
||||
value = target.checked ? "true" : "false";
|
||||
} else {
|
||||
value = target.value ?? target.textContent;
|
||||
}
|
||||
return {
|
||||
value: value,
|
||||
values: {},
|
||||
};
|
||||
}
|
||||
case "input":
|
||||
case "invalid":
|
||||
case "reset":
|
||||
case "submit": {
|
||||
let target = event.target;
|
||||
let value = target.value ?? target.textContent;
|
||||
if (target.type === "checkbox") {
|
||||
value = target.checked ? "true" : "false";
|
||||
}
|
||||
return {
|
||||
value: value,
|
||||
values: {},
|
||||
};
|
||||
}
|
||||
case "click":
|
||||
case "contextmenu":
|
||||
case "doubleclick":
|
||||
case "drag":
|
||||
case "dragend":
|
||||
case "dragenter":
|
||||
case "dragexit":
|
||||
case "dragleave":
|
||||
case "dragover":
|
||||
case "dragstart":
|
||||
case "drop":
|
||||
case "mousedown":
|
||||
case "mouseenter":
|
||||
case "mouseleave":
|
||||
case "mousemove":
|
||||
case "mouseout":
|
||||
case "mouseover":
|
||||
case "mouseup": {
|
||||
const {
|
||||
altKey,
|
||||
button,
|
||||
buttons,
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
pageX,
|
||||
pageY,
|
||||
screenX,
|
||||
screenY,
|
||||
shiftKey,
|
||||
} = event;
|
||||
return {
|
||||
alt_key: altKey,
|
||||
button: button,
|
||||
buttons: buttons,
|
||||
client_x: clientX,
|
||||
client_y: clientY,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
page_x: pageX,
|
||||
page_y: pageY,
|
||||
screen_x: screenX,
|
||||
screen_y: screenY,
|
||||
shift_key: shiftKey,
|
||||
};
|
||||
}
|
||||
case "pointerdown":
|
||||
case "pointermove":
|
||||
case "pointerup":
|
||||
case "pointercancel":
|
||||
case "gotpointercapture":
|
||||
case "lostpointercapture":
|
||||
case "pointerenter":
|
||||
case "pointerleave":
|
||||
case "pointerover":
|
||||
case "pointerout": {
|
||||
const {
|
||||
altKey,
|
||||
button,
|
||||
buttons,
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
pageX,
|
||||
pageY,
|
||||
screenX,
|
||||
screenY,
|
||||
shiftKey,
|
||||
pointerId,
|
||||
width,
|
||||
height,
|
||||
pressure,
|
||||
tangentialPressure,
|
||||
tiltX,
|
||||
tiltY,
|
||||
twist,
|
||||
pointerType,
|
||||
isPrimary,
|
||||
} = event;
|
||||
return {
|
||||
alt_key: altKey,
|
||||
button: button,
|
||||
buttons: buttons,
|
||||
client_x: clientX,
|
||||
client_y: clientY,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
page_x: pageX,
|
||||
page_y: pageY,
|
||||
screen_x: screenX,
|
||||
screen_y: screenY,
|
||||
shift_key: shiftKey,
|
||||
pointer_id: pointerId,
|
||||
width: width,
|
||||
height: height,
|
||||
pressure: pressure,
|
||||
tangential_pressure: tangentialPressure,
|
||||
tilt_x: tiltX,
|
||||
tilt_y: tiltY,
|
||||
twist: twist,
|
||||
pointer_type: pointerType,
|
||||
is_primary: isPrimary,
|
||||
};
|
||||
}
|
||||
case "select": {
|
||||
return {};
|
||||
}
|
||||
case "touchcancel":
|
||||
case "touchend":
|
||||
case "touchmove":
|
||||
case "touchstart": {
|
||||
const { altKey, ctrlKey, metaKey, shiftKey } = event;
|
||||
return {
|
||||
// changed_touches: event.changedTouches,
|
||||
// target_touches: event.targetTouches,
|
||||
// touches: event.touches,
|
||||
alt_key: altKey,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
shift_key: shiftKey,
|
||||
};
|
||||
}
|
||||
case "scroll": {
|
||||
return {};
|
||||
}
|
||||
case "wheel": {
|
||||
const { deltaX, deltaY, deltaZ, deltaMode } = event;
|
||||
return {
|
||||
delta_x: deltaX,
|
||||
delta_y: deltaY,
|
||||
delta_z: deltaZ,
|
||||
delta_mode: deltaMode,
|
||||
};
|
||||
}
|
||||
case "animationstart":
|
||||
case "animationend":
|
||||
case "animationiteration": {
|
||||
const { animationName, elapsedTime, pseudoElement } = event;
|
||||
return {
|
||||
animation_name: animationName,
|
||||
elapsed_time: elapsedTime,
|
||||
pseudo_element: pseudoElement,
|
||||
};
|
||||
}
|
||||
case "transitionend": {
|
||||
const { propertyName, elapsedTime, pseudoElement } = event;
|
||||
return {
|
||||
property_name: propertyName,
|
||||
elapsed_time: elapsedTime,
|
||||
pseudo_element: pseudoElement,
|
||||
};
|
||||
}
|
||||
case "abort":
|
||||
case "canplay":
|
||||
case "canplaythrough":
|
||||
case "durationchange":
|
||||
case "emptied":
|
||||
case "encrypted":
|
||||
case "ended":
|
||||
case "error":
|
||||
case "loadeddata":
|
||||
case "loadedmetadata":
|
||||
case "loadstart":
|
||||
case "pause":
|
||||
case "play":
|
||||
case "playing":
|
||||
case "progress":
|
||||
case "ratechange":
|
||||
case "seeked":
|
||||
case "seeking":
|
||||
case "stalled":
|
||||
case "suspend":
|
||||
case "timeupdate":
|
||||
case "volumechange":
|
||||
case "waiting": {
|
||||
return {};
|
||||
}
|
||||
case "toggle": {
|
||||
return {};
|
||||
}
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
function serializeIpcMessage(method, params = {}) {
|
||||
return JSON.stringify({ method, params });
|
||||
}
|
||||
const bool_attrs = {
|
||||
allowfullscreen: true,
|
||||
allowpaymentrequest: true,
|
||||
async: true,
|
||||
autofocus: true,
|
||||
autoplay: true,
|
||||
checked: true,
|
||||
controls: true,
|
||||
default: true,
|
||||
defer: true,
|
||||
disabled: true,
|
||||
formnovalidate: true,
|
||||
hidden: true,
|
||||
ismap: true,
|
||||
itemscope: true,
|
||||
loop: true,
|
||||
multiple: true,
|
||||
muted: true,
|
||||
nomodule: true,
|
||||
novalidate: true,
|
||||
open: true,
|
||||
playsinline: true,
|
||||
readonly: true,
|
||||
required: true,
|
||||
reversed: true,
|
||||
selected: true,
|
||||
truespeed: true,
|
||||
};
|
||||
@@ -1 +1,64 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub(crate) mod events;
|
||||
pub mod adapters {
|
||||
#[cfg(feature = "warp")]
|
||||
pub mod warp_adapter;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum_adapter;
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
pub mod actix_adapter;
|
||||
}
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
pub use adapters::warp_adapter::connect;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
pub use adapters::axum_adapter::connect;
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
pub use adapters::actix_adapter::connect;
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Liveview {
|
||||
pool: LocalPoolHandle,
|
||||
addr: String,
|
||||
}
|
||||
|
||||
impl Liveview {
|
||||
pub fn body(&self, header: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{header}
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script>
|
||||
var WS_ADDR = "ws://{addr}/app";
|
||||
{interpreter}
|
||||
main();
|
||||
</script>
|
||||
</body>
|
||||
</html>"#,
|
||||
addr = self.addr,
|
||||
interpreter = include_str!("../src/interpreter.js")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(addr: impl Into<SocketAddr>) -> Liveview {
|
||||
let addr: SocketAddr = addr.into();
|
||||
|
||||
Liveview {
|
||||
pool: LocalPoolHandle::new(16),
|
||||
addr: addr.to_string(),
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-mobile"
|
||||
version = "0.0.3"
|
||||
version = "0.2.0"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Mobile-compatible renderer for Dioxus"
|
||||
@@ -12,7 +12,7 @@ license = "MIT/Apache-2.0"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-desktop = { path = "../desktop", version = "^0.1.6" }
|
||||
dioxus-desktop = { path = "../desktop", version = "^0.2.0" }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dist/
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"rust-analyzer.cargo.allFeatures": true
|
||||
}
|
||||
+23
-19
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "dioxus-router"
|
||||
version = "0.1.1"
|
||||
version = "0.2.2"
|
||||
edition = "2018"
|
||||
description = "Dioxus VirtualDOM renderer for the web browser using websys"
|
||||
description = "Cross-platform router for Dioxus apps"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
@@ -11,13 +11,11 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.1.9", default-features = false }
|
||||
dioxus-html = { path = "../html", version = "^0.1.6", default-features = false }
|
||||
dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" }
|
||||
|
||||
serde = "1"
|
||||
url = "2.2.2"
|
||||
serde_urlencoded = "0.7"
|
||||
dioxus-core = { path = "../core", version = "^0.2.0", default-features = false }
|
||||
dioxus-html = { path = "../html", version = "^0.2.0", default-features = false }
|
||||
dioxus-core-macro = { path = "../core-macro", version = "^0.2.0" }
|
||||
futures-channel = "0.3.21"
|
||||
url = { version = "2.2.2", default-features = false }
|
||||
|
||||
# for wasm
|
||||
web-sys = { version = "0.3", features = [
|
||||
@@ -30,19 +28,21 @@ web-sys = { version = "0.3", features = [
|
||||
"Url",
|
||||
"UrlSearchParams",
|
||||
"Window",
|
||||
"Location"
|
||||
], optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
js-sys = { version = "0.3", optional = true }
|
||||
gloo = { version = "0.5", optional = true }
|
||||
gloo-events = { version = "0.1.1", optional = true }
|
||||
log = "0.4.14"
|
||||
|
||||
thiserror = "1.0.30"
|
||||
futures-util = "0.3.21"
|
||||
serde = { version = "1", optional = true }
|
||||
serde_urlencoded = { version = "0.7.1", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["derive", "web"]
|
||||
web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"]
|
||||
desktop = []
|
||||
mobile = []
|
||||
derive = []
|
||||
default = ["query"]
|
||||
web = ["web-sys", "gloo-events", "js-sys", "wasm-bindgen"]
|
||||
query = ["serde", "serde_urlencoded"]
|
||||
wasm_test = []
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -52,9 +52,13 @@ log = "0.4.14"
|
||||
wasm-logger = "0.2.0"
|
||||
wasm-bindgen-test = "0.3"
|
||||
gloo-utils = "0.1.2"
|
||||
dioxus-ssr = { path = "../ssr" }
|
||||
|
||||
dioxus-router = { path = ".", default-features = false }
|
||||
|
||||
[target.wasm32-unknown-unknown.dev-dependencies]
|
||||
dioxus-router = { path = ".", features = ["web"] }
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Document",
|
||||
]
|
||||
features = ["Document"]
|
||||
Loaded 100 of 281 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user