Merge branch 'master' into jk/all-the-css
This commit is contained in:
commit
f69430a308
107 files changed
+3393
-1638
No files matched your search
@@ -15,8 +15,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
once_cell = "1.8"
|
||||
proc-macro-error = "1.0.4"
|
||||
# dioxus-rsx = { path = "../rsx" }
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 },
|
||||
@@ -82,7 +81,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,
|
||||
@@ -182,7 +181,6 @@ impl Parse for Element {
|
||||
name: el_name,
|
||||
attributes,
|
||||
children,
|
||||
listeners,
|
||||
_is_static: false,
|
||||
})
|
||||
}
|
||||
@@ -193,14 +191,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 +218,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 {
|
||||
|
||||
@@ -15,6 +15,8 @@ mod component;
|
||||
mod element;
|
||||
mod node;
|
||||
|
||||
pub mod pretty;
|
||||
|
||||
// Re-export the namespaces into each other
|
||||
pub use component::*;
|
||||
pub use element::*;
|
||||
@@ -29,8 +31,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 +79,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 +87,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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
@@ -853,7 +852,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 +875,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 +898,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 +956,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 +1092,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 +1102,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 +1110,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);
|
||||
@@ -280,7 +286,7 @@ mod tests {
|
||||
.map(|i| {
|
||||
let val = cx.props.inner.clone();
|
||||
|
||||
LazyNodes::new_some(move |f| {
|
||||
LazyNodes::new(move |f| {
|
||||
log::debug!("hell closure");
|
||||
let inner = DropInner { id: i };
|
||||
f.text(format_args!("hello world {:?}, {:?}", inner.id, val))
|
||||
@@ -288,7 +294,7 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
LazyNodes::new_some(|f| {
|
||||
LazyNodes::new(|f| {
|
||||
log::debug!("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,6 +77,9 @@ 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,
|
||||
|
||||
@@ -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,74 +44,146 @@ 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>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -218,8 +295,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) {
|
||||
|
||||
+81
-14
@@ -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),
|
||||
@@ -181,7 +181,6 @@ impl Debug for VNode<'_> {
|
||||
.field("fnptr", &comp.user_fc)
|
||||
.field("key", &comp.key)
|
||||
.field("scope", &comp.scope)
|
||||
.field("originator", &comp.originator)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
@@ -200,6 +199,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 +211,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 +241,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 +304,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 +327,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 +398,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 +428,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 +492,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 +500,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the custom alloactor for this component
|
||||
#[inline]
|
||||
pub fn bump(&self) -> &'a bumpalo::Bump {
|
||||
self.bump
|
||||
@@ -482,6 +542,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VElement`]
|
||||
pub fn element(
|
||||
&self,
|
||||
el: impl DioxusElement,
|
||||
@@ -500,6 +561,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 +593,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a new [`Attribute`]
|
||||
pub fn attr(
|
||||
&self,
|
||||
name: &'static str,
|
||||
@@ -546,6 +611,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VComponent`]
|
||||
pub fn component<P>(
|
||||
&self,
|
||||
component: fn(Scope<'a, P>) -> Element,
|
||||
@@ -561,11 +627,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 +649,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 +658,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 +679,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 +719,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 +744,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 +775,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
|
||||
|
||||
+19
-12
@@ -11,6 +11,7 @@ use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// for traceability, we use the raw fn pointer to identify the function
|
||||
@@ -96,11 +97,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.
|
||||
@@ -396,7 +396,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,
|
||||
}
|
||||
|
||||
@@ -579,12 +582,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 +602,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));
|
||||
})
|
||||
}
|
||||
@@ -653,8 +661,7 @@ impl ScopeState {
|
||||
self.shared_contexts
|
||||
.borrow_mut()
|
||||
.insert(TypeId::of::<T>(), value.clone())
|
||||
.map(|f| f.downcast::<T>().ok())
|
||||
.flatten();
|
||||
.and_then(|f| f.downcast::<T>().ok());
|
||||
value
|
||||
}
|
||||
|
||||
@@ -684,8 +691,7 @@ impl ScopeState {
|
||||
self.shared_contexts
|
||||
.borrow_mut()
|
||||
.insert(TypeId::of::<T>(), value.clone())
|
||||
.map(|f| f.downcast::<T>().ok())
|
||||
.flatten();
|
||||
.and_then(|f| f.downcast::<T>().ok());
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -747,7 +753,8 @@ impl ScopeState {
|
||||
self.push_future(fut);
|
||||
}
|
||||
|
||||
// todo: attach some state to the future to know if we should poll it
|
||||
/// 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);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
+19
-11
@@ -13,31 +13,39 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
|
||||
[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 = [
|
||||
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"
|
||||
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", version = "^0.1.7" }
|
||||
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,16 @@ 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 type WryProtocl = (
|
||||
pub(crate) type WryProtocol = (
|
||||
String,
|
||||
Box<dyn Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static>,
|
||||
);
|
||||
@@ -28,15 +33,31 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 +96,21 @@ impl DesktopConfig {
|
||||
where
|
||||
F: Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static,
|
||||
{
|
||||
self.protocos.push((name, Box::new(handler)));
|
||||
self.protocols.push((name, Box::new(handler)));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_icon(&mut self, icon: Icon) -> &mut Self {
|
||||
self.window.window.window_icon = Some(icon);
|
||||
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 +120,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,106 @@
|
||||
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
||||
use dioxus_core::*;
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::atomic::AtomicBool,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
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<RwLock<VecDeque<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(RwLock::new(VecDeque::new()));
|
||||
let pending_edits = edit_queue.clone();
|
||||
|
||||
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
|
||||
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
|
||||
.write()
|
||||
.unwrap()
|
||||
.push_front(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 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 _ = 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.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,23 @@ use std::rc::Rc;
|
||||
use dioxus_core::ScopeState;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
|
||||
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) -> &Rc<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 +28,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,33 +46,148 @@ 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 minimize(&self, minimized: bool) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized));
|
||||
pub fn set_minimized(&self, minimized: bool) {
|
||||
let _ = self.proxy.send_event(Minimize(minimized));
|
||||
}
|
||||
|
||||
/// set window maximize state
|
||||
pub fn maximize(&self, maximized: bool) {
|
||||
let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized));
|
||||
pub fn set_maximized(&self, maximized: bool) {
|
||||
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(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(Fullscreen(fullscreen));
|
||||
}
|
||||
|
||||
/// set resizable state
|
||||
pub fn set_resizable(&self, resizable: bool) {
|
||||
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(AlwaysOnTop(top));
|
||||
}
|
||||
|
||||
/// set cursor visible or not
|
||||
pub fn set_cursor_visible(&self, visible: bool) {
|
||||
let _ = self.proxy.send_event(CursorVisible(visible));
|
||||
}
|
||||
|
||||
/// set cursor grab
|
||||
pub fn set_cursor_grab(&self, grab: bool) {
|
||||
let _ = self.proxy.send_event(CursorGrab(grab));
|
||||
}
|
||||
|
||||
/// set window title
|
||||
pub fn set_title(&self, title: &str) {
|
||||
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(SetDecorations(decoration));
|
||||
}
|
||||
|
||||
/// opens DevTool window
|
||||
pub fn devtool(&self) {
|
||||
let _ = self.proxy.send_event(DevTool);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
use wry::application::event_loop::ControlFlow;
|
||||
use wry::application::window::Fullscreen as WryFullscreen;
|
||||
|
||||
use crate::controller::DesktopController;
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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",
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
<!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) {
|
||||
<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>
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+73
-281
@@ -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_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,
|
||||
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,29 +123,32 @@ 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 resource_dir = cfg.resource_dir.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) {
|
||||
@@ -204,54 +157,14 @@ 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())
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
@@ -260,10 +173,32 @@ 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) {
|
||||
alert("You've tried to open context menu");
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
} else {
|
||||
document.attachEvent('oncontextmenu', function() {
|
||||
alert("You've tried to open context menu");
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -282,46 +217,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.
|
||||
let _ = window.drag_window();
|
||||
}
|
||||
}
|
||||
UserWindowEvent::CloseWindow => {
|
||||
// close window
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
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::FocusWindow => {
|
||||
for webview in desktop.webviews.values() {
|
||||
let window = webview.window();
|
||||
window.set_focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::UserEvent(user_event) => {
|
||||
desktop_context::handler(user_event, &mut desktop, control_flow)
|
||||
}
|
||||
Event::MainEventsCleared => {}
|
||||
Event::Resumed => {}
|
||||
@@ -332,108 +229,3 @@ pub fn launch_with_props<P: 'static + Send>(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub enum UserWindowEvent {
|
||||
Update,
|
||||
DragWindow,
|
||||
CloseWindow,
|
||||
FocusWindow,
|
||||
Minimize(bool),
|
||||
Maximize(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,82 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use wry::{
|
||||
http::{status::StatusCode, Request, Response, ResponseBuilder},
|
||||
Result,
|
||||
};
|
||||
|
||||
pub(super) fn desktop_handler(request: &Request, asset_root: Option<PathBuf>) -> 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() {
|
||||
ResponseBuilder::new()
|
||||
.mimetype("text/html")
|
||||
.body(include_bytes!("./index.html").to_vec())
|
||||
} 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()));
|
||||
|
||||
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' 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!
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "fermi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core" }
|
||||
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::<AtomRoot>() {
|
||||
Some(root) => root,
|
||||
None => cx.provide_root_context(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::<AtomRoot>() {
|
||||
Some(ctx) => ctx,
|
||||
None => cx.provide_context(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,58 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::*;
|
||||
}
|
||||
|
||||
mod callback;
|
||||
mod root;
|
||||
|
||||
pub use atoms::*;
|
||||
pub use callback::*;
|
||||
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;
|
||||
pub use atom_ref::*;
|
||||
pub use atom_root::*;
|
||||
pub use init_atom_root::*;
|
||||
pub use read::*;
|
||||
pub use set::*;
|
||||
}
|
||||
|
||||
/// 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,103 @@
|
||||
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> {
|
||||
log::trace!("registering atom {:?}", f.unique_id());
|
||||
|
||||
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>(&self, _f: impl Readable<V>) -> &V {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -13,3 +13,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../../packages/core", version = "^0.1.9" }
|
||||
futures-channel = "0.3.21"
|
||||
log = { version = "0.4", features = ["release_max_level_off"] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
dioxus-core = { path = "../../packages/core", version = "^0.1.9" }
|
||||
@@ -1,3 +1,6 @@
|
||||
// #![deny(missing_docs)]
|
||||
//! Useful foundational hooks for Dioxus
|
||||
|
||||
mod usestate;
|
||||
pub use usestate::{use_state, UseState};
|
||||
|
||||
@@ -13,8 +16,8 @@ pub use usecoroutine::*;
|
||||
mod usefuture;
|
||||
pub use usefuture::*;
|
||||
|
||||
mod usesuspense;
|
||||
pub use usesuspense::*;
|
||||
mod useeffect;
|
||||
pub use useeffect::*;
|
||||
|
||||
#[macro_export]
|
||||
/// A helper macro for using hooks in async environements.
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{
|
||||
cell::{Cell, Ref, RefCell, RefMut},
|
||||
collections::HashSet,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
type ProvidedState<T> = RefCell<ProvidedStateInner<T>>;
|
||||
@@ -10,7 +11,7 @@ type ProvidedState<T> = 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>,
|
||||
}
|
||||
|
||||
|
||||
+128
-106
@@ -1,122 +1,144 @@
|
||||
use dioxus_core::{ScopeState, TaskId};
|
||||
pub use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use std::future::Future;
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
/*
|
||||
use std::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, ignore
|
||||
/// 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(),
|
||||
cx.use_hook(|_| {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
let task = cx.push_future(init(rx));
|
||||
cx.provide_context(CoroutineHandle { tx, task })
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a handle to a coroutine higher in the tree
|
||||
///
|
||||
/// See the docs for [`use_coroutine`] for more details.
|
||||
pub fn use_coroutine_handle<M: 'static>(cx: &ScopeState) -> Option<&Rc<CoroutineHandle<M>>> {
|
||||
cx.use_hook(|_| cx.consume_context::<CoroutineHandle<M>>())
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub struct CoroutineHandle<T> {
|
||||
tx: UnboundedSender<T>,
|
||||
task: TaskId,
|
||||
}
|
||||
|
||||
impl<T> CoroutineHandle<T> {
|
||||
/// Get the ID of this coroutine
|
||||
#[must_use]
|
||||
pub fn task_id(&self) -> TaskId {
|
||||
self.task
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// state.pending_fut.set(Some(Box::pin(f)));
|
||||
let task2 = use_coroutine(&cx, view_task);
|
||||
|
||||
// if let Some(fut) = state.running_fut.as_mut() {
|
||||
// cx.push_future(fut);
|
||||
// }
|
||||
let task3 = use_coroutine(&cx, |rx| complex_task(rx, 10));
|
||||
|
||||
// if let Some(fut) = state.running_fut.take() {
|
||||
// state.running.set(true);
|
||||
// fut.resume();
|
||||
// }
|
||||
None
|
||||
}
|
||||
|
||||
// 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) });
|
||||
// });
|
||||
async fn view_task(mut rx: UnboundedReceiver<i32>) {
|
||||
while let Some(msg) = rx.next().await {
|
||||
println!("got message: {}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
// let submit = unsafe { std::mem::transmute(submit) };
|
||||
// state.submit.get_mut().replace(submit);
|
||||
enum Actions {
|
||||
CloseAll,
|
||||
OpenAll,
|
||||
}
|
||||
|
||||
// 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 }
|
||||
}
|
||||
|
||||
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>>>>>
|
||||
}
|
||||
|
||||
pub struct CoroutineHandle<'a> {
|
||||
cx: &'a ScopeState,
|
||||
inner: &'a State,
|
||||
}
|
||||
|
||||
impl Clone for CoroutineHandle<'_> {
|
||||
fn clone(&self) -> Self {
|
||||
CoroutineHandle {
|
||||
cx: self.cx,
|
||||
inner: self.inner,
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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());
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.inner.running.get()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+208
-37
@@ -1,57 +1,228 @@
|
||||
use dioxus_core::ScopeState;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use dioxus_core::ScopeState;
|
||||
|
||||
pub fn use_ref<'a, T: 'static>(cx: &'a ScopeState, f: impl FnOnce() -> T) -> &'a UseRef<T> {
|
||||
/// `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> {
|
||||
cx.use_hook(|_| UseRef {
|
||||
update_callback: cx.schedule_update(),
|
||||
value: Rc::new(RefCell::new(f())),
|
||||
update: cx.schedule_update(),
|
||||
value: Rc::new(RefCell::new(initialize_refcell())),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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)();
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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 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()
|
||||
}
|
||||
|
||||
/// 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.update)();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use std::{
|
||||
cell::{RefCell, RefMut},
|
||||
fmt::{Debug, Display},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// Store state between component renders.
|
||||
@@ -69,7 +70,7 @@ pub fn use_state<'a, T: 'static>(
|
||||
|
||||
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>>>,
|
||||
}
|
||||
|
||||
@@ -63,5 +63,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 {
|
||||
@@ -53,14 +53,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);
|
||||
}
|
||||
@@ -102,15 +100,15 @@ 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) {
|
||||
} else if (ns !== null || ns !== undefined) {
|
||||
node.setAttributeNS(ns, name, value);
|
||||
} else {
|
||||
switch (name) {
|
||||
case "value":
|
||||
if (value != node.value) {
|
||||
if (value !== node.value) {
|
||||
node.value = value;
|
||||
}
|
||||
break;
|
||||
@@ -125,7 +123,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 +131,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);
|
||||
@@ -200,20 +200,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.preventDefault();
|
||||
}
|
||||
}
|
||||
@@ -237,16 +239,16 @@ 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") {
|
||||
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 {
|
||||
@@ -258,14 +260,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 +281,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 +345,7 @@ export function serialize_event(event) {
|
||||
}
|
||||
return {
|
||||
value: value,
|
||||
values: {},
|
||||
};
|
||||
}
|
||||
case "input":
|
||||
@@ -349,7 +354,7 @@ 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 {
|
||||
@@ -544,6 +549,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;
|
||||
|
||||
@@ -16,8 +16,8 @@ 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"
|
||||
# url = "2.2.2"
|
||||
|
||||
# for wasm
|
||||
web-sys = { version = "0.3", features = [
|
||||
|
||||
@@ -34,6 +34,17 @@ pub struct LinkProps<'a> {
|
||||
#[props(default, strip_option)]
|
||||
title: Option<&'a str>,
|
||||
|
||||
#[props(default = true)]
|
||||
autodetect: bool,
|
||||
|
||||
/// Is this link an external link?
|
||||
#[props(default = false)]
|
||||
external: bool,
|
||||
|
||||
/// New tab?
|
||||
#[props(default = false)]
|
||||
new_tab: bool,
|
||||
|
||||
children: Element<'a>,
|
||||
|
||||
#[props(default)]
|
||||
@@ -41,17 +52,38 @@ pub struct LinkProps<'a> {
|
||||
}
|
||||
|
||||
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||
// log::trace!("render Link to {}", cx.props.to);
|
||||
if let Some(service) = cx.consume_context::<RouterService>() {
|
||||
let LinkProps {
|
||||
to,
|
||||
href,
|
||||
class,
|
||||
id,
|
||||
title,
|
||||
autodetect,
|
||||
external,
|
||||
new_tab,
|
||||
children,
|
||||
..
|
||||
} = cx.props;
|
||||
|
||||
let is_http = to.starts_with("http") || to.starts_with("https");
|
||||
let outerlink = (*autodetect && is_http) || *external;
|
||||
|
||||
let prevent_default = if outerlink { "" } else { "onclick" };
|
||||
|
||||
return cx.render(rsx! {
|
||||
a {
|
||||
href: "{cx.props.to}",
|
||||
class: format_args!("{}", cx.props.class.unwrap_or("")),
|
||||
id: format_args!("{}", cx.props.id.unwrap_or("")),
|
||||
title: format_args!("{}", cx.props.title.unwrap_or("")),
|
||||
|
||||
prevent_default: "onclick",
|
||||
onclick: move |_| service.push_route(cx.props.to),
|
||||
href: "{to}",
|
||||
class: format_args!("{}", class.unwrap_or("")),
|
||||
id: format_args!("{}", id.unwrap_or("")),
|
||||
title: format_args!("{}", title.unwrap_or("")),
|
||||
prevent_default: "{prevent_default}",
|
||||
target: format_args!("{}", if *new_tab { "_blank" } else { "" }),
|
||||
onclick: move |_| {
|
||||
if !outerlink {
|
||||
service.push_route(to);
|
||||
}
|
||||
},
|
||||
|
||||
&cx.props.children
|
||||
}
|
||||
|
||||
@@ -32,7 +32,5 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
|
||||
cx.props.onchange.call(path.to_string());
|
||||
}
|
||||
|
||||
cx.render(rsx!(
|
||||
div { &cx.props.children }
|
||||
))
|
||||
cx.render(rsx!(&cx.props.children))
|
||||
}
|
||||
@@ -106,7 +106,7 @@ impl Drop for UseRouteListener {
|
||||
}
|
||||
|
||||
/// This hook provides access to the `RouterService` for the app.
|
||||
pub fn use_router(cx: &ScopeState) -> &RouterService {
|
||||
pub fn use_router(cx: &ScopeState) -> &Rc<RouterService> {
|
||||
cx.use_hook(|_| {
|
||||
cx.consume_context::<RouterService>()
|
||||
.expect("Cannot call use_route outside the scope of a Router component")
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{
|
||||
cell::{Cell, Ref, RefCell},
|
||||
collections::{HashMap, HashSet},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use dioxus_core::ScopeId;
|
||||
@@ -10,7 +11,7 @@ use dioxus_core::ScopeId;
|
||||
use crate::platform::RouterProvider;
|
||||
|
||||
pub struct RouterService {
|
||||
pub(crate) regen_route: Rc<dyn Fn(ScopeId)>,
|
||||
pub(crate) regen_route: Arc<dyn Fn(ScopeId)>,
|
||||
pub(crate) pending_events: Rc<RefCell<Vec<RouteEvent>>>,
|
||||
slots: Rc<RefCell<Vec<(ScopeId, String)>>>,
|
||||
onchange_listeners: Rc<RefCell<HashSet<ScopeId>>>,
|
||||
@@ -42,7 +43,7 @@ enum RouteSlot {
|
||||
}
|
||||
|
||||
impl RouterService {
|
||||
pub fn new(regen_route: Rc<dyn Fn(ScopeId)>, root_scope: ScopeId) -> Self {
|
||||
pub fn new(regen_route: Arc<dyn Fn(ScopeId)>, root_scope: ScopeId) -> Self {
|
||||
let history = BrowserHistory::default();
|
||||
let location = history.location();
|
||||
let path = location.path();
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "dioxus-rsx"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proc-macro-error = "1"
|
||||
proc-macro2 = { version = "1.0.6" }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
||||
@@ -0,0 +1,234 @@
|
||||
//! Parse components into the VComponent VNode
|
||||
//! ==========================================
|
||||
//!
|
||||
//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the vcomponent format.
|
||||
//! We can be reasonably sure that whatever enters this parsing path is in the right format.
|
||||
//! This feature must support
|
||||
//! - [x] Namespaced components
|
||||
//! - [x] Fields
|
||||
//! - [x] Componentbuilder synax
|
||||
//! - [x] Optional commas
|
||||
//! - [ ] Children
|
||||
//! - [ ] Keys
|
||||
//! - [ ] Properties spreading with with `..` syntax
|
||||
|
||||
use super::*;
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
ext::IdentExt,
|
||||
parse::{Parse, ParseBuffer, ParseStream},
|
||||
token, Expr, Ident, LitStr, Result, Token,
|
||||
};
|
||||
|
||||
pub struct Component {
|
||||
pub name: syn::Path,
|
||||
pub body: Vec<ComponentField>,
|
||||
pub children: Vec<BodyNode>,
|
||||
pub manual_props: Option<Expr>,
|
||||
}
|
||||
|
||||
impl Parse for Component {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
let name = syn::Path::parse_mod_style(stream)?;
|
||||
|
||||
let content: ParseBuffer;
|
||||
|
||||
// if we see a `{` then we have a block
|
||||
// else parse as a function-like call
|
||||
if stream.peek(token::Brace) {
|
||||
syn::braced!(content in stream);
|
||||
} else {
|
||||
syn::parenthesized!(content in stream);
|
||||
}
|
||||
|
||||
let mut body = Vec::new();
|
||||
let mut children = Vec::new();
|
||||
let mut manual_props = None;
|
||||
|
||||
while !content.is_empty() {
|
||||
// if we splat into a component then we're merging properties
|
||||
if content.peek(Token![..]) {
|
||||
content.parse::<Token![..]>()?;
|
||||
manual_props = Some(content.parse::<Expr>()?);
|
||||
} else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
|
||||
body.push(content.parse::<ComponentField>()?);
|
||||
} else {
|
||||
children.push(content.parse::<BodyNode>()?);
|
||||
}
|
||||
|
||||
if content.peek(Token![,]) {
|
||||
let _ = content.parse::<Token![,]>();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
body,
|
||||
children,
|
||||
manual_props,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Component {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let name = &self.name;
|
||||
|
||||
let mut has_key = None;
|
||||
|
||||
let builder = match &self.manual_props {
|
||||
Some(manual_props) => {
|
||||
let mut toks = quote! {
|
||||
let mut __manual_props = #manual_props;
|
||||
};
|
||||
for field in &self.body {
|
||||
if field.name == "key" {
|
||||
has_key = Some(field);
|
||||
} else {
|
||||
let name = &field.name;
|
||||
let val = &field.content;
|
||||
toks.append_all(quote! {
|
||||
__manual_props.#name = #val;
|
||||
});
|
||||
}
|
||||
}
|
||||
toks.append_all(quote! {
|
||||
__manual_props
|
||||
});
|
||||
quote! {{
|
||||
#toks
|
||||
}}
|
||||
}
|
||||
None => {
|
||||
let mut toks = quote! { fc_to_builder(#name) };
|
||||
for field in &self.body {
|
||||
match field.name.to_string().as_str() {
|
||||
"key" => {
|
||||
//
|
||||
has_key = Some(field);
|
||||
}
|
||||
_ => toks.append_all(quote! {#field}),
|
||||
}
|
||||
}
|
||||
|
||||
if !self.children.is_empty() {
|
||||
let childs = &self.children;
|
||||
toks.append_all(quote! {
|
||||
.children(__cx.create_children([ #( #childs ),* ]))
|
||||
});
|
||||
}
|
||||
|
||||
toks.append_all(quote! {
|
||||
.build()
|
||||
});
|
||||
toks
|
||||
}
|
||||
};
|
||||
|
||||
let key_token = match has_key {
|
||||
Some(field) => {
|
||||
let inners = &field.content;
|
||||
quote! { Some(format_args_f!(#inners)) }
|
||||
}
|
||||
None => quote! { None },
|
||||
};
|
||||
|
||||
let fn_name = self.name.segments.last().unwrap().ident.to_string();
|
||||
|
||||
tokens.append_all(quote! {
|
||||
__cx.component(
|
||||
#name,
|
||||
#builder,
|
||||
#key_token,
|
||||
#fn_name
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// the struct's fields info
|
||||
pub struct ComponentField {
|
||||
name: Ident,
|
||||
content: ContentField,
|
||||
}
|
||||
|
||||
enum ContentField {
|
||||
ManExpr(Expr),
|
||||
Formatted(LitStr),
|
||||
OnHandlerRaw(Expr),
|
||||
}
|
||||
|
||||
impl ToTokens for ContentField {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match self {
|
||||
ContentField::ManExpr(e) => e.to_tokens(tokens),
|
||||
ContentField::Formatted(s) => tokens.append_all(quote! {
|
||||
__cx.raw_text(format_args_f!(#s)).0
|
||||
}),
|
||||
ContentField::OnHandlerRaw(e) => tokens.append_all(quote! {
|
||||
__cx.event_handler(#e)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ComponentField {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let name = Ident::parse_any(input)?;
|
||||
input.parse::<Token![:]>()?;
|
||||
|
||||
if name.to_string().starts_with("on") {
|
||||
let content = ContentField::OnHandlerRaw(input.parse()?);
|
||||
return Ok(Self { name, content });
|
||||
}
|
||||
|
||||
if name == "key" {
|
||||
let content = ContentField::ManExpr(input.parse()?);
|
||||
return Ok(Self { name, content });
|
||||
}
|
||||
|
||||
if input.peek(LitStr) && input.peek2(Token![,]) {
|
||||
let t: LitStr = input.fork().parse()?;
|
||||
|
||||
if is_literal_foramtted(&t) {
|
||||
let content = ContentField::Formatted(input.parse()?);
|
||||
return Ok(Self { name, content });
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
let content = ContentField::ManExpr(input.parse()?);
|
||||
Ok(Self { name, content })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ComponentField {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let ComponentField { name, content, .. } = self;
|
||||
tokens.append_all(quote! {
|
||||
.#name(#content)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn is_literal_foramtted(lit: &LitStr) -> bool {
|
||||
let s = lit.value();
|
||||
let mut chars = s.chars();
|
||||
|
||||
while let Some(next) = chars.next() {
|
||||
if next == '{' {
|
||||
let nen = chars.next();
|
||||
if nen != Some('{') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
use super::*;
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseBuffer, ParseStream},
|
||||
Expr, Ident, LitStr, Result, Token,
|
||||
};
|
||||
|
||||
// =======================================
|
||||
// Parse the VNode::Element type
|
||||
// =======================================
|
||||
pub struct Element {
|
||||
pub name: Ident,
|
||||
pub key: Option<LitStr>,
|
||||
pub attributes: Vec<ElementAttrNamed>,
|
||||
pub children: Vec<BodyNode>,
|
||||
pub _is_static: bool,
|
||||
}
|
||||
|
||||
impl Parse for Element {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
let el_name = Ident::parse(stream)?;
|
||||
|
||||
// parse the guts
|
||||
let content: ParseBuffer;
|
||||
syn::braced!(content in stream);
|
||||
|
||||
let mut attributes: Vec<ElementAttrNamed> = vec![];
|
||||
let mut children: Vec<BodyNode> = vec![];
|
||||
let mut key = None;
|
||||
let mut _el_ref = None;
|
||||
|
||||
// parse fields with commas
|
||||
// break when we don't get this pattern anymore
|
||||
// start parsing bodynodes
|
||||
// "def": 456,
|
||||
// abc: 123,
|
||||
loop {
|
||||
// Parse the raw literal fields
|
||||
if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
|
||||
let name = content.parse::<LitStr>()?;
|
||||
let ident = name.clone();
|
||||
|
||||
content.parse::<Token![:]>()?;
|
||||
|
||||
if content.peek(LitStr) && content.peek2(Token![,]) {
|
||||
let value = content.parse::<LitStr>()?;
|
||||
attributes.push(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::CustomAttrText { name, value },
|
||||
});
|
||||
} else {
|
||||
let value = content.parse::<Expr>()?;
|
||||
|
||||
attributes.push(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::CustomAttrExpression { name, value },
|
||||
});
|
||||
}
|
||||
|
||||
if content.is_empty() {
|
||||
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"
|
||||
)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
|
||||
let name = content.parse::<Ident>()?;
|
||||
let ident = name.clone();
|
||||
|
||||
let name_str = name.to_string();
|
||||
content.parse::<Token![:]>()?;
|
||||
|
||||
if name_str.starts_with("on") {
|
||||
attributes.push(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::EventTokens {
|
||||
name,
|
||||
tokens: content.parse()?,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
match name_str.as_str() {
|
||||
"key" => {
|
||||
key = Some(content.parse()?);
|
||||
}
|
||||
"classes" => todo!("custom class list not supported yet"),
|
||||
// "namespace" => todo!("custom namespace not supported yet"),
|
||||
"node_ref" => {
|
||||
_el_ref = Some(content.parse::<Expr>()?);
|
||||
}
|
||||
_ => {
|
||||
if content.peek(LitStr) {
|
||||
attributes.push(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::AttrText {
|
||||
name,
|
||||
value: content.parse()?,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
attributes.push(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::AttrExpression {
|
||||
name,
|
||||
value: content.parse()?,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if content.is_empty() {
|
||||
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"
|
||||
)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
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
|
||||
}";
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}";
|
||||
)
|
||||
}
|
||||
|
||||
children.push(content.parse::<BodyNode>()?);
|
||||
// consume comma if it exists
|
||||
// we don't actually care if there *are* commas after elements/text
|
||||
if content.peek(Token![,]) {
|
||||
let _ = content.parse::<Token![,]>();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
key,
|
||||
name: el_name,
|
||||
attributes,
|
||||
children,
|
||||
_is_static: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Element {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let name = &self.name;
|
||||
let children = &self.children;
|
||||
|
||||
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,
|
||||
__cx.bump().alloc([ #(#listeners),* ]),
|
||||
__cx.bump().alloc([ #(#attr),* ]),
|
||||
__cx.bump().alloc([ #(#children),* ]),
|
||||
#key,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ElementAttr {
|
||||
/// attribute: "valuee {}"
|
||||
AttrText { name: Ident, value: LitStr },
|
||||
|
||||
/// attribute: true,
|
||||
AttrExpression { name: Ident, value: Expr },
|
||||
|
||||
/// "attribute": "value {}"
|
||||
CustomAttrText { name: LitStr, value: LitStr },
|
||||
|
||||
/// "attribute": true,
|
||||
CustomAttrExpression { name: LitStr, value: Expr },
|
||||
|
||||
// /// onclick: move |_| {}
|
||||
// EventClosure { name: Ident, closure: ExprClosure },
|
||||
/// onclick: {}
|
||||
EventTokens { name: Ident, tokens: Expr },
|
||||
}
|
||||
|
||||
pub struct ElementAttrNamed {
|
||||
pub el_name: Ident,
|
||||
pub attr: ElementAttr,
|
||||
}
|
||||
|
||||
impl ToTokens for ElementAttrNamed {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let ElementAttrNamed { el_name, attr } = self;
|
||||
|
||||
tokens.append_all(match attr {
|
||||
ElementAttr::AttrText { name, value } => {
|
||||
quote! {
|
||||
dioxus_elements::#el_name.#name(__cx, format_args_f!(#value))
|
||||
}
|
||||
}
|
||||
ElementAttr::AttrExpression { name, value } => {
|
||||
quote! {
|
||||
dioxus_elements::#el_name.#name(__cx, #value)
|
||||
}
|
||||
}
|
||||
ElementAttr::CustomAttrText { name, value } => {
|
||||
quote! {
|
||||
__cx.attr( #name, format_args_f!(#value), None, false )
|
||||
}
|
||||
}
|
||||
ElementAttr::CustomAttrExpression { name, value } => {
|
||||
quote! {
|
||||
__cx.attr( #name, format_args_f!(#value), None, false )
|
||||
}
|
||||
}
|
||||
// ElementAttr::EventClosure { name, closure } => {
|
||||
// quote! {
|
||||
// dioxus_elements::on::#name(__cx, #closure)
|
||||
// }
|
||||
// }
|
||||
ElementAttr::EventTokens { name, tokens } => {
|
||||
quote! {
|
||||
dioxus_elements::on::#name(__cx, #tokens)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//! Parse the root tokens in the rsx!{} macro
|
||||
//! =========================================
|
||||
//!
|
||||
//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing.
|
||||
//! This feature must support:
|
||||
//! - [x] Optionally rendering if the `in XYZ` pattern is present
|
||||
//! - [x] Fragments as top-level element (through ambiguous)
|
||||
//! - [x] Components as top-level element (through ambiguous)
|
||||
//! - [x] Tags as top-level elements (through ambiguous)
|
||||
//! - [x] Good errors if parsing fails
|
||||
//!
|
||||
//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful.
|
||||
|
||||
mod component;
|
||||
mod element;
|
||||
mod node;
|
||||
|
||||
pub mod pretty;
|
||||
|
||||
// Re-export the namespaces into each other
|
||||
pub use component::*;
|
||||
pub use element::*;
|
||||
pub use node::*;
|
||||
|
||||
// imports
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Ident, Result, Token,
|
||||
};
|
||||
|
||||
pub struct CallBody {
|
||||
pub custom_context: Option<Ident>,
|
||||
pub roots: Vec<BodyNode>,
|
||||
}
|
||||
|
||||
impl Parse for CallBody {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let custom_context = if input.peek(Ident) && input.peek2(Token![,]) {
|
||||
let name = input.parse::<Ident>()?;
|
||||
input.parse::<Token![,]>()?;
|
||||
|
||||
Some(name)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut roots = Vec::new();
|
||||
|
||||
while !input.is_empty() {
|
||||
let node = input.parse::<BodyNode>()?;
|
||||
|
||||
if input.peek(Token![,]) {
|
||||
let _ = input.parse::<Token![,]>();
|
||||
}
|
||||
|
||||
roots.push(node);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
custom_context,
|
||||
roots,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize the same way, regardless of flavor
|
||||
impl ToTokens for CallBody {
|
||||
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
|
||||
let inner = if self.roots.len() == 1 {
|
||||
let inner = &self.roots[0];
|
||||
quote! { #inner }
|
||||
} else {
|
||||
let childs = &self.roots;
|
||||
quote! { __cx.fragment_root([ #(#childs),* ]) }
|
||||
};
|
||||
|
||||
match &self.custom_context {
|
||||
// The `in cx` pattern allows directly rendering
|
||||
Some(ident) => out_tokens.append_all(quote! {
|
||||
#ident.render(LazyNodes::new(move |__cx: NodeFactory| -> VNode {
|
||||
use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
#inner
|
||||
}))
|
||||
}),
|
||||
|
||||
// Otherwise we just build the LazyNode wrapper
|
||||
None => out_tokens.append_all(quote! {
|
||||
LazyNodes::new(move |__cx: NodeFactory| -> VNode {
|
||||
use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
#inner
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use super::*;
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
token, Attribute, Expr, LitStr, Result, Token,
|
||||
};
|
||||
|
||||
/*
|
||||
Parse
|
||||
-> div {}
|
||||
-> Component {}
|
||||
-> component()
|
||||
-> "text {with_args}"
|
||||
-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr
|
||||
*/
|
||||
pub enum BodyNode {
|
||||
Element(Element),
|
||||
Component(Component),
|
||||
Text(LitStr),
|
||||
RawExpr(Expr),
|
||||
Meta(Attribute),
|
||||
}
|
||||
|
||||
impl Parse for BodyNode {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
if stream.peek(LitStr) {
|
||||
return Ok(BodyNode::Text(stream.parse()?));
|
||||
}
|
||||
|
||||
// div {} -> el
|
||||
// Div {} -> comp
|
||||
if stream.peek(syn::Ident) && stream.peek2(token::Brace) {
|
||||
if stream
|
||||
.fork()
|
||||
.parse::<Ident>()?
|
||||
.to_string()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap()
|
||||
.is_ascii_uppercase()
|
||||
{
|
||||
return Ok(BodyNode::Component(stream.parse()?));
|
||||
} else {
|
||||
return Ok(BodyNode::Element(stream.parse::<Element>()?));
|
||||
}
|
||||
}
|
||||
|
||||
// component() -> comp
|
||||
// ::component {} -> comp
|
||||
// ::component () -> comp
|
||||
if (stream.peek(syn::Ident) && stream.peek2(token::Paren))
|
||||
|| (stream.peek(Token![::]))
|
||||
|| (stream.peek(Token![:]) && stream.peek2(Token![:]))
|
||||
{
|
||||
return Ok(BodyNode::Component(stream.parse::<Component>()?));
|
||||
}
|
||||
|
||||
// crate::component{} -> comp
|
||||
// crate::component() -> comp
|
||||
if let Ok(pat) = stream.fork().parse::<syn::Path>() {
|
||||
if pat.segments.len() > 1 {
|
||||
return Ok(BodyNode::Component(stream.parse::<Component>()?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BodyNode::RawExpr(stream.parse::<Expr>()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for BodyNode {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match &self {
|
||||
BodyNode::Element(el) => el.to_tokens(tokens),
|
||||
BodyNode::Component(comp) => comp.to_tokens(tokens),
|
||||
BodyNode::Text(txt) => tokens.append_all(quote! {
|
||||
__cx.text(format_args_f!(#txt))
|
||||
}),
|
||||
BodyNode::RawExpr(exp) => tokens.append_all(quote! {
|
||||
__cx.fragment_from_iter(#exp)
|
||||
}),
|
||||
BodyNode::Meta(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
//! pretty printer for rsx!
|
||||
@@ -15,21 +15,18 @@ dioxus-core = { path = "../core", version = "^0.1.9" }
|
||||
dioxus-html = { path = "../html", version = "^0.1.6" }
|
||||
js-sys = "0.3.56"
|
||||
wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] }
|
||||
lazy_static = "1.4.0"
|
||||
wasm-bindgen-futures = "0.4.29"
|
||||
log = { version = "0.4.14", features = ["release_max_level_off"] }
|
||||
fxhash = "0.2.1"
|
||||
wasm-logger = "0.2.0"
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
wasm-bindgen-test = "0.3.29"
|
||||
once_cell = "1.9.0"
|
||||
async-channel = "1.6.1"
|
||||
anyhow = "1.0.53"
|
||||
gloo-timers = { version = "0.2.3", features = ["futures"] }
|
||||
futures-util = "0.3.19"
|
||||
smallstr = "0.2.0"
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0", features = ["web"] }
|
||||
serde-wasm-bindgen = "0.4.2"
|
||||
futures-channel = "0.3.21"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.56"
|
||||
@@ -80,3 +77,5 @@ panic_hook = ["console_error_panic_hook"]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
wasm-bindgen-test = "0.3.29"
|
||||
dioxus-ssr = { path = "../ssr" }
|
||||
wasm-logger = "0.2.0"
|
||||
|
||||
@@ -87,7 +87,12 @@ impl WebsysDom {
|
||||
}
|
||||
});
|
||||
|
||||
let root = load_document().get_element_by_id(&cfg.rootname).unwrap();
|
||||
// a match here in order to avoid some error during runtime browser test
|
||||
let document = load_document();
|
||||
let root = match document.get_element_by_id(&cfg.rootname) {
|
||||
Some(root) => root,
|
||||
None => document.create_element("body").ok().unwrap(),
|
||||
};
|
||||
|
||||
Self {
|
||||
interpreter: Interpreter::new(root.clone()),
|
||||
@@ -122,8 +127,8 @@ impl WebsysDom {
|
||||
self.interpreter.RemoveEventListener(root, event)
|
||||
}
|
||||
|
||||
DomEdit::RemoveAttribute { root, name } => {
|
||||
self.interpreter.RemoveAttribute(root, name)
|
||||
DomEdit::RemoveAttribute { root, name, ns } => {
|
||||
self.interpreter.RemoveAttribute(root, name, ns)
|
||||
}
|
||||
|
||||
DomEdit::CreateTextNode { text, root } => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! Dioxus WebSys
|
||||
//!
|
||||
//! ## Overview
|
||||
@@ -211,7 +213,7 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
|
||||
websys_dom.apply_edits(edits.edits);
|
||||
}
|
||||
|
||||
let work_loop = ric_raf::RafLoop::new();
|
||||
let mut work_loop = ric_raf::RafLoop::new();
|
||||
|
||||
loop {
|
||||
log::trace!("waiting for work");
|
||||
|
||||
+11
-10
@@ -7,6 +7,7 @@
|
||||
//! Because RIC doesn't work on Safari, we polyfill using the "ricpolyfill.js" file and use some basic detection to see
|
||||
//! if RIC is available.
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use js_sys::Function;
|
||||
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
|
||||
@@ -14,21 +15,21 @@ use web_sys::{window, Window};
|
||||
|
||||
pub(crate) struct RafLoop {
|
||||
window: Window,
|
||||
ric_receiver: async_channel::Receiver<u32>,
|
||||
raf_receiver: async_channel::Receiver<()>,
|
||||
ric_receiver: futures_channel::mpsc::UnboundedReceiver<u32>,
|
||||
raf_receiver: futures_channel::mpsc::UnboundedReceiver<()>,
|
||||
ric_closure: Closure<dyn Fn(JsValue)>,
|
||||
raf_closure: Closure<dyn Fn(JsValue)>,
|
||||
}
|
||||
|
||||
impl RafLoop {
|
||||
pub fn new() -> Self {
|
||||
let (raf_sender, raf_receiver) = async_channel::unbounded();
|
||||
let (raf_sender, raf_receiver) = futures_channel::mpsc::unbounded();
|
||||
|
||||
let raf_closure: Closure<dyn Fn(JsValue)> = Closure::wrap(Box::new(move |_v: JsValue| {
|
||||
raf_sender.try_send(()).unwrap()
|
||||
raf_sender.unbounded_send(()).unwrap()
|
||||
}));
|
||||
|
||||
let (ric_sender, ric_receiver) = async_channel::unbounded();
|
||||
let (ric_sender, ric_receiver) = futures_channel::mpsc::unbounded();
|
||||
|
||||
let has_idle_callback = {
|
||||
let bo = window().unwrap().dyn_into::<js_sys::Object>().unwrap();
|
||||
@@ -45,7 +46,7 @@ impl RafLoop {
|
||||
10
|
||||
};
|
||||
|
||||
ric_sender.try_send(time_remaining).unwrap()
|
||||
ric_sender.unbounded_send(time_remaining).unwrap()
|
||||
}));
|
||||
|
||||
// execute the polyfill for safari
|
||||
@@ -64,16 +65,16 @@ impl RafLoop {
|
||||
}
|
||||
}
|
||||
/// waits for some idle time and returns a timeout future that expires after the idle time has passed
|
||||
pub async fn wait_for_idle_time(&self) -> TimeoutFuture {
|
||||
pub async fn wait_for_idle_time(&mut self) -> TimeoutFuture {
|
||||
let ric_fn = self.ric_closure.as_ref().dyn_ref::<Function>().unwrap();
|
||||
let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap();
|
||||
let deadline = self.ric_receiver.recv().await.unwrap();
|
||||
let deadline = self.ric_receiver.next().await.unwrap();
|
||||
TimeoutFuture::new(deadline)
|
||||
}
|
||||
|
||||
pub async fn wait_for_raf(&self) {
|
||||
pub async fn wait_for_raf(&mut self) {
|
||||
let raf_fn = self.raf_closure.as_ref().dyn_ref::<Function>().unwrap();
|
||||
let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap();
|
||||
self.raf_receiver.recv().await.unwrap();
|
||||
self.raf_receiver.next().await.unwrap();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user