= 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
}
diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs
index 7099311ce..931933386 100644
--- a/packages/core-macro/src/lib.rs
+++ b/packages/core-macro/src/lib.rs
@@ -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`,
diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs
index d569e711a..6c65c5944 100644
--- a/packages/core-macro/src/props/mod.rs
+++ b/packages/core-macro/src/props/mod.rs
@@ -43,27 +43,21 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result {
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<...>`",
diff --git a/packages/core-macro/src/router.rs b/packages/core-macro/src/router.rs
deleted file mode 100644
index 3edad75e3..000000000
--- a/packages/core-macro/src/router.rs
+++ /dev/null
@@ -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,
- variants: Punctuated,
- not_found_route: Option,
-}
-
-impl Parse for Routable {
- fn parse(input: ParseStream) -> syn::Result {
- 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,
-) -> syn::Result<(Option, Vec)> {
- let mut not_founds = vec![];
- let mut ats: Vec = 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::>();
-
- 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::()?;
- 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 {
- // 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::>();
-
- // 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 {
- // 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
- // }
- // }
- }
-}
diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs
index 4e9d449cf..dfdda74b8 100644
--- a/packages/core-macro/src/rsx/component.rs
+++ b/packages/core-macro/src/rsx/component.rs
@@ -23,10 +23,10 @@ use syn::{
};
pub struct Component {
- name: syn::Path,
- body: Vec,
- children: Vec,
- manual_props: Option,
+ pub name: syn::Path,
+ pub body: Vec,
+ pub children: Vec,
+ pub manual_props: Option,
}
impl Parse for Component {
@@ -199,8 +199,7 @@ impl Parse for ComponentField {
}
if input.peek(LitStr) && input.peek2(LitStr) {
- let item = input.parse::().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()?);
diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs
index ebc401b6e..96c7a2efc 100644
--- a/packages/core-macro/src/rsx/element.rs
+++ b/packages/core-macro/src/rsx/element.rs
@@ -11,12 +11,11 @@ use syn::{
// Parse the VNode::Element type
// =======================================
pub struct Element {
- name: Ident,
- key: Option,
- attributes: Vec,
- listeners: Vec,
- children: Vec,
- _is_static: bool,
+ pub name: Ident,
+ pub key: Option,
+ pub attributes: Vec,
+ pub children: Vec,
+ 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 = vec![];
- let mut listeners: Vec = vec![];
let mut children: Vec = vec![];
let mut key = None;
let mut _el_ref = None;
@@ -54,6 +52,7 @@ impl Parse for Element {
});
} else {
let value = content.parse::()?;
+
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::().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::()?;
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::().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::().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::().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::()?);
@@ -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 {
diff --git a/packages/core-macro/src/rsx/errors.rs b/packages/core-macro/src/rsx/errors.rs
new file mode 100644
index 000000000..27e2c7f98
--- /dev/null
+++ b/packages/core-macro/src/rsx/errors.rs
@@ -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"
+ )
+ };
+}
diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/core-macro/src/rsx/mod.rs
index 5ff5d85f7..6b9dfc3dc 100644
--- a/packages/core-macro/src/rsx/mod.rs
+++ b/packages/core-macro/src/rsx/mod.rs
@@ -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,
- roots: Vec,
+ pub custom_context: Option,
+ pub roots: Vec,
}
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
})
diff --git a/packages/core-macro/src/rsx/pretty.rs b/packages/core-macro/src/rsx/pretty.rs
new file mode 100644
index 000000000..26ad54006
--- /dev/null
+++ b/packages/core-macro/src/rsx/pretty.rs
@@ -0,0 +1 @@
+//! pretty printer for rsx!
diff --git a/packages/core-macro/src/rsxtemplate.rs b/packages/core-macro/src/rsxtemplate.rs
deleted file mode 100644
index 9222d7117..000000000
--- a/packages/core-macro/src/rsxtemplate.rs
+++ /dev/null
@@ -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 {
- if s.peek(LitStr) {
- use std::str::FromStr;
-
- let lit = s.parse::()?;
- 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::() {
- Ok(r) => Ok(Self { inner: r }),
- Err(e) => Err(e),
- }
- } else {
- panic!("Not a str lit")
- }
- // let t = s.parse::()?;
-
- // let new_stream = TokenStream::from(t.to_s)
-
- // let cx: Ident = s.parse()?;
- // s.parse::()?;
- // 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> = vec![nodes_toks.parse()?];
- // while nodes_toks.peek(Token![,]) {
- // nodes_toks.parse::()?;
- // 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);
- }
-}
diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml
index 03b378e88..480c3904d 100644
--- a/packages/core/Cargo.toml
+++ b/packages/core/Cargo.toml
@@ -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
diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs
index d7189a710..3d8b9082d 100644
--- a/packages/core/src/diff.rs
+++ b/packages/core/src/diff.rs
@@ -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)
}
}
}
diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs
index 8386cedd8..1a9899e4a 100644
--- a/packages/core/src/events.rs
+++ b/packages/core/src/events.rs
@@ -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,
pub(crate) data: Arc,
}
impl AnyEvent {
+ /// Convert this AnyEvent into a specific UiEvent with EventData.
+ ///
+ /// ```rust, ignore
+ /// let evt: FormEvent = evvt.downcast().unwrap();
+ /// ```
pub fn downcast(self) -> Option> {
let AnyEvent { data, bubble_state } = self;
- if let Ok(data) = data.downcast::() {
- Some(UiEvent { bubble_state, data })
- } else {
- None
- }
+ data.downcast::()
+ .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 generic type.
+///
+/// For the HTML crate, this would include [`MouseEvent`], [`FormEvent`] etc.
pub struct UiEvent {
+ /// The internal data of the event
+ /// This is wrapped in an Arc so that it can be sent across threads
pub data: Arc,
#[allow(unused)]
diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs
index 1340c916c..db4460597 100644
--- a/packages/core/src/lazynodes.rs
+++ b/packages/core/src/lazynodes.rs
@@ -35,14 +35,32 @@ enum StackNodeStorage<'a, 'b> {
}
impl<'a, 'b> LazyNodes<'a, 'b> {
- pub fn new_some(_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>| {
+ 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(_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>| {
- 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(val: F) -> Self
where
F: FnMut(Option>) -> Option> + '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::>();
- LazyNodes::new_some(|f| {
- log::debug!("main closure");
+ LazyNodes::new(|f| {
+ eprintln!("main closure");
f.fragment_from_iter(it)
})
};
diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs
index e81ddca87..f31e2e328 100644
--- a/packages/core/src/lib.rs
+++ b/packages/core/src/lib.rs
@@ -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();
+ )*}
+}
diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs
index bd94d0b71..2d3115ce3 100644
--- a/packages/core/src/mutations.rs
+++ b/packages/core/src/mutations.rs
@@ -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>,
+
+ /// The list of Scopes that were diffed, created, and removed during the Diff process.
pub dirty_scopes: FxHashSet,
+
+ /// The list of nodes to connect to the RealDOM.
pub refs: Vec>,
}
@@ -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) {
diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs
index 7a36f58ce..0d3359a8c 100644
--- a/packages/core/src/nodes.rs
+++ b/packages/core/src/nodes.rs
@@ -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> {
/// A placeholder node only generated when Fragments don't have any children.
pub struct VPlaceholder {
+ /// The [`ElementId`] of the placeholder.
pub id: Cell >,
}
/// A bump-allocated string slice and metadata.
pub struct VText<'src> {
- pub text: &'src str,
+ /// The [`ElementId`] of the VText.
pub id: Cell >,
+
+ /// 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 >,
+
+ /// 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 >,
+
+ /// 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 >>,
}
@@ -381,12 +436,23 @@ impl 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>,
+
+ /// 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 >>,
}
@@ -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(
&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- + '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
- + '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
- >,
@@ -679,6 +752,7 @@ impl<'a> NodeFactory<'a> {
}
}
+ /// Create a new [`EventHandler`] from an [`FnMut`]
pub fn event_handler
(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>;
}
diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs
index f42bc36ba..e81fc52a2 100644
--- a/packages/core/src/properties.rs
+++ b/packages/core/src/properties.rs
@@ -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
diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs
index 00c3024bf..010c63b03 100644
--- a/packages/core/src/scopes.rs
+++ b/packages/core/src/scopes.rs
@@ -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,
// shared state -> todo: move this out of scopestate
- pub(crate) shared_contexts: RefCell>>,
+ pub(crate) shared_contexts: RefCell>>,
pub(crate) tasks: Rc,
}
@@ -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 {
+ 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 {
+ pub fn schedule_update(&self) -> Arc {
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 {
+ pub fn schedule_update_any(&self) -> Arc {
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(&self, value: T) -> Rc {
- let value = Rc::new(value);
+ pub fn provide_context(&self, value: T) -> T {
self.shared_contexts
.borrow_mut()
- .insert(TypeId::of::(), value.clone())
- .map(|f| f.downcast::().ok())
- .flatten();
+ .insert(TypeId::of::(), Box::new(value.clone()))
+ .and_then(|f| f.downcast::().ok());
value
}
@@ -676,16 +702,13 @@ impl ScopeState {
/// rsx!(cx, div { "hello {state.0}" })
/// }
/// ```
- pub fn provide_root_context(&self, value: T) -> Rc {
- let value = Rc::new(value);
-
+ pub fn provide_root_context(&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::(), value.clone())
- .map(|f| f.downcast::().ok())
- .flatten();
+ .insert(TypeId::of::(), Box::new(value.clone()))
+ .and_then(|f| f.downcast::().ok());
return value;
}
@@ -698,7 +721,7 @@ impl ScopeState {
let exists = parent
.shared_contexts
.borrow_mut()
- .insert(TypeId::of::(), value.clone());
+ .insert(TypeId::of::(), 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(&self) -> Option> {
+ pub fn consume_context(&self) -> Option {
if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::()) {
- Some(shared.clone().downcast::().unwrap())
+ Some((*shared.downcast_ref::().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::()) {
- return Some(shared.clone().downcast::().unwrap());
+ return Some(shared.downcast_ref::().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 + '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>,
+ pub(crate) task_map: RefCell>>,
gen: Cell,
sender: UnboundedSender,
}
+
pub(crate) type InnerTask = Pin>>;
impl TaskQueue {
- fn new(sender: UnboundedSender) -> Rc {
- Rc::new(Self {
- tasks: RefCell::new(FxHashMap::default()),
- gen: Cell::new(0),
- sender,
- })
- }
- fn push_fut(&self, task: impl Future + 'static) -> TaskId {
+ fn spawn(&self, scope: ScopeId, task: impl Future + '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()
}
diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs
index b9640792d..a6a524e27 100644
--- a/packages/core/src/util.rs
+++ b/packages/core/src/util.rs
@@ -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,
diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs
index a7092104c..8d3fd1789 100644
--- a/packages/core/src/virtual_dom.rs
+++ b/packages/core/src/virtual_dom.rs
@@ -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) {
diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml
index fa1bc88dc..a1dc1adc8 100644
--- a/packages/desktop/Cargo.toml
+++ b/packages/desktop/Cargo.toml
@@ -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
diff --git a/packages/desktop/src/assets/default_icon.bin b/packages/desktop/src/assets/default_icon.bin
new file mode 100644
index 000000000..2d4898f0c
Binary files /dev/null and b/packages/desktop/src/assets/default_icon.bin differ
diff --git a/packages/desktop/src/assets/default_icon.png b/packages/desktop/src/assets/default_icon.png
new file mode 100644
index 000000000..d0742ea8a
Binary files /dev/null and b/packages/desktop/src/assets/default_icon.png differ
diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs
index 5baa46f5d..84ac3a72a 100644
--- a/packages/desktop/src/cfg.rs
+++ b/packages/desktop/src/cfg.rs
@@ -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 bool>>,
- pub protocos: Vec,
+ pub(crate) window: WindowBuilder,
+ pub(crate) file_drop_handler: Option bool>>,
+ pub(crate) protocols: Vec,
pub(crate) pre_rendered: Option,
pub(crate) event_handler: Option>,
+ pub(crate) disable_context_menu: bool,
+ pub(crate) resource_dir: Option,
+ pub(crate) custom_head: Option,
+ pub(crate) custom_index: Option,
}
-pub type WryProtocl = (
+pub(crate) type WryProtocol = (
String,
Box WryResult + '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) -> 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 + '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())
+// }
diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs
new file mode 100644
index 000000000..2fa1355d2
--- /dev/null
+++ b/packages/desktop/src/controller.rs
@@ -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,
+ pub(super) sender: futures_channel::mpsc::UnboundedSender,
+ pub(super) pending_edits: Arc>>,
+ pub(super) quit_app_on_close: bool,
+ pub(super) is_ready: Arc,
+}
+
+impl DesktopController {
+ // Launch the virtualdom on its own thread managed by tokio
+ // returns the desktop state
+ pub(super) fn new_on_tokio(
+ root: Component,
+ props: P,
+ proxy: EventLoopProxy,
+ ) -> Self {
+ let edit_queue = Arc::new(Mutex::new(Vec::new()));
+ let (sender, receiver) = futures_channel::mpsc::unbounded::();
+
+ 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();
+ }
+ }
+ }
+}
diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs
index d180e1235..155f7af1b 100644
--- a/packages/desktop/src/desktop_context.rs
+++ b/packages/desktop/src/desktop_context.rs
@@ -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