Merge branch 'master' into jk/coroutine-coroutineoverhaul

This commit is contained in:
Jonathan Kelley committed 2022-02-20 23:53:55 -05:00
commit f1af18c60b
213 files changed
+6234 -4644

No files matched your search

+39 -1
View File
@@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### New Features
- <csr-id-d2372717bd01fcff50af0572360e3f763d4c869d/> flatten props attrs
- <csr-id-47bc4e4a44a7d08d4f42102d13f0766d9d6bf358/> add "optional" flag for props
### Bug Fixes
- <csr-id-90abd9c9a08c165384fae9c1f7c3fd098d512c48/> detection of f-string formatting in components
### Commit Statistics
<csr-read-only-do-not-edit/>
- 9 commits contributed to the release over the course of 12 calendar days.
- 3 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
### Commit Details
<csr-read-only-do-not-edit/>
<details><summary>view details</summary>
* **Uncategorized**
- Merge pull request #95 from DioxusLabs/jk/filedragindrop ([`ca0dd4a`](https://github.comgit//DioxusLabs/dioxus/commit/ca0dd4aa7192d483a195d420363f39d771f3e471))
- Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d))
- flatten props attrs ([`d237271`](https://github.comgit//DioxusLabs/dioxus/commit/d2372717bd01fcff50af0572360e3f763d4c869d))
- Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f))
- detection of f-string formatting in components ([`90abd9c`](https://github.comgit//DioxusLabs/dioxus/commit/90abd9c9a08c165384fae9c1f7c3fd098d512c48))
- Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11))
- Merge pull request #107 from autarch/autarch/half-assed-router ([`8d3ac3f`](https://github.comgit//DioxusLabs/dioxus/commit/8d3ac3ff148aef9d10a393eda453a11c1e882f58))
- Merge pull request #127 from DioxusLabs/jk/handler-tweak ([`5bce294`](https://github.comgit//DioxusLabs/dioxus/commit/5bce294a86941090660b1cfd87809a2b0b76d2ce))
- add "optional" flag for props ([`47bc4e4`](https://github.comgit//DioxusLabs/dioxus/commit/47bc4e4a44a7d08d4f42102d13f0766d9d6bf358))
</details>
## v0.1.6 (2022-01-08)
### Documentation
@@ -64,7 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<csr-read-only-do-not-edit/>
- 138 commits contributed to the release over the course of 352 calendar days.
- 139 commits contributed to the release over the course of 352 calendar days.
- 123 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
@@ -75,6 +112,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<details><summary>view details</summary>
* **Uncategorized**
- Release dioxus-core-macro v0.1.6 ([`db0a5bd`](https://github.comgit//DioxusLabs/dioxus/commit/db0a5bd6ec93803cddb3c6fda4172257b9c301c6))
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76))
- Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee))
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-core-macro"
version = "0.1.6"
version = "0.1.7"
authors = ["Jonathan Kelley"]
edition = "2021"
description = "Core macro for Dioxus Virtual DOM"
@@ -15,7 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
proc-macro = true
[dependencies]
once_cell = "1.8"
dioxus-rsx = { path = "../rsx" }
proc-macro-error = "1.0.4"
proc-macro2 = { version = "1.0.6" }
quote = "1.0"
@@ -1,8 +0,0 @@
fn main() {
// use dioxus_core_macro::*;
// let r = html! {
// <div>
// "hello world"
// </div>
// };
}
@@ -1,22 +0,0 @@
use dioxus_core_macro::{inline_props, Props};
fn main() {}
type Element<'a> = ();
pub struct Scope<'a, T = ()> {
props: &'a T,
}
// #[inline_props]
pub fn component<'a>(
cx: Scope<'a>,
chkk: String,
chkk2: String,
r: u32,
cat: &'a str,
drd: String,
e: String,
) -> Element<'a> {
let r = chkk.len();
}
-24
View File
@@ -1,24 +0,0 @@
fn main() {}
pub mod dioxus {
pub mod prelude {
pub trait Properties {
type Builder;
const IS_STATIC: bool;
fn builder() -> Self::Builder;
unsafe fn memoize(&self, other: &Self) -> bool;
}
}
}
/// This implementation should require a "PartialEq" because it memoizes (no external references)
#[derive(PartialEq, dioxus_core_macro::Props)]
struct SomeProps {
a: String,
}
/// This implementation does not require a "PartialEq" because it does not memoize
#[derive(dioxus_core_macro::Props)]
struct SomePropsTwo<'a> {
_a: &'a str,
}
-443
View File
@@ -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();
}
}
+4 -36
View File
@@ -2,11 +2,9 @@ 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;
#[proc_macro]
pub fn format_args_f(input: TokenStream) -> TokenStream {
@@ -180,42 +178,12 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
#[proc_macro_error::proc_macro_error]
#[proc_macro]
pub fn rsx(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) {
match syn::parse::<dioxus_rsx::CallBody>(s) {
Err(err) => err.to_compile_error().into(),
Ok(stream) => stream.to_token_stream().into(),
}
}
/// 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>`,
-214
View File
@@ -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
// }
// }
}
}
-84
View File
@@ -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);
}
}
+55 -2
View File
@@ -5,6 +5,58 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Documentation
- <csr-id-036a0ff49a7dade0e04c9c07071a1ff49133ee24/> add comments for the Handler
### New Features
- <csr-id-bad4b773b7bbab6a38d4f48d88d8c5a8787927ac/> add "spawn" method
- <csr-id-d2bd1751436ef4bec554bb361ba87aea1357036a/> allow providing context to the root component
### Bug Fixes
- <csr-id-d9a07ddddb45ba00e35645ac0e0ad1a4d2dd996d/> provide_root_context on root scopes
- <csr-id-c8d528b3b18e320ca0cee1542ae5fc659c1cca81/> proprogation of root context
- <csr-id-e47ead5347ce778935f8f2127fcb14f520eda0ce/> allow eventhandler to derive default
### Commit Statistics
<csr-read-only-do-not-edit/>
- 18 commits contributed to the release over the course of 13 calendar days.
- 8 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
### Commit Details
<csr-read-only-do-not-edit/>
<details><summary>view details</summary>
* **Uncategorized**
- Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a))
- Merge branch 'master' of github.com:DioxusLabs/dioxus ([`8899701`](https://github.comgit//DioxusLabs/dioxus/commit/88997019c5c74658bfd76344d6bb6f3565943b41))
- add miri stress test ([`e9792e9`](https://github.comgit//DioxusLabs/dioxus/commit/e9792e9b95521fcf0b0b4e240d7cf0397cd79b8f))
- add "spawn" method ([`bad4b77`](https://github.comgit//DioxusLabs/dioxus/commit/bad4b773b7bbab6a38d4f48d88d8c5a8787927ac))
- drop hooks before resetting bump arena ([`2e4f765`](https://github.comgit//DioxusLabs/dioxus/commit/2e4f7659327b69b74e20af59b6d5e65c699b6adb))
- provide_root_context on root scopes ([`d9a07dd`](https://github.comgit//DioxusLabs/dioxus/commit/d9a07ddddb45ba00e35645ac0e0ad1a4d2dd996d))
- Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d))
- Merge pull request #121 from DioxusLabs/jk/unify ([`b287a4c`](https://github.comgit//DioxusLabs/dioxus/commit/b287a4cab3eec9e6961a3f010846291f4f105747))
- Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f))
- proprogation of root context ([`c8d528b`](https://github.comgit//DioxusLabs/dioxus/commit/c8d528b3b18e320ca0cee1542ae5fc659c1cca81))
- allow providing context to the root component ([`d2bd175`](https://github.comgit//DioxusLabs/dioxus/commit/d2bd1751436ef4bec554bb361ba87aea1357036a))
- Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11))
- Merge pull request #107 from autarch/autarch/half-assed-router ([`8d3ac3f`](https://github.comgit//DioxusLabs/dioxus/commit/8d3ac3ff148aef9d10a393eda453a11c1e882f58))
- Merge pull request #138 from mrxiaozhuox/master ([`8c7473d`](https://github.comgit//DioxusLabs/dioxus/commit/8c7473d1943dd133f388ec36116c9d8295861b97))
- Merge pull request #133 from mrxiaozhuox/master ([`887f69d`](https://github.comgit//DioxusLabs/dioxus/commit/887f69d5b47bdcde4fe0eab094c0cd0de23e4f3f))
- Don't expect all components to have a scope in ScopeArena.ensure_drop_safety ([`9b282d8`](https://github.comgit//DioxusLabs/dioxus/commit/9b282d877ba0fe2463687cde7ea899b7f8510425))
- add comments for the Handler ([`036a0ff`](https://github.comgit//DioxusLabs/dioxus/commit/036a0ff49a7dade0e04c9c07071a1ff49133ee24))
- allow eventhandler to derive default ([`e47ead5`](https://github.comgit//DioxusLabs/dioxus/commit/e47ead5347ce778935f8f2127fcb14f520eda0ce))
</details>
## v0.1.7 (2022-01-08)
### Documentation
@@ -138,7 +190,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<csr-read-only-do-not-edit/>
- 437 commits contributed to the release over the course of 358 calendar days.
- 438 commits contributed to the release over the course of 358 calendar days.
- 416 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
@@ -149,8 +201,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<details><summary>view details</summary>
* **Uncategorized**
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- Release dioxus-core v0.1.7 ([`16d73b2`](https://github.comgit//DioxusLabs/dioxus/commit/16d73b240fc446c6f6996c19ccf52bbcda2eca79))
- add title to doc comment ([`d11f322`](https://github.comgit//DioxusLabs/dioxus/commit/d11f322f554e7dbf43b988c9cfda56498cc49872))
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- better document the `EventHandler` type ([`be9f1a5`](https://github.comgit//DioxusLabs/dioxus/commit/be9f1a52ad2b04f101397ae34482ea7394df653b))
- component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76))
- Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee))
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-core"
version = "0.1.7"
version = "0.1.9"
authors = ["Jonathan Kelley"]
edition = "2018"
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
@@ -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"
-33
View File
@@ -1,33 +0,0 @@
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
fn main() {}
fn app(cx: Scope) -> Element {
cx.render(rsx!(div {
app2(
p: "asd"
)
}))
}
#[derive(Props)]
struct Borrowed<'a> {
p: &'a str,
}
fn app2<'a>(cx: Scope<'a, Borrowed<'a>>) -> Element {
let g = eat2(&cx);
rsx!(cx, "")
}
fn eat2(s: &ScopeState) {}
fn eat(f: &str) {}
fn bleat() {
let blah = String::from("asd");
eat(&blah);
}
@@ -1,40 +0,0 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
fn main() {
let mut dom = VirtualDom::new(parent);
let edits = dom.rebuild();
dbg!(edits);
}
fn parent(cx: Scope) -> Element {
let value = cx.use_hook(|_| String::new());
cx.render(rsx! {
div {
child(
name: value,
h1 {"hi"}
)
}
})
}
#[derive(Props)]
struct ChildProps<'a> {
name: &'a str,
children: Element<'a>,
}
fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
cx.render(rsx! {
div {
"it's nested {cx.props.name}",
&cx.props.children
}
})
}
-9
View File
@@ -1,9 +0,0 @@
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
// very tiny hello world
fn main() {
dioxus::VirtualDom::new(|cx| rsx!(cx, "hello world"));
}
-54
View File
@@ -1,54 +0,0 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
fn main() {
let _ = VirtualDom::new(parent);
}
fn parent(cx: Scope) -> Element {
let value = cx.use_hook(|_| String::new());
cx.render(rsx! {
div {
child( name: value )
}
})
}
#[derive(Props)]
struct ChildProps<'a> {
name: &'a str,
}
fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
cx.render(rsx! {
div {
h1 { "it's nested" }
grandchild( name: cx.props.name )
}
})
}
#[derive(Props)]
struct Grandchild<'a> {
name: &'a str,
}
fn grandchild<'a>(cx: Scope<'a, Grandchild>) -> Element<'a> {
cx.render(rsx! {
div { "Hello {cx.props.name}!" }
great_grandchild( name: cx.props.name )
})
}
fn great_grandchild<'a>(cx: Scope<'a, Grandchild>) -> Element<'a> {
cx.render(rsx! {
div {
h1 { "it's nested" }
}
})
}
+496 -673
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -12,7 +12,6 @@ pub(crate) mod util;
pub(crate) mod virtual_dom;
pub(crate) mod innerlude {
pub(crate) use crate::diff::*;
pub use crate::events::*;
pub use crate::lazynodes::*;
pub use crate::mutations::*;
+8
View File
@@ -143,6 +143,10 @@ impl<'a> Mutations<'a> {
self.edits.push(InsertBefore { n, root });
}
pub(crate) fn append_children(&mut self, n: u32) {
self.edits.push(AppendChildren { many: n });
}
// Remove Nodes from the dom
pub(crate) fn remove(&mut self, id: u64) {
self.edits.push(Remove { root: id });
@@ -217,6 +221,10 @@ impl<'a> Mutations<'a> {
let name = attribute.name;
self.edits.push(RemoveAttribute { name, root });
}
pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) {
self.dirty_scopes.insert(scope);
}
}
// refs are only assigned once
+24 -13
View File
@@ -4,7 +4,7 @@
//! cheap and *very* fast to construct - building a full tree should be quick.
use crate::{
innerlude::{Element, Properties, Scope, ScopeId, ScopeState},
innerlude::{ComponentPtr, Element, Properties, Scope, ScopeId, ScopeState},
lazynodes::LazyNodes,
AnyEvent, Component,
};
@@ -147,13 +147,6 @@ impl<'src> VNode<'src> {
}
}
pub(crate) fn children(&self) -> &[VNode<'src>] {
match &self {
VNode::Fragment(f) => f.children,
_ => &[],
}
}
// Create an "owned" version of the vnode.
pub fn decouple(&self) -> VNode<'src> {
match *self {
@@ -175,13 +168,21 @@ impl Debug for VNode<'_> {
.field("key", &el.key)
.field("attrs", &el.attributes)
.field("children", &el.children)
.field("id", &el.id)
.finish(),
VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text),
VNode::Placeholder(_) => write!(s, "VNode::VPlaceholder"),
VNode::Placeholder(t) => write!(s, "VNode::VPlaceholder {{ id: {:?} }}", t.id),
VNode::Fragment(frag) => {
write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children)
}
VNode::Component(comp) => write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc),
VNode::Component(comp) => s
.debug_struct("VNode::VComponent")
.field("name", &comp.fn_name)
.field("fnptr", &comp.user_fc)
.field("key", &comp.key)
.field("scope", &comp.scope)
.field("originator", &comp.originator)
.finish(),
}
}
}
@@ -351,11 +352,18 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>;
/// }
///
/// ```
#[derive(Default)]
pub struct EventHandler<'bump, T = ()> {
pub callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
}
impl<'a, T> Default for EventHandler<'a, T> {
fn default() -> Self {
Self {
callback: RefCell::new(None),
}
}
}
impl<T> EventHandler<'_, T> {
/// Call this event handler with the appropriate event type
pub fn call(&self, event: T) {
@@ -377,7 +385,8 @@ pub struct VComponent<'src> {
pub originator: ScopeId,
pub scope: Cell<Option<ScopeId>>,
pub can_memoize: bool,
pub user_fc: *const (),
pub user_fc: ComponentPtr,
pub fn_name: &'static str,
pub props: RefCell<Option<Box<dyn AnyProps + 'src>>>,
}
@@ -542,6 +551,7 @@ impl<'a> NodeFactory<'a> {
component: fn(Scope<'a, P>) -> Element,
props: P,
key: Option<Arguments>,
fn_name: &'static str,
) -> VNode<'a>
where
P: Properties + 'a,
@@ -550,8 +560,9 @@ impl<'a> NodeFactory<'a> {
key: key.map(|f| self.raw_text(f).0),
scope: Default::default(),
can_memoize: P::IS_STATIC,
user_fc: component as *const (),
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),
+57 -66
View File
@@ -13,7 +13,9 @@ use std::{
rc::Rc,
};
pub(crate) type FcSlot = *const ();
/// for traceability, we use the raw fn pointer to identify the function
/// we also get the component name, but that's not necessarily unique in the app
pub(crate) type ComponentPtr = *mut std::os::raw::c_void;
pub(crate) struct Heuristic {
hook_arena_size: usize,
@@ -28,7 +30,7 @@ pub(crate) struct ScopeArena {
pub scope_gen: Cell<usize>,
pub bump: Bump,
pub scopes: RefCell<FxHashMap<ScopeId, *mut ScopeState>>,
pub heuristics: RefCell<FxHashMap<FcSlot, Heuristic>>,
pub heuristics: RefCell<FxHashMap<ComponentPtr, Heuristic>>,
pub free_scopes: RefCell<Vec<*mut ScopeState>>,
pub nodes: RefCell<Slab<*const VNode<'static>>>,
pub tasks: Rc<TaskQueue>,
@@ -82,7 +84,7 @@ impl ScopeArena {
pub(crate) fn new_with_key(
&self,
fc_ptr: *const (),
fc_ptr: ComponentPtr,
vcomp: Box<dyn AnyProps>,
parent_scope: Option<ScopeId>,
container: ElementId,
@@ -94,7 +96,7 @@ impl ScopeArena {
// Get the height of the scope
let height = parent_scope
.map(|id| self.get_scope(id).map(|scope| scope.height))
.map(|id| self.get_scope(id).map(|scope| scope.height + 1))
.flatten()
.unwrap_or_default();
@@ -110,31 +112,62 @@ impl ScopeArena {
if let Some(old_scope) = self.free_scopes.borrow_mut().pop() {
// reuse the old scope
let scope = unsafe { &mut *old_scope };
scope.props.get_mut().replace(vcomp);
scope.container = container;
scope.our_arena_idx = new_scope_id;
scope.parent_scope = parent_scope;
scope.height = height;
scope.fnptr = fc_ptr;
scope.props.get_mut().replace(vcomp);
scope.subtree.set(subtree);
scope.our_arena_idx = new_scope_id;
scope.container = container;
scope.frames[0].reset();
scope.frames[1].reset();
scope.shared_contexts.get_mut().clear();
scope.items.get_mut().listeners.clear();
scope.items.get_mut().borrowed_props.clear();
scope.hook_idx.set(0);
scope.hook_vals.get_mut().clear();
let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope);
debug_assert!(any_item.is_none());
} else {
// else create a new scope
let (node_capacity, hook_capacity) = self
.heuristics
.borrow()
.get(&fc_ptr)
.map(|h| (h.node_arena_size, h.hook_arena_size))
.unwrap_or_default();
self.scopes.borrow_mut().insert(
new_scope_id,
self.bump.alloc(ScopeState::new(
height,
self.bump.alloc(ScopeState {
container,
new_scope_id,
our_arena_idx: new_scope_id,
parent_scope,
vcomp,
self.tasks.clone(),
self.heuristics
.borrow()
.get(&fc_ptr)
.map(|h| (h.node_arena_size, h.hook_arena_size))
.unwrap_or_default(),
)),
height,
fnptr: fc_ptr,
props: RefCell::new(Some(vcomp)),
frames: [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)],
// todo: subtrees
subtree: Cell::new(0),
is_subtree_root: Cell::new(false),
generation: 0.into(),
tasks: self.tasks.clone(),
shared_contexts: Default::default(),
items: RefCell::new(SelfReferentialItems {
listeners: Default::default(),
borrowed_props: Default::default(),
}),
hook_arena: Bump::new(),
hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)),
hook_idx: Default::default(),
}),
);
}
@@ -189,8 +222,6 @@ impl ScopeArena {
/// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will
/// be dropped.
pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
log::trace!("Ensuring drop safety for scope {:?}", scope_id);
if let Some(scope) = self.get_scope(scope_id) {
let mut items = scope.items.borrow_mut();
@@ -201,7 +232,6 @@ impl ScopeArena {
if let Some(scope_id) = comp.scope.get() {
self.ensure_drop_safety(scope_id);
}
drop(comp.props.take());
});
@@ -217,18 +247,18 @@ impl ScopeArena {
// Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
log::trace!("Running scope {:?}", id);
self.ensure_drop_safety(id);
// todo: we *know* that this is aliased by the contents of the scope itself
let scope = unsafe { &mut *self.get_scope_raw(id).expect("could not find scope") };
log::trace!("running scope {:?} symbol: {:?}", id, scope.fnptr);
// Safety:
// - We dropped the listeners, so no more &mut T can be used while these are held
// - All children nodes that rely on &mut T are replaced with a new reference
scope.hook_idx.set(0);
// book keeping to ensure safety around the borrowed data
{
// Safety:
// - We've dropped all references to the wip bump frame with "ensure_drop_safety"
@@ -241,8 +271,6 @@ impl ScopeArena {
debug_assert!(items.borrowed_props.is_empty());
}
// safety: this is definitely not dropped
/*
If the component returns None, then we fill in a placeholder node. This will wipe what was there.
An alternate approach is to leave the Real Dom the same, but that can lead to safety issues and a lot more checks.
@@ -284,14 +312,13 @@ impl ScopeArena {
while let Some(id) = cur_el.take() {
if let Some(el) = nodes.get(id.0) {
log::trace!("Found valid receiver element");
let real_el = unsafe { &**el };
log::debug!("looking for listener on {:?}", real_el);
if let VNode::Element(real_el) = real_el {
for listener in real_el.listeners.borrow().iter() {
if listener.event == event.name {
log::trace!("Found valid receiver event");
log::debug!("calling listener {:?}", listener.event);
if state.canceled.get() {
// stop bubbling if canceled
break;
@@ -421,6 +448,7 @@ pub struct ScopeState {
pub(crate) container: ElementId,
pub(crate) our_arena_idx: ScopeId,
pub(crate) height: u32,
pub(crate) fnptr: ComponentPtr,
// todo: subtrees
pub(crate) is_subtree_root: Cell<bool>,
@@ -449,43 +477,6 @@ pub struct SelfReferentialItems<'a> {
// Public methods exposed to libraries and components
impl ScopeState {
fn new(
height: u32,
container: ElementId,
our_arena_idx: ScopeId,
parent_scope: Option<*mut ScopeState>,
vcomp: Box<dyn AnyProps>,
tasks: Rc<TaskQueue>,
(node_capacity, hook_capacity): (usize, usize),
) -> Self {
ScopeState {
container,
our_arena_idx,
parent_scope,
height,
props: RefCell::new(Some(vcomp)),
frames: [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)],
// todo: subtrees
subtree: Cell::new(0),
is_subtree_root: Cell::new(false),
generation: 0.into(),
tasks,
shared_contexts: Default::default(),
items: RefCell::new(SelfReferentialItems {
listeners: Default::default(),
borrowed_props: Default::default(),
}),
hook_arena: Bump::new(),
hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)),
hook_idx: Default::default(),
}
}
/// Get the subtree ID that this scope belongs to.
///
/// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
+57 -50
View File
@@ -2,6 +2,7 @@
//!
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
use crate::diff::DiffState;
use crate::innerlude::*;
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures_util::{future::poll_fn, StreamExt};
@@ -212,7 +213,7 @@ impl VirtualDom {
let scopes = ScopeArena::new(channel.0.clone());
scopes.new_with_key(
root as *const _,
root as ComponentPtr,
Box::new(VComponentProps {
props: root_props,
memo: |_a, _b| unreachable!("memo on root will neve be run"),
@@ -448,6 +449,7 @@ impl VirtualDom {
/// apply_mutations(mutations);
/// }
/// ```
#[allow(unused)]
pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec<Mutations> {
let mut committed_mutations = vec![];
@@ -467,36 +469,41 @@ impl VirtualDom {
h1.cmp(&h2).reverse()
});
log::debug!("dirty_scopes: {:?}", self.dirty_scopes);
if let Some(scopeid) = self.dirty_scopes.pop() {
if !ran_scopes.contains(&scopeid) {
ran_scopes.insert(scopeid);
self.scopes.run_scope(scopeid);
let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid));
diff_state.stack.push(DiffInstruction::Diff { new, old });
diff_state.stack.scope_stack.push(scopeid);
diff_state.diff_scope(scopeid);
let scope = scopes.get_scope(scopeid).unwrap();
diff_state.stack.element_stack.push(scope.container);
let DiffState { mutations, .. } = diff_state;
log::debug!("succesffuly resolved scopes {:?}", mutations.dirty_scopes);
for scope in &mutations.dirty_scopes {
self.dirty_scopes.remove(scope);
}
committed_mutations.push(mutations);
// todo: pause the diff machine
// if diff_state.work(&mut deadline) {
// let DiffState { mutations, .. } = diff_state;
// for scope in &mutations.dirty_scopes {
// self.dirty_scopes.remove(scope);
// }
// committed_mutations.push(mutations);
// } else {
// // leave the work in an incomplete state
// //
// // todo: we should store the edits and re-apply them later
// // for now, we just dump the work completely (threadsafe)
// return committed_mutations;
// }
}
}
if diff_state.work(&mut deadline) {
let DiffState { mutations, .. } = diff_state;
for scope in &mutations.dirty_scopes {
self.dirty_scopes.remove(scope);
}
committed_mutations.push(mutations);
} else {
// leave the work in an incomplete state
//
// todo: we should store the edits and re-apply them later
// for now, we just dump the work completely (threadsafe)
return committed_mutations;
}
}
committed_mutations
@@ -524,13 +531,15 @@ impl VirtualDom {
let mut diff_state = DiffState::new(&self.scopes);
self.scopes.run_scope(scope_id);
diff_state
.stack
.create_node(self.scopes.fin_head(scope_id), MountType::Append);
diff_state.stack.element_stack.push(ElementId(0));
diff_state.stack.scope_stack.push(scope_id);
diff_state.work(|| false);
diff_state.element_stack.push(ElementId(0));
diff_state.scope_stack.push(scope_id);
let node = self.scopes.fin_head(scope_id);
let created = diff_state.create_node(node);
diff_state.mutations.append_children(created as u32);
self.dirty_scopes.clear();
assert!(self.dirty_scopes.is_empty());
@@ -577,12 +586,11 @@ impl VirtualDom {
);
diff_machine.force_diff = true;
diff_machine.stack.push(DiffInstruction::Diff { old, new });
diff_machine.stack.scope_stack.push(scope_id);
diff_machine.scope_stack.push(scope_id);
let scope = diff_machine.scopes.get_scope(scope_id).unwrap();
diff_machine.stack.element_stack.push(scope.container);
diff_machine.work(|| false);
diff_machine.element_stack.push(scope.container);
diff_machine.diff_node(old, new);
diff_machine.mutations
}
@@ -621,10 +629,10 @@ impl VirtualDom {
/// ```
pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> {
let mut machine = DiffState::new(&self.scopes);
machine.stack.push(DiffInstruction::Diff { new, old });
machine.stack.element_stack.push(ElementId(0));
machine.stack.scope_stack.push(ScopeId(0));
machine.work(|| false);
machine.element_stack.push(ElementId(0));
machine.scope_stack.push(ScopeId(0));
machine.diff_node(old, new);
machine.mutations
}
@@ -643,11 +651,11 @@ impl VirtualDom {
/// ```
pub fn create_vnodes<'a>(&'a self, nodes: LazyNodes<'a, '_>) -> Mutations<'a> {
let mut machine = DiffState::new(&self.scopes);
machine.stack.element_stack.push(ElementId(0));
machine
.stack
.create_node(self.render_vnodes(nodes), MountType::Append);
machine.work(|| false);
machine.scope_stack.push(ScopeId(0));
machine.element_stack.push(ElementId(0));
let node = self.render_vnodes(nodes);
let created = machine.create_node(node);
machine.mutations.append_children(created as u32);
machine.mutations
}
@@ -672,16 +680,15 @@ impl VirtualDom {
let (old, new) = (self.render_vnodes(left), self.render_vnodes(right));
let mut create = DiffState::new(&self.scopes);
create.stack.scope_stack.push(ScopeId(0));
create.stack.element_stack.push(ElementId(0));
create.stack.create_node(old, MountType::Append);
create.work(|| false);
create.scope_stack.push(ScopeId(0));
create.element_stack.push(ElementId(0));
let created = create.create_node(old);
create.mutations.append_children(created as u32);
let mut edit = DiffState::new(&self.scopes);
edit.stack.scope_stack.push(ScopeId(0));
edit.stack.element_stack.push(ElementId(0));
edit.stack.push(DiffInstruction::Diff { old, new });
edit.work(|| false);
edit.scope_stack.push(ScopeId(0));
edit.element_stack.push(ElementId(0));
edit.diff_node(old, new);
(create.mutations, edit.mutations)
}
+1
View File
@@ -0,0 +1 @@
struct_lit_width = 80
+28 -112
View File
@@ -36,18 +36,9 @@ fn test_original_diff() {
assert_eq!(
mutations.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateElement {
root: 2,
tag: "div"
},
CreateTextNode {
root: 3,
text: "Hello, world!"
},
CreateElement { root: 1, tag: "div" },
CreateElement { root: 2, tag: "div" },
CreateTextNode { root: 3, text: "Hello, world!" },
AppendChildren { many: 1 },
AppendChildren { many: 1 },
AppendChildren { many: 1 },
@@ -81,34 +72,13 @@ fn create() {
assert_eq!(
mutations.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateElement {
root: 2,
tag: "div"
},
CreateTextNode {
root: 3,
text: "Hello, world!"
},
CreateElement {
root: 4,
tag: "div"
},
CreateElement {
root: 5,
tag: "div"
},
CreateTextNode {
root: 6,
text: "hello"
},
CreateTextNode {
root: 7,
text: "world"
},
CreateElement { root: 1, tag: "div" },
CreateElement { root: 2, tag: "div" },
CreateTextNode { root: 3, text: "Hello, world!" },
CreateElement { root: 4, tag: "div" },
CreateElement { root: 5, tag: "div" },
CreateTextNode { root: 6, text: "hello" },
CreateTextNode { root: 7, text: "world" },
AppendChildren { many: 2 },
AppendChildren { many: 1 },
AppendChildren { many: 2 },
@@ -135,32 +105,14 @@ fn create_list() {
assert_eq!(
mutations.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateTextNode {
root: 2,
text: "hello"
},
CreateElement { root: 1, tag: "div" },
CreateTextNode { root: 2, text: "hello" },
AppendChildren { many: 1 },
CreateElement {
root: 3,
tag: "div"
},
CreateTextNode {
root: 4,
text: "hello"
},
CreateElement { root: 3, tag: "div" },
CreateTextNode { root: 4, text: "hello" },
AppendChildren { many: 1 },
CreateElement {
root: 5,
tag: "div"
},
CreateTextNode {
root: 6,
text: "hello"
},
CreateElement { root: 5, tag: "div" },
CreateTextNode { root: 6, text: "hello" },
AppendChildren { many: 1 },
AppendChildren { many: 3 },
]
@@ -185,22 +137,10 @@ fn create_simple() {
assert_eq!(
mutations.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateElement {
root: 2,
tag: "div"
},
CreateElement {
root: 3,
tag: "div"
},
CreateElement {
root: 4,
tag: "div"
},
CreateElement { root: 1, tag: "div" },
CreateElement { root: 2, tag: "div" },
CreateElement { root: 3, tag: "div" },
CreateElement { root: 4, tag: "div" },
AppendChildren { many: 4 },
]
);
@@ -235,36 +175,18 @@ fn create_components() {
mutations.edits,
[
CreateElement { root: 1, tag: "h1" },
CreateElement {
root: 2,
tag: "div"
},
CreateTextNode {
root: 3,
text: "abc1"
},
CreateElement { root: 2, tag: "div" },
CreateTextNode { root: 3, text: "abc1" },
AppendChildren { many: 1 },
CreateElement { root: 4, tag: "p" },
CreateElement { root: 5, tag: "h1" },
CreateElement {
root: 6,
tag: "div"
},
CreateTextNode {
root: 7,
text: "abc2"
},
CreateElement { root: 6, tag: "div" },
CreateTextNode { root: 7, text: "abc2" },
AppendChildren { many: 1 },
CreateElement { root: 8, tag: "p" },
CreateElement { root: 9, tag: "h1" },
CreateElement {
root: 10,
tag: "div"
},
CreateTextNode {
root: 11,
text: "abc3"
},
CreateElement { root: 10, tag: "div" },
CreateTextNode { root: 11, text: "abc3" },
AppendChildren { many: 1 },
CreateElement { root: 12, tag: "p" },
AppendChildren { many: 9 },
@@ -285,14 +207,8 @@ fn anchors() {
assert_eq!(
mutations.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateTextNode {
root: 2,
text: "hello"
},
CreateElement { root: 1, tag: "div" },
CreateTextNode { root: 2, text: "hello" },
AppendChildren { many: 1 },
CreatePlaceholder { root: 3 },
AppendChildren { many: 2 },
+60 -196
View File
@@ -1,4 +1,5 @@
#![allow(unused, non_upper_case_globals)]
//! Diffing Tests
//!
//! These tests only verify that the diffing algorithm works properly for single components.
@@ -31,26 +32,14 @@ fn html_and_rsx_generate_the_same_output() {
assert_eq!(
create.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateTextNode {
root: 2,
text: "Hello world"
},
CreateElement { root: 1, tag: "div" },
CreateTextNode { root: 2, text: "Hello world" },
AppendChildren { many: 1 },
AppendChildren { many: 1 },
]
);
assert_eq!(
change.edits,
[SetText {
text: "Goodbye world",
root: 2
},]
);
assert_eq!(change.edits, [SetText { text: "Goodbye world", root: 2 },]);
}
/// Should result in 3 elements on the stack
@@ -67,32 +56,14 @@ fn fragments_create_properly() {
assert_eq!(
create.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateTextNode {
root: 2,
text: "Hello a"
},
CreateElement { root: 1, tag: "div" },
CreateTextNode { root: 2, text: "Hello a" },
AppendChildren { many: 1 },
CreateElement {
root: 3,
tag: "div"
},
CreateTextNode {
root: 4,
text: "Hello b"
},
CreateElement { root: 3, tag: "div" },
CreateTextNode { root: 4, text: "Hello b" },
AppendChildren { many: 1 },
CreateElement {
root: 5,
tag: "div"
},
CreateTextNode {
root: 6,
text: "Hello c"
},
CreateElement { root: 5, tag: "div" },
CreateTextNode { root: 6, text: "Hello c" },
AppendChildren { many: 1 },
AppendChildren { many: 3 },
]
@@ -116,10 +87,7 @@ fn empty_fragments_create_anchors() {
assert_eq!(
change.edits,
[
CreateElement {
root: 2,
tag: "div"
},
CreateElement { root: 2, tag: "div" },
ReplaceWith { m: 1, root: 1 }
]
);
@@ -138,29 +106,15 @@ fn empty_fragments_create_many_anchors() {
create.edits,
[CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }]
);
assert_eq!(
change.edits,
[
CreateElement {
root: 2,
tag: "div"
},
CreateElement {
root: 3,
tag: "div"
},
CreateElement {
root: 4,
tag: "div"
},
CreateElement {
root: 5,
tag: "div"
},
CreateElement {
root: 6,
tag: "div"
},
CreateElement { root: 2, tag: "div" },
CreateElement { root: 3, tag: "div" },
CreateElement { root: 4, tag: "div" },
CreateElement { root: 5, tag: "div" },
CreateElement { root: 6, tag: "div" },
ReplaceWith { m: 5, root: 1 }
]
);
@@ -188,32 +142,14 @@ fn empty_fragments_create_anchors_with_many_children() {
assert_eq!(
change.edits,
[
CreateElement {
tag: "div",
root: 2,
},
CreateTextNode {
text: "hello: 0",
root: 3
},
CreateElement { tag: "div", root: 2 },
CreateTextNode { text: "hello: 0", root: 3 },
AppendChildren { many: 1 },
CreateElement {
tag: "div",
root: 4,
},
CreateTextNode {
text: "hello: 1",
root: 5
},
CreateElement { tag: "div", root: 4 },
CreateTextNode { text: "hello: 1", root: 5 },
AppendChildren { many: 1 },
CreateElement {
tag: "div",
root: 6,
},
CreateTextNode {
text: "hello: 2",
root: 7
},
CreateElement { tag: "div", root: 6 },
CreateTextNode { text: "hello: 2", root: 7 },
AppendChildren { many: 1 },
ReplaceWith { root: 1, m: 3 }
]
@@ -236,23 +172,11 @@ fn many_items_become_fragment() {
assert_eq!(
create.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateTextNode {
text: "hello",
root: 2
},
CreateElement { root: 1, tag: "div" },
CreateTextNode { text: "hello", root: 2 },
AppendChildren { many: 1 },
CreateElement {
root: 3,
tag: "div"
},
CreateTextNode {
text: "hello",
root: 4
},
CreateElement { root: 3, tag: "div" },
CreateTextNode { text: "hello", root: 4 },
AppendChildren { many: 1 },
AppendChildren { many: 2 },
]
@@ -315,7 +239,7 @@ fn two_fragments_with_differrent_elements_are_differet() {
// replace the divs with new h1s
CreateElement { tag: "h1", root: 7 },
ReplaceWith { root: 1, m: 1 },
CreateElement { tag: "h1", root: 8 },
CreateElement { tag: "h1", root: 1 }, // notice how 1 gets re-used
ReplaceWith { root: 2, m: 1 },
]
);
@@ -339,39 +263,27 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() {
assert_eq!(
create.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateElement {
root: 2,
tag: "div"
},
CreateElement {
root: 3,
tag: "div"
},
CreateElement {
root: 4,
tag: "div"
},
CreateElement {
root: 5,
tag: "div"
},
CreateElement { root: 1, tag: "div" },
CreateElement { root: 2, tag: "div" },
CreateElement { root: 3, tag: "div" },
CreateElement { root: 4, tag: "div" },
CreateElement { root: 5, tag: "div" },
CreateElement { root: 6, tag: "p" },
AppendChildren { many: 6 },
]
);
// note: key reuse is always the last node that got used
// slab maintains a linked list, essentially
assert_eq!(
change.edits,
[
Remove { root: 3 },
Remove { root: 4 },
Remove { root: 5 },
CreateElement { root: 7, tag: "h1" },
ReplaceWith { root: 1, m: 1 },
CreateElement { root: 8, tag: "h1" },
CreateElement { root: 5, tag: "h1" }, // 3 gets reused
ReplaceWith { root: 1, m: 1 }, // 1 gets deleted
CreateElement { root: 1, tag: "h1" }, // 1 gets reused
ReplaceWith { root: 2, m: 1 },
]
);
@@ -395,14 +307,8 @@ fn two_fragments_with_same_elements_are_differet() {
assert_eq!(
create.edits,
[
CreateElement {
root: 1,
tag: "div"
},
CreateElement {
root: 2,
tag: "div"
},
CreateElement { root: 1, tag: "div" },
CreateElement { root: 2, tag: "div" },
CreateElement { root: 3, tag: "p" },
AppendChildren { many: 3 },
]
@@ -410,18 +316,9 @@ fn two_fragments_with_same_elements_are_differet() {
assert_eq!(
change.edits,
[
CreateElement {
root: 4,
tag: "div"
},
CreateElement {
root: 5,
tag: "div"
},
CreateElement {
root: 6,
tag: "div"
},
CreateElement { root: 4, tag: "div" },
CreateElement { root: 5, tag: "div" },
CreateElement { root: 6, tag: "div" },
InsertAfter { root: 2, n: 3 },
]
);
@@ -628,14 +525,8 @@ fn keyed_diffing_additions() {
assert_eq!(
change.edits,
[
CreateElement {
root: 6,
tag: "div"
},
CreateElement {
root: 7,
tag: "div"
},
CreateElement { root: 6, tag: "div" },
CreateElement { root: 7, tag: "div" },
InsertAfter { n: 2, root: 5 }
]
);
@@ -663,14 +554,8 @@ fn keyed_diffing_additions_and_moves_on_ends() {
change.edits,
[
// create 11, 12
CreateElement {
tag: "div",
root: 5
},
CreateElement {
tag: "div",
root: 6
},
CreateElement { tag: "div", root: 5 },
CreateElement { tag: "div", root: 6 },
InsertAfter { root: 3, n: 2 },
// move 7 to the front
PushRoot { root: 4 },
@@ -684,13 +569,13 @@ fn keyed_diffing_additions_and_moves_in_middle() {
let dom = new_dom();
let left = rsx!({
[/**/ 4, 5, 6, 7 /**/].iter().map(|f| {
[/**/ 1, 2, 3, 4 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
let right = rsx!({
[/**/ 7, 4, 13, 17, 5, 11, 12, 6 /**/].iter().map(|f| {
[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
})
});
@@ -701,26 +586,14 @@ fn keyed_diffing_additions_and_moves_in_middle() {
assert_eq!(
change.edits,
[
// create 13, 17
CreateElement {
tag: "div",
root: 5
},
CreateElement {
tag: "div",
root: 6
},
InsertBefore { root: 2, n: 2 },
// create 11, 12
CreateElement {
tag: "div",
root: 7
},
CreateElement {
tag: "div",
root: 8
},
// create 5, 6
CreateElement { tag: "div", root: 5 },
CreateElement { tag: "div", root: 6 },
InsertBefore { root: 3, n: 2 },
// create 7, 8
CreateElement { tag: "div", root: 7 },
CreateElement { tag: "div", root: 8 },
InsertBefore { root: 2, n: 2 },
// move 7
PushRoot { root: 4 },
InsertBefore { root: 1, n: 1 }
@@ -756,16 +629,10 @@ fn controlled_keyed_diffing_out_of_order() {
// remove 7
// create 9 and insert before 6
CreateElement {
root: 5,
tag: "div"
},
CreateElement { root: 5, tag: "div" },
InsertBefore { n: 1, root: 3 },
// create 0 and insert before 5
CreateElement {
root: 6,
tag: "div"
},
CreateElement { root: 6, tag: "div" },
InsertBefore { n: 1, root: 2 },
]
);
@@ -792,10 +659,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
assert_eq!(
changes.edits,
[
CreateElement {
root: 6,
tag: "div"
},
CreateElement { root: 6, tag: "div" },
InsertBefore { n: 1, root: 3 },
PushRoot { root: 4 },
InsertBefore { n: 1, root: 1 },
+4 -16
View File
@@ -42,14 +42,8 @@ fn test_early_abort() {
assert_eq!(
edits.edits,
[
CreateElement {
tag: "div",
root: 1,
},
CreateTextNode {
text: "Hello, world!",
root: 2,
},
CreateElement { tag: "div", root: 1 },
CreateTextNode { text: "Hello, world!", root: 2 },
AppendChildren { many: 1 },
AppendChildren { many: 1 },
]
@@ -65,14 +59,8 @@ fn test_early_abort() {
assert_eq!(
edits.edits,
[
CreateElement {
tag: "div",
root: 2,
},
CreateTextNode {
text: "Hello, world!",
root: 4,
},
CreateElement { tag: "div", root: 1 }, // keys get reused
CreateTextNode { text: "Hello, world!", root: 2 }, // keys get reused
AppendChildren { many: 1 },
ReplaceWith { root: 3, m: 1 },
]
+39 -81
View File
@@ -1,4 +1,5 @@
#![allow(unused, non_upper_case_globals)]
#![allow(non_snake_case)]
//! Tests for the lifecycle of components.
use dioxus::prelude::*;
@@ -27,12 +28,7 @@ fn manual_diffing() {
};
let value = Arc::new(Mutex::new("Hello"));
let mut dom = VirtualDom::new_with_props(
App,
AppProps {
value: value.clone(),
},
);
let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone() });
let _ = dom.rebuild();
@@ -45,7 +41,7 @@ fn manual_diffing() {
#[test]
fn events_generate() {
static App: Component = |cx| {
fn app(cx: Scope) -> Element {
let count = cx.use_hook(|_| 0);
let inner = match *count {
@@ -66,7 +62,7 @@ fn events_generate() {
cx.render(inner)
};
let mut dom = VirtualDom::new(App);
let mut dom = VirtualDom::new(app);
let mut channel = dom.get_scheduler_channel();
assert!(dom.has_work());
@@ -74,28 +70,12 @@ fn events_generate() {
assert_eq!(
edits.edits,
[
CreateElement {
tag: "div",
root: 1,
},
NewEventListener {
event_name: "click",
scope: ScopeId(0),
root: 1,
},
CreateElement {
tag: "div",
root: 2,
},
CreateTextNode {
text: "nested",
root: 3,
},
CreateElement { tag: "div", root: 1 },
NewEventListener { event_name: "click", scope: ScopeId(0), root: 1 },
CreateElement { tag: "div", root: 2 },
CreateTextNode { text: "nested", root: 3 },
AppendChildren { many: 1 },
CreateTextNode {
text: "Click me!",
root: 4,
},
CreateTextNode { text: "Click me!", root: 4 },
AppendChildren { many: 2 },
AppendChildren { many: 1 },
]
@@ -104,7 +84,7 @@ fn events_generate() {
#[test]
fn components_generate() {
static App: Component = |cx| {
fn app(cx: Scope) -> Element {
let render_phase = cx.use_hook(|_| 0);
*render_phase += 1;
@@ -121,106 +101,84 @@ fn components_generate() {
})
};
static Child: Component = |cx| {
fn Child(cx: Scope) -> Element {
log::debug!("Running child");
cx.render(rsx! {
h1 {}
})
};
}
let mut dom = VirtualDom::new(App);
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild();
assert_eq!(
edits.edits,
[
CreateTextNode {
text: "Text0",
root: 1,
},
CreateTextNode { text: "Text0", root: 1 },
AppendChildren { many: 1 },
]
);
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
dom.hard_diff(ScopeId(0)).edits,
[
CreateElement {
tag: "div",
root: 2,
},
CreateElement { tag: "div", root: 2 },
ReplaceWith { root: 1, m: 1 },
]
);
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
dom.hard_diff(ScopeId(0)).edits,
[
CreateTextNode {
text: "Text2",
root: 3,
},
CreateTextNode { text: "Text2", root: 1 },
ReplaceWith { root: 2, m: 1 },
]
);
let edits = dom.hard_diff(ScopeId(0));
// child {}
assert_eq!(
edits.edits,
dom.hard_diff(ScopeId(0)).edits,
[
CreateElement { tag: "h1", root: 4 },
ReplaceWith { root: 3, m: 1 },
CreateElement { tag: "h1", root: 2 },
ReplaceWith { root: 1, m: 1 },
]
);
let edits = dom.hard_diff(ScopeId(0));
// placeholder
assert_eq!(
edits.edits,
[CreatePlaceholder { root: 5 }, ReplaceWith { root: 4, m: 1 },]
dom.hard_diff(ScopeId(0)).edits,
[CreatePlaceholder { root: 1 }, ReplaceWith { root: 2, m: 1 },]
);
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
dom.hard_diff(ScopeId(0)).edits,
[
CreateTextNode {
text: "text 3",
root: 6,
},
ReplaceWith { root: 5, m: 1 },
CreateTextNode { text: "text 3", root: 2 },
ReplaceWith { root: 1, m: 1 },
]
);
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
dom.hard_diff(ScopeId(0)).edits,
[
CreateTextNode {
text: "text 0",
root: 7,
},
CreateTextNode {
text: "text 1",
root: 8,
},
ReplaceWith { root: 6, m: 2 },
CreateTextNode { text: "text 0", root: 1 },
CreateTextNode { text: "text 1", root: 3 },
ReplaceWith { root: 2, m: 2 },
]
);
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
dom.hard_diff(ScopeId(0)).edits,
[
CreateElement { tag: "h1", root: 9 },
ReplaceWith { root: 7, m: 1 },
Remove { root: 8 },
CreateElement { tag: "h1", root: 2 },
ReplaceWith { root: 1, m: 1 },
Remove { root: 3 },
]
);
}
#[test]
fn component_swap() {
static App: Component = |cx| {
fn app(cx: Scope) -> Element {
let render_phase = cx.use_hook(|_| 0);
*render_phase += 1;
@@ -296,7 +254,7 @@ fn component_swap() {
})
};
let mut dom = VirtualDom::new(App);
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild();
dbg!(&edits);
+111 -6
View File
@@ -1,3 +1,4 @@
#![allow(non_snake_case)]
/*
Stress Miri as much as possible.
@@ -152,12 +153,7 @@ fn free_works_on_root_props() {
}
}
let mut dom = new_dom(
app,
Custom {
val: String::from("asd"),
},
);
let mut dom = new_dom(app, Custom { val: String::from("asd") });
dom.rebuild();
}
@@ -313,3 +309,112 @@ fn leak_thru_children() {
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
}
#[test]
fn test_pass_thru() {
#[inline_props]
fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element {
cx.render(rsx! {
header {
nav { children }
}
})
}
fn NavMenu(cx: Scope) -> Element {
rsx!(cx,
NavBrand {}
div {
NavStart {}
NavEnd {}
}
)
}
fn NavBrand(cx: Scope) -> Element {
rsx!(cx, div {})
}
fn NavStart(cx: Scope) -> Element {
rsx!(cx, div {})
}
fn NavEnd(cx: Scope) -> Element {
rsx!(cx, div {})
}
#[inline_props]
fn MainContainer<'a>(
cx: Scope,
nav: Element<'a>,
body: Element<'a>,
footer: Element<'a>,
) -> Element {
cx.render(rsx! {
div {
class: "columns is-mobile",
div {
class: "column is-full",
nav,
body,
footer,
}
}
})
}
fn app(cx: Scope) -> Element {
let nav = cx.render(rsx! {
NavContainer {
NavMenu {}
}
});
let body = cx.render(rsx! {
div {}
});
let footer = cx.render(rsx! {
div {}
});
cx.render(rsx! {
MainContainer {
nav: nav,
body: body,
footer: footer,
}
})
}
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
for _ in 0..40 {
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
dom.work_with_deadline(|| false);
}
}
+107
View File
@@ -0,0 +1,107 @@
#![allow(unused, non_upper_case_globals)]
#![allow(non_snake_case)]
//! Diffing Tests
//!
//! These tests only verify that the diffing algorithm works properly for single components.
//!
//! It does not validated that component lifecycles work properly. This is done in another test file.
use dioxus::{prelude::*, DomEdit, ScopeId};
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
mod test_logging;
fn new_dom() -> VirtualDom {
const IS_LOGGING_ENABLED: bool = false;
test_logging::set_up_logging(IS_LOGGING_ENABLED);
VirtualDom::new(|cx| rsx!(cx, "hi"))
}
use DomEdit::*;
/// Should push the text node onto the stack and modify it
#[test]
fn nested_passthru_creates() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
Child {
Child {
Child {
div {
"hi"
}
}
}
}
})
};
#[inline_props]
fn Child<'a>(cx: Scope, children: Element<'a>) -> Element {
cx.render(rsx! { children })
};
let mut dom = VirtualDom::new(app);
let mut channel = dom.get_scheduler_channel();
assert!(dom.has_work());
let edits = dom.rebuild();
assert_eq!(
edits.edits,
[
CreateElement { tag: "div", root: 1 },
CreateTextNode { text: "hi", root: 2 },
AppendChildren { many: 1 },
AppendChildren { many: 1 },
]
)
}
/// Should push the text node onto the stack and modify it
#[test]
fn nested_passthru_creates_add() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
Child {
"1"
Child {
"2"
Child {
"3"
div {
"hi"
}
}
}
}
})
};
#[inline_props]
fn Child<'a>(cx: Scope, children: Element<'a>) -> Element {
cx.render(rsx! {
children
})
};
let mut dom = VirtualDom::new(app);
let mut channel = dom.get_scheduler_channel();
assert!(dom.has_work());
let edits = dom.rebuild();
assert_eq!(
edits.edits,
[
CreateTextNode { text: "1", root: 1 },
CreateTextNode { text: "2", root: 2 },
CreateTextNode { text: "3", root: 3 },
CreateElement { tag: "div", root: 4 },
CreateTextNode { text: "hi", root: 5 },
AppendChildren { many: 1 },
AppendChildren { many: 4 },
]
)
}
+97 -6
View File
@@ -1,6 +1,6 @@
#![allow(unused, non_upper_case_globals)]
#![allow(unused, non_upper_case_globals, non_snake_case)]
use dioxus::{prelude::*, DomEdit, Mutations};
use dioxus::{prelude::*, DomEdit, Mutations, SchedulerMsg, ScopeId};
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
@@ -29,11 +29,102 @@ fn shared_state_test() {
assert_eq!(
edits,
[
CreateTextNode {
root: 1,
text: "Hello, world!"
},
CreateTextNode { root: 1, text: "Hello, world!" },
AppendChildren { many: 1 },
]
);
}
#[test]
fn swap_test() {
struct MySharedState(&'static str);
fn app(cx: Scope) -> Element {
let val = cx.use_hook(|_| 0);
*val += 1;
cx.provide_context(MySharedState("world!"));
let child = match *val % 2 {
0 => rsx!(
Child1 {
Child1 { }
Child2 { }
}
),
_ => rsx!(
Child2 {
Child2 { }
Child2 { }
}
),
};
cx.render(rsx!(
Router {
div { child }
}
))
}
#[inline_props]
fn Router<'a>(cx: Scope, children: Element<'a>) -> Element<'a> {
cx.render(rsx!(div { children }))
}
#[inline_props]
fn Child1<'a>(cx: Scope, children: Element<'a>) -> Element {
let shared = cx.consume_context::<MySharedState>().unwrap();
println!("Child1: {}", shared.0);
cx.render(rsx! {
div {
"{shared.0}",
children
}
})
}
#[inline_props]
fn Child2<'a>(cx: Scope, children: Element<'a>) -> Element {
let shared = cx.consume_context::<MySharedState>().unwrap();
println!("Child2: {}", shared.0);
cx.render(rsx! {
h1 {
"{shared.0}",
children
}
})
}
let mut dom = VirtualDom::new(app);
let Mutations { edits, .. } = dom.rebuild();
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
}
+2 -8
View File
@@ -68,15 +68,9 @@ fn conditional_rendering() {
mutations.edits,
[
CreateElement { root: 1, tag: "h1" },
CreateTextNode {
root: 2,
text: "hello"
},
CreateTextNode { root: 2, text: "hello" },
AppendChildren { many: 1 },
CreateElement {
root: 3,
tag: "span"
},
CreateElement { root: 3, tag: "span" },
CreateTextNode { root: 4, text: "a" },
AppendChildren { many: 1 },
CreatePlaceholder { root: 5 },
+89 -5
View File
@@ -5,6 +5,76 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Documentation
- <csr-id-a239d2ba6ac7f1f3d09de16c022ce8ca52cf0f63/> fix web doc example and use &mut for builders everywhere
### New Features
- <csr-id-430cde7068d308f2783e33d278fd0c0efa659c1b/> default asset server
- <csr-id-95e93ed0bcf6c69990f4cf3c6448b2bf5da96c36/> remove dioxus id on non-event elements
- <csr-id-eb138848ec7a8978f0ed7c717374684d2315dc03/> also hide placeholder node
- <csr-id-05331ddd8033f6997d4916179b62f4d62f832988/> wire up both desktop and web
- <csr-id-8f4aa84f1a4f2443b34d81ee42490564e168de53/> bool attr white list
- <csr-id-5bf6c96f9fed04de949403202bafcbeadb5d2030/> setup a typescript build
### Bug Fixes
- <csr-id-22308eb26a9ea48b14f5f5abb833aa90a4e3fc40/> custom protocol receiver type
- <csr-id-6bc45b1c5064a4e2b04d452c52a8167ad179691e/> clippy
- <csr-id-bad36162af764291f5a031b6233d151f61d745a4/> wry pathing
- <csr-id-be614e6535e6e13e6ff93e9c6a171c1c002e6b01/> cursor jumping and use set instead of lsit
- <csr-id-92561612c727e73356d7d36e16af39aacf02a56d/> format code
- <csr-id-2073b400df55f0c6d8bed7371b2313be6c064e6e/> check `href` null
- <csr-id-21232285d9d84168d9003969ddd254fc22951e4b/> add exclusion list
- <csr-id-327f9015481809d8e5b9e69f26202e8d66dd198e/> check `href` null
- <csr-id-8089023a6c3a54957af9c9c05c9dee6088b059ef/> prevent `submit` default
### Commit Statistics
<csr-read-only-do-not-edit/>
- 26 commits contributed to the release over the course of 11 calendar days.
- 17 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
### Commit Details
<csr-read-only-do-not-edit/>
<details><summary>view details</summary>
* **Uncategorized**
- custom protocol receiver type ([`22308eb`](https://github.comgit//DioxusLabs/dioxus/commit/22308eb26a9ea48b14f5f5abb833aa90a4e3fc40))
- default asset server ([`430cde7`](https://github.comgit//DioxusLabs/dioxus/commit/430cde7068d308f2783e33d278fd0c0efa659c1b))
- fix web doc example and use &mut for builders everywhere ([`a239d2b`](https://github.comgit//DioxusLabs/dioxus/commit/a239d2ba6ac7f1f3d09de16c022ce8ca52cf0f63))
- Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3))
- clippy ([`6bc45b1`](https://github.comgit//DioxusLabs/dioxus/commit/6bc45b1c5064a4e2b04d452c52a8167ad179691e))
- Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401))
- wry pathing ([`bad3616`](https://github.comgit//DioxusLabs/dioxus/commit/bad36162af764291f5a031b6233d151f61d745a4))
- remove dioxus id on non-event elements ([`95e93ed`](https://github.comgit//DioxusLabs/dioxus/commit/95e93ed0bcf6c69990f4cf3c6448b2bf5da96c36))
- also hide placeholder node ([`eb13884`](https://github.comgit//DioxusLabs/dioxus/commit/eb138848ec7a8978f0ed7c717374684d2315dc03))
- drag and drop support ([`9ae981a`](https://github.comgit//DioxusLabs/dioxus/commit/9ae981a1af4b5474ce16e27e070794d59128c12a))
- feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9))
- cursor jumping and use set instead of lsit ([`be614e6`](https://github.comgit//DioxusLabs/dioxus/commit/be614e6535e6e13e6ff93e9c6a171c1c002e6b01))
- Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f))
- feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3))
- Merge pull request #101 from alexkirsz/ci ([`29bf424`](https://github.comgit//DioxusLabs/dioxus/commit/29bf424b0976b95ff645bb128d0e758cf0186614))
- Merge pull request #139 from DioxusLabs/jk/provide-context-any ([`70f2ef4`](https://github.comgit//DioxusLabs/dioxus/commit/70f2ef43db5b6737bd9bcbfc1aa21c834ce4b395))
- Merge branch 'master' into jk/unify ([`824defa`](https://github.comgit//DioxusLabs/dioxus/commit/824defa2dbcc16d66588b3976699d89b65a8a068))
- wire up both desktop and web ([`05331dd`](https://github.comgit//DioxusLabs/dioxus/commit/05331ddd8033f6997d4916179b62f4d62f832988))
- format code ([`9256161`](https://github.comgit//DioxusLabs/dioxus/commit/92561612c727e73356d7d36e16af39aacf02a56d))
- Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11))
- bool attr white list ([`8f4aa84`](https://github.comgit//DioxusLabs/dioxus/commit/8f4aa84f1a4f2443b34d81ee42490564e168de53))
- setup a typescript build ([`5bf6c96`](https://github.comgit//DioxusLabs/dioxus/commit/5bf6c96f9fed04de949403202bafcbeadb5d2030))
- check `href` null ([`2073b40`](https://github.comgit//DioxusLabs/dioxus/commit/2073b400df55f0c6d8bed7371b2313be6c064e6e))
- add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b))
- check `href` null ([`327f901`](https://github.comgit//DioxusLabs/dioxus/commit/327f9015481809d8e5b9e69f26202e8d66dd198e))
- prevent `submit` default ([`8089023`](https://github.comgit//DioxusLabs/dioxus/commit/8089023a6c3a54957af9c9c05c9dee6088b059ef))
</details>
## v0.1.5 (2022-01-08)
### Documentation
@@ -37,6 +107,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- <csr-id-f782e142118fb7acf1b88a0f3fbb03e4a5e3e91e/> omg what a dumb mistake
- <csr-id-4a0068f09918adbc299150edcf777f342ced0dd3/> bless up, no more segfaults
- <csr-id-7dfe89c9581f45a445f17f9fe4bb94e61f67e971/> wire up event delegator for webview
- <csr-id-46fd6ac3450ca5ebf9aecb2d59a5a92b2a68bdd0/> link open in browser
- <csr-id-f006f50317f4b75fac353bc988db057a281ba7f8/> move `rpc` to handler
- <csr-id-9e04ce5342850d2e0a01dde169807d6f6eb16566/> `open_browser` bool attribute
- <csr-id-c737c424b05ad8453e8770a14a0d210fb0c7c2fe/> link open in browser
- <csr-id-a0f60152bc7e5866f114ed469809ce8be70d17d4/> link open in browser
### Bug Fixes
@@ -48,13 +123,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- <csr-id-a33f7701fcf5f917fea8719253650b5ad92554fd/> tags
- <csr-id-601078f9cf78a58d7502a377676ac94f3cf037bf/> desktop and mobile
- <csr-id-ba9e1dbb8fa24048a6c9ccef8a8722688226a845/> messed up how lifetimes worked, need to render once per component
- <csr-id-62b637f8b0eaf616c49461fa23b9251a79abc147/> error pattern
- <csr-id-5233ee97d9314f7f0e0bdf05c56d2a9e4201a596/> format code
### Commit Statistics
<csr-read-only-do-not-edit/>
- 91 commits contributed to the release over the course of 151 calendar days.
- 78 commits where understood as [conventional](https://www.conventionalcommits.org).
- 98 commits contributed to the release over the course of 151 calendar days.
- 84 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
### Commit Details
@@ -64,11 +141,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<details><summary>view details</summary>
* **Uncategorized**
- Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111))
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b))
- Release dioxus-desktop v0.1.5 ([`cd0dcac`](https://github.comgit//DioxusLabs/dioxus/commit/cd0dcacaf2862f26d29acb21d98f75d41b940e3f))
- handle bool attrs properly ([`8d685f4`](https://github.comgit//DioxusLabs/dioxus/commit/8d685f40b7e0ef6521c60310d8687291e9b9c48a))
- link open in browser ([`46fd6ac`](https://github.comgit//DioxusLabs/dioxus/commit/46fd6ac3450ca5ebf9aecb2d59a5a92b2a68bdd0))
- Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111))
- error pattern ([`62b637f`](https://github.comgit//DioxusLabs/dioxus/commit/62b637f8b0eaf616c49461fa23b9251a79abc147))
- move `rpc` to handler ([`f006f50`](https://github.comgit//DioxusLabs/dioxus/commit/f006f50317f4b75fac353bc988db057a281ba7f8))
- `open_browser` bool attribute ([`9e04ce5`](https://github.comgit//DioxusLabs/dioxus/commit/9e04ce5342850d2e0a01dde169807d6f6eb16566))
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- format code ([`5233ee9`](https://github.comgit//DioxusLabs/dioxus/commit/5233ee97d9314f7f0e0bdf05c56d2a9e4201a596))
- link open in browser ([`c737c42`](https://github.comgit//DioxusLabs/dioxus/commit/c737c424b05ad8453e8770a14a0d210fb0c7c2fe))
- Merge pull request #89 from DioxusLabs/jk/simplify-example-run ([`8b6aa8b`](https://github.comgit//DioxusLabs/dioxus/commit/8b6aa8b880b6cb5c95e0c0743aad4e4e74388e05))
- link open in browser ([`a0f6015`](https://github.comgit//DioxusLabs/dioxus/commit/a0f60152bc7e5866f114ed469809ce8be70d17d4))
- Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee))
- Merge pull request #80 from DioxusLabs/jk/router2dotoh ([`cdc2d8e`](https://github.comgit//DioxusLabs/dioxus/commit/cdc2d8ec6d123245c2ea5f6d10af02b6a6833994))
- clear warnigns ([`175a6a1`](https://github.comgit//DioxusLabs/dioxus/commit/175a6a199c6738d8d0c7646ba0ec3fc4406c6535))
+22 -13
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-desktop"
version = "0.1.5"
version = "0.1.6"
authors = ["Jonathan Kelley"]
edition = "2018"
description = "Dioxus VirtualDOM renderer for a remote webview instance"
@@ -12,29 +12,38 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-core = { path = "../core", version ="^0.1.7", features = ["serialize"] }
argh = "0.1.4"
serde = "1.0.120"
serde_json = "1.0.61"
thiserror = "1.0.23"
log = "0.4.13"
html-escape = "0.2.9"
wry = "0.12.2"
futures-channel = "0.3"
tokio = { version = "1.12.0", features = [
dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] }
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.6"}
dioxus-html = { path = "../html", features = ["serialize"], version ="^0.1.4"}
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" }
[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
-34
View File
@@ -1,34 +0,0 @@
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
use std::time::Duration;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
let count = use_state(&cx, || 0);
use_future(&cx, || {
let count = count.for_async();
async move {
loop {
tokio::time::sleep(Duration::from_millis(1000)).await;
*count.modify() += 1;
}
}
});
cx.render(rsx! {
div {
h1 { "High-Five counter: {count}" }
button {
onclick: move |_| count.set(0),
"Click me!"
}
}
})
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

+50 -7
View File
@@ -1,3 +1,4 @@
use wry::application::window::Icon;
use wry::{
application::{
event_loop::EventLoop,
@@ -11,14 +12,14 @@ 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 type WryProtocl = (
pub(crate) type WryProtocol = (
String,
Box<dyn Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static>,
);
@@ -31,7 +32,7 @@ impl DesktopConfig {
Self {
event_handler: None,
window,
protocos: Vec::new(),
protocols: Vec::new(),
file_drop_handler: None,
pre_rendered: None,
}
@@ -71,11 +72,25 @@ impl DesktopConfig {
self
}
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
pub fn with_custom_protocol<F>(&mut self, name: String, handler: F) -> &mut Self
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 +100,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())
// }
+107
View File
@@ -0,0 +1,107 @@
use crate::desktop_context::DesktopContext;
use crate::user_window_events::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();
}
}
}
}
+107 -89
View File
@@ -1,100 +1,118 @@
use std::cell::RefCell;
use std::rc::Rc;
use dioxus::prelude::Scope;
use dioxus_core as dioxus;
use dioxus_core::{Context, Element, LazyNodes, NodeFactory, Properties};
use dioxus_core_macro::Props;
use dioxus_core::ScopeState;
use wry::application::event_loop::EventLoopProxy;
/*
This module provides a set of Dioxus components to easily manage windows, tabs, etc.
use crate::user_window_events::UserWindowEvent;
use UserWindowEvent::*;
Windows can be created anywhere in the tree, making them very flexible for things like modals, etc.
type ProxyType = EventLoopProxy<UserWindowEvent>;
*/
pub struct DesktopContext {}
/// Desktop-Window handle api context
///
/// you can use this context control some window event
///
/// you can use `cx.consume_context::<DesktopContext>` to get this context
///
/// ```rust
/// let desktop = cx.consume_context::<DesktopContext>().unwrap();
/// ```
#[derive(Clone)]
pub struct DesktopContext {
proxy: ProxyType,
}
impl DesktopContext {
fn add_window(&mut self) {
//
pub(crate) fn new(proxy: ProxyType) -> Self {
Self { proxy }
}
fn close_window(&mut self) {
//
/// trigger the drag-window event
///
/// Moves the window with the left mouse button until the button is released.
///
/// you need use it in `onmousedown` event:
/// ```rust
/// onmousedown: move |_| { desktop.drag_window(); }
/// ```
pub fn drag(&self) {
let _ = self.proxy.send_event(DragWindow);
}
/// set window minimize state
pub fn set_minimized(&self, minimized: bool) {
let _ = self.proxy.send_event(Minimize(minimized));
}
/// set window maximize state
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(CloseWindow);
}
/// set window to focus
pub fn focus(&self) {
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);
}
}
enum WindowHandlers {
Resized(Box<dyn Fn()>),
Moved(Box<dyn Fn()>),
CloseRequested(Box<dyn Fn()>),
Destroyed(Box<dyn Fn()>),
DroppedFile(Box<dyn Fn()>),
HoveredFile(Box<dyn Fn()>),
HoverFileCancelled(Box<dyn Fn()>),
ReceivedTimeText(Box<dyn Fn()>),
Focused(Box<dyn Fn()>),
}
#[derive(Props)]
pub struct WebviewWindowProps<'a> {
onclose: &'a dyn FnMut(()),
onopen: &'a dyn FnMut(()),
/// focuse me
onfocused: &'a dyn FnMut(()),
children: Element,
}
/// A handle to a
///
///
///
///
///
///
///
///
///
pub fn WebviewWindow(cx: Scope<WebviewWindowProps>) -> Element {
let dtcx = cx.consume_state::<RefCell<DesktopContext>>()?;
cx.use_hook(|_| {});
// render the children directly
todo!()
// cx.render(LazyNodes::new(move |f: NodeFactory| {
// f.fragment_from_iter(cx.children())
// }))
}
pub struct WindowHandle {}
/// Get a handle to the current window from inside a component
pub fn use_current_window(cx: Scope) -> Option<WindowHandle> {
todo!()
}
#[test]
fn syntax_works() {
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_core_macro::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
static App: Component = |cx| {
cx.render(rsx! {
// left window
WebviewWindow {
onclose: move |evt| {}
onopen: move |evt| {}
onfocused: move |evt| {}
div {
}
}
})
};
/// 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()
}
+30 -9
View File
@@ -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",
+11 -18
View File
@@ -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>
+64 -203
View File
@@ -1,78 +1,30 @@
//! 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")]
pub mod cfg;
mod controller;
pub mod desktop_context;
pub mod escape;
pub mod events;
mod protocol;
mod user_window_events;
use cfg::DesktopConfig;
use controller::DesktopController;
pub use desktop_context::use_window;
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},
window::Window,
};
pub use wry;
pub use wry::application as tao;
use wry::{
application::event_loop::EventLoopProxy,
webview::RpcRequest,
webview::{WebView, WebViewBuilder},
};
use wry::webview::WebViewBuilder;
use crate::events::trigger_from_serialized;
/// Launch the WebView and run the event loop.
///
@@ -149,7 +101,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();
@@ -174,24 +126,26 @@ pub fn launch_with_props<P: 'static + Send>(
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" => {
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(user_window_events::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) {
@@ -200,30 +154,13 @@ pub fn launch_with_props<P: 'static + Send>(
}
}
}
}
_ => {}
}
None
})
.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://", "");
if path.trim_end_matches('/') == "index.html" {
wry::http::ResponseBuilder::new()
.mimetype("text/html")
.body(include_bytes!("./index.html").to_vec())
} else if path.trim_end_matches('/') == "index.html/index.js" {
wry::http::ResponseBuilder::new()
.mimetype("text/javascript")
.body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec())
} else {
wry::http::ResponseBuilder::new()
.status(wry::http::status::StatusCode::NOT_FOUND)
.body(format!("Not found: {}", path).as_bytes().to_vec())
}
_ => (),
})
.unwrap_or_else(|| {
log::warn!("invalid IPC message received");
});
})
.with_custom_protocol(String::from("dioxus"), protocol::desktop_handler)
.with_file_drop_handler(move |window, evet| {
file_handler
.as_ref()
@@ -231,10 +168,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!(debug_assertions) {
// in debug, we are okay with the reload menu showing and dev tool
webview = webview.with_dev_tool(true);
} else {
// 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;
});
}
"#,
)
}
desktop.webviews.insert(window_id, webview.build().unwrap());
}
@@ -253,11 +212,8 @@ pub fn launch_with_props<P: 'static + Send>(
_ => {}
},
Event::UserEvent(_evt) => {
//
match _evt {
UserWindowEvent::Update => desktop.try_load_ready_webviews(),
}
Event::UserEvent(user_event) => {
user_window_events::handler(user_event, &mut desktop, control_flow)
}
Event::MainEventsCleared => {}
Event::Resumed => {}
@@ -268,98 +224,3 @@ pub fn launch_with_props<P: 'static + Send>(
}
})
}
pub enum UserWindowEvent {
Update,
}
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();
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 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");
}
}
}
+47
View File
@@ -0,0 +1,47 @@
use std::path::Path;
use wry::{
http::{status::StatusCode, Request, Response, ResponseBuilder},
Result,
};
pub(super) fn desktop_handler(request: &Request) -> 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 path_buf = Path::new(trimmed).canonicalize()?;
let cur_path = Path::new(".").canonicalize()?;
if !path_buf.starts_with(cur_path) {
return ResponseBuilder::new()
.status(StatusCode::FORBIDDEN)
.body(String::from("Forbidden").into_bytes());
}
if !path_buf.exists() {
return ResponseBuilder::new()
.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 = std::fs::read(path_buf)?;
let meta = format!("{}", mime);
ResponseBuilder::new().mimetype(&meta).body(data)
}
}
+51
View File
@@ -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,73 @@
use wry::application::event_loop::ControlFlow;
use wry::application::window::Fullscreen as WryFullscreen;
use crate::controller::DesktopController;
#[derive(Debug)]
pub(crate) 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,
}
use UserWindowEvent::*;
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(),
}
}
+14
View File
@@ -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"
+92
View File
@@ -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
+28
View File
@@ -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());
}
+25
View File
@@ -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!()
}
}
+25
View File
@@ -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());
}
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+53
View File
@@ -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();
)*}
}
+62
View File
@@ -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)
}
}
+11
View File
@@ -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())),
})
}
+36
View File
@@ -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()
}
+13
View File
@@ -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)>
})
}
+58
View File
@@ -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);
}
+103
View File
@@ -0,0 +1,103 @@
use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc};
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: Rc<dyn Fn(ScopeId)>,
}
pub struct Slot {
pub value: Rc<dyn Any>,
pub subscribers: HashSet<ScopeId>,
}
impl AtomRoot {
pub fn new(update_any: Rc<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!()
}
}
+35 -1
View File
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Bug Fixes
- <csr-id-c092bd43edf1891c427722bc9ca04e11c9359069/> use_state
- <csr-id-a8952a9ee8d8831fa911cef4e7035293c3a9f88b/> exampels
### Commit Statistics
<csr-read-only-do-not-edit/>
- 9 commits contributed to the release over the course of 21 calendar days.
- 3 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
### Commit Details
<csr-read-only-do-not-edit/>
<details><summary>view details</summary>
* **Uncategorized**
- Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a))
- Merge pull request #158 from DioxusLabs/jk/router-onchange ([`08988e1`](https://github.comgit//DioxusLabs/dioxus/commit/08988e1bfec35eadb30e50b5d677dbb36af91b9b))
- use_state ([`c092bd4`](https://github.comgit//DioxusLabs/dioxus/commit/c092bd43edf1891c427722bc9ca04e11c9359069))
- exampels ([`a8952a9`](https://github.comgit//DioxusLabs/dioxus/commit/a8952a9ee8d8831fa911cef4e7035293c3a9f88b))
- Merge branch 'master' into jk/update-hooks ([`5c4bd08`](https://github.comgit//DioxusLabs/dioxus/commit/5c4bd0881bc10572440d9272ceb8ca774431c406))
- modify usestate to be borrowed ([`58839f4`](https://github.comgit//DioxusLabs/dioxus/commit/58839f47bae3c49cadd4b41da9a5debebb1def99))
- Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d))
- Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f))
- Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11))
</details>
## v0.1.6 (2022-01-08)
### Documentation
@@ -52,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<csr-read-only-do-not-edit/>
- 93 commits contributed to the release over the course of 358 calendar days.
- 94 commits contributed to the release over the course of 358 calendar days.
- 82 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
@@ -63,6 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<details><summary>view details</summary>
* **Uncategorized**
- Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49))
- Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111))
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76))
+3 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-hooks"
version = "0.1.6"
version = "0.1.7"
authors = ["Jonathan Kelley"]
edition = "2018"
description = "Dioxus VirtualDOM renderer for a remote webview instance"
@@ -12,8 +12,9 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-core = { path = "../../packages/core", version = "^0.1.7" }
dioxus-core = { path = "../../packages/core", version = "^0.1.9" }
[dev-dependencies]
futures-util = { version = "0.3", default-features = false }
dioxus-core = { path = "../../packages/core", version = "^0.1.9" }
+22 -42
View File
@@ -16,45 +16,25 @@ pub use usefuture::*;
mod usesuspense;
pub use usesuspense::*;
// #[macro_export]
// macro_rules! to_owned {
// ($($es:ident),+) => {$(
// #[allow(unused_mut)]
// let mut $es = $es.to_owned();
// )*}
// }
// /// Calls `for_async` on the series of paramters.
// ///
// /// If the type is Clone, then it will be cloned. However, if the type is not `clone`
// /// then it must have a `for_async` method for Rust to lower down into.
// ///
// /// See: how use_state implements `for_async` but *not* through the trait.
// #[macro_export]
// macro_rules! for_async {
// ($($es:ident),+) => {$(
// #[allow(unused_mut)]
// let mut $es = $es.for_async();
// )*}
// }
// /// This is a marker trait that uses decoherence.
// ///
// /// It is *not* meant for hooks to actually implement, but rather defer to their
// /// underlying implementation if they *don't* implement the trait.
// ///
// ///
// pub trait AsyncHook {
// type Output;
// fn for_async(self) -> Self::Output;
// }
// impl<T> AsyncHook for T
// where
// T: ToOwned<Owned = T>,
// {
// type Output = T;
// fn for_async(self) -> Self::Output {
// self
// }
// }
#[macro_export]
/// A helper macro for using hooks in async environements.
///
/// # Usage
///
///
/// ```
/// let (data) = use_ref(&cx, || {});
///
/// let handle_thing = move |_| {
/// to_owned![data]
/// cx.spawn(async move {
/// // do stuff
/// });
/// }
/// ```
macro_rules! to_owned {
($($es:ident),+) => {$(
#[allow(unused_mut)]
let mut $es = $es.to_owned();
)*}
}
+1 -1
View File
@@ -33,7 +33,7 @@ impl<T> ProvidedStateInner<T> {
/// This hook provides some relatively light ergonomics around shared state.
///
/// It is not a substitute for a proper state management system, but it is capable enough to provide use_state - type
/// ergonimics in a pinch, with zero cost.
/// ergonomics in a pinch, with zero cost.
///
/// # Example
///
+207 -37
View File
@@ -1,57 +1,227 @@
use dioxus_core::ScopeState;
use std::{
cell::{Ref, RefCell, RefMut},
rc::Rc,
};
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: Rc<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)();
}
}
+346
View File
@@ -0,0 +1,346 @@
#![warn(clippy::pedantic)]
use dioxus_core::prelude::*;
use std::{
cell::{RefCell, RefMut},
fmt::{Debug, Display},
rc::Rc,
};
/// Store state between component renders.
///
/// ## Dioxus equivalent of useState, designed for Rust
///
/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and
/// modify state between component renders. When the state is updated, the component will re-render.
///
///
/// ```ignore
/// const Example: Component = |cx| {
/// let (count, set_count) = use_state(&cx, || 0);
///
/// cx.render(rsx! {
/// div {
/// h1 { "Count: {count}" }
/// button { onclick: move |_| set_count(a - 1), "Increment" }
/// button { onclick: move |_| set_count(a + 1), "Decrement" }
/// }
/// ))
/// }
/// ```
pub fn use_state<'a, T: 'static>(
cx: &'a ScopeState,
initial_state_fn: impl FnOnce() -> T,
) -> (&'a T, &'a UseState<T>) {
let hook = cx.use_hook(move |_| {
let current_val = Rc::new(initial_state_fn());
let update_callback = cx.schedule_update();
let slot = Rc::new(RefCell::new(current_val.clone()));
let setter = Rc::new({
crate::to_owned![update_callback, slot];
move |new| {
{
let mut slot = slot.borrow_mut();
// if there's only one reference (weak or otherwise), we can just swap the values
// Typically happens when the state is set multiple times - we don't want to create a new Rc for each new value
if let Some(val) = Rc::get_mut(&mut slot) {
*val = new;
} else {
*slot = Rc::new(new);
}
}
update_callback();
}
});
UseState {
current_val,
update_callback,
setter,
slot,
}
});
hook.current_val = hook.slot.borrow().clone();
(hook.current_val.as_ref(), hook)
}
pub struct UseState<T: 'static> {
pub(crate) current_val: Rc<T>,
pub(crate) update_callback: Rc<dyn Fn()>,
pub(crate) setter: Rc<dyn Fn(T)>,
pub(crate) slot: Rc<RefCell<Rc<T>>>,
}
impl<T: 'static> UseState<T> {
/// Get the current value of the state by cloning its container Rc.
///
/// This is useful when you are dealing with state in async contexts but need
/// to know the current value. You are not given a reference to the state.
///
/// # Examples
/// An async context might need to know the current value:
///
/// ```rust, ignore
/// fn component(cx: Scope) -> Element {
/// let (count, set_count) = use_state(&cx, || 0);
/// cx.spawn({
/// let set_count = set_count.to_owned();
/// async move {
/// let current = set_count.current();
/// }
/// })
/// }
/// ```
#[must_use]
pub fn current(&self) -> Rc<T> {
self.slot.borrow().clone()
}
/// Get the `setter` function directly without the `UseState` wrapper.
///
/// This is useful for passing the setter function to other components.
///
/// However, for most cases, calling `to_owned` o`UseState`te is the
/// preferred way to get "anoth`set_state`tate handle.
///
///
/// # Examples
/// A component might require an `Rc<dyn Fn(T)>` as an input to set a value.
///
/// ```rust, ignore
/// fn component(cx: Scope) -> Element {
/// let (value, set_value) = use_state(&cx, || 0);
///
/// rsx!{
/// Component {
/// handler: set_val.setter()
/// }
/// }
/// }
/// ```
#[must_use]
pub fn setter(&self) -> Rc<dyn Fn(T)> {
self.setter.clone()
}
/// Set the state to a new value, using the current state value as a reference.
///
/// This is similar to passing a closure to React's `set_value` function.
///
/// # Examples
///
/// Basic usage:
/// ```rust
/// # use dioxus_core::prelude::*;
/// # use dioxus_hooks::*;
/// fn component(cx: Scope) -> Element {
/// let (value, set_value) = use_state(&cx, || 0);
///
/// // to increment the value
/// set_value.modify(|v| v + 1);
///
/// // usage in async
/// cx.spawn({
/// let set_value = set_value.to_owned();
/// async move {
/// set_value.modify(|v| v + 1);
/// }
/// });
///
/// # todo!()
/// }
/// ```
pub fn modify(&self, f: impl FnOnce(&T) -> T) {
let new_val = {
let current = self.slot.borrow();
f(current.as_ref())
};
(self.setter)(new_val);
}
/// Get the value of the state when this handle was created.
///
/// This method is useful when you want an `Rc` around the data to cheaply
/// pass it around your app.
///
/// ## Warning
///
/// This will return a stale value if used within async contexts.
///
/// Try `current` to get the real current value of the state.
///
/// ## Example
///
/// ```rust, ignore
/// # use dioxus_core::prelude::*;
/// # use dioxus_hooks::*;
/// fn component(cx: Scope) -> Element {
/// let (value, set_value) = use_state(&cx, || 0);
///
/// let as_rc = set_value.get();
/// assert_eq!(as_rc.as_ref(), &0);
///
/// # todo!()
/// }
/// ```
#[must_use]
pub fn get(&self) -> &Rc<T> {
&self.current_val
}
/// Mark the component that create this [`UseState`] as dirty, forcing it to re-render.
///
/// ```rust, ignore
/// fn component(cx: Scope) -> Element {
/// let (count, set_count) = use_state(&cx, || 0);
/// cx.spawn({
/// let set_count = set_count.to_owned();
/// async move {
/// // for the component to re-render
/// set_count.needs_update();
/// }
/// })
/// }
/// ```
pub fn needs_update(&self) {
(self.update_callback)();
}
}
impl<T: Clone> UseState<T> {
/// Get a mutable handle to the value by calling `ToOwned::to_owned` on the
/// current value.
///
/// This is essentially cloning the underlying value and then setting it,
/// giving you a mutable handle in the process. This method is intended for
/// types that are cheaply cloneable.
///
/// If you are comfortable dealing with `RefMut`, then you can use `make_mut` to get
/// the underlying slot. However, be careful with `RefMut` since you might panic
/// if the `RefCell` is left open.
///
/// # Examples
///
/// ```
/// let (val, set_val) = use_state(&cx, || 0);
///
/// set_val.with_mut(|v| *v = 1);
/// ```
pub fn with_mut(&self, apply: impl FnOnce(&mut T)) {
let mut slot = self.slot.borrow_mut();
let mut inner = slot.as_ref().to_owned();
apply(&mut inner);
if let Some(new) = Rc::get_mut(&mut slot) {
*new = inner;
} else {
*slot = Rc::new(inner);
}
self.needs_update();
}
/// Get a mutable handle to the value by calling `ToOwned::to_owned` on the
/// current value.
///
/// This is essentially cloning the underlying value and then setting it,
/// giving you a mutable handle in the process. This method is intended for
/// types that are cheaply cloneable.
///
/// # Warning
/// Be careful with `RefMut` since you might panic if the `RefCell` is left open!
///
/// # Examples
///
/// ```
/// let (val, set_val) = use_state(&cx, || 0);
///
/// set_val.with_mut(|v| *v = 1);
/// ```
#[must_use]
pub fn make_mut(&self) -> RefMut<T> {
let mut slot = self.slot.borrow_mut();
self.needs_update();
if Rc::strong_count(&*slot) > 0 {
*slot = Rc::new(slot.as_ref().to_owned());
}
RefMut::map(slot, |rc| Rc::get_mut(rc).expect("the hard count to be 0"))
}
}
impl<T: 'static> Clone for UseState<T> {
fn clone(&self) -> Self {
UseState {
current_val: self.current_val.clone(),
update_callback: self.update_callback.clone(),
setter: self.setter.clone(),
slot: self.slot.clone(),
}
}
}
impl<'a, T: 'static + Display> std::fmt::Display for UseState<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.current_val)
}
}
impl<T> PartialEq<UseState<T>> for UseState<T> {
fn eq(&self, other: &UseState<T>) -> bool {
// some level of memoization for UseState
Rc::ptr_eq(&self.slot, &other.slot)
}
}
impl<T: Debug> Debug for UseState<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.current_val)
}
}
impl<'a, T> std::ops::Deref for UseState<T> {
type Target = Rc<dyn Fn(T)>;
fn deref(&self) -> &Self::Target {
&self.setter
}
}
#[test]
fn api_makes_sense() {
#[allow(unused)]
fn app(cx: Scope) -> Element {
let (val, set_val) = use_state(&cx, || 0);
set_val(0);
set_val.modify(|v| v + 1);
let real_current = set_val.current();
match val {
10 => {
set_val(20);
set_val.modify(|v| v + 1);
}
20 => {}
_ => {
println!("{real_current}");
}
}
cx.spawn({
crate::to_owned![set_val];
async move {
set_val.modify(|f| f + 1);
}
});
cx.render(LazyNodes::new(|f| f.static_text("asd")))
}
}
-215
View File
@@ -1,215 +0,0 @@
use super::owned::UseStateOwned;
use std::{
cell::{Ref, RefMut},
fmt::{Debug, Display},
rc::Rc,
};
pub struct UseState<'a, T: 'static>(pub(crate) &'a UseStateOwned<T>);
impl<T> Copy for UseState<'_, T> {}
impl<'a, T: 'static> Clone for UseState<'a, T> {
fn clone(&self) -> Self {
UseState(self.0)
}
}
impl<T: Debug> Debug for UseState<'_, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0.current_val)
}
}
impl<'a, T: 'static> UseState<'a, T> {
/// Tell the Dioxus Scheduler that we need to be processed
pub fn needs_update(&self) {
if !self.0.update_scheuled.get() {
self.0.update_scheuled.set(true);
(self.0.update_callback)();
}
}
pub fn set(&self, new_val: T) {
*self.0.wip.borrow_mut() = Some(new_val);
self.needs_update();
}
pub fn get(&self) -> &'a T {
&self.0.current_val
}
pub fn get_rc(&self) -> &'a Rc<T> {
&self.0.current_val
}
/// Get the current status of the work-in-progress data
pub fn get_wip(&self) -> Ref<Option<T>> {
self.0.wip.borrow()
}
/// Get the current status of the work-in-progress data
pub fn get_wip_mut(&self) -> RefMut<Option<T>> {
self.0.wip.borrow_mut()
}
pub fn classic(self) -> (&'a T, Rc<dyn Fn(T)>) {
(&self.0.current_val, self.setter())
}
pub fn setter(&self) -> Rc<dyn Fn(T)> {
let slot = self.0.wip.clone();
let callback = self.0.update_callback.clone();
Rc::new(move |new| {
callback();
*slot.borrow_mut() = Some(new)
})
}
pub fn with(&self, f: impl FnOnce(&mut T)) {
let mut val = self.0.wip.borrow_mut();
if let Some(inner) = val.as_mut() {
f(inner);
}
}
pub fn for_async(&self) -> UseStateOwned<T> {
let UseStateOwned {
current_val,
wip,
update_callback,
update_scheuled,
} = self.0;
UseStateOwned {
current_val: current_val.clone(),
wip: wip.clone(),
update_callback: update_callback.clone(),
update_scheuled: update_scheuled.clone(),
}
}
}
impl<'a, T: 'static + ToOwned<Owned = T>> UseState<'a, T> {
/// Gain mutable access to the new value via [`RefMut`].
///
/// If `modify` is called, then the component will re-render.
///
/// This method is only available when the value is a `ToOwned` type.
///
/// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value.
///
/// To get a reference to the current value, use `.get()`
pub fn modify(self) -> RefMut<'a, T> {
// make sure we get processed
self.needs_update();
// Bring out the new value, cloning if it we need to
// "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this
RefMut::map(self.0.wip.borrow_mut(), |slot| {
if slot.is_none() {
*slot = Some(self.0.current_val.as_ref().to_owned());
}
slot.as_mut().unwrap()
})
}
pub fn inner(self) -> T {
self.0.current_val.as_ref().to_owned()
}
}
impl<'a, T> std::ops::Deref for UseState<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.get()
}
}
// enable displaty for the handle
impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.current_val)
}
}
impl<'a, V, T: PartialEq<V>> PartialEq<V> for UseState<'a, T> {
fn eq(&self, other: &V) -> bool {
self.get() == other
}
}
impl<'a, O, T: std::ops::Not<Output = O> + Copy> std::ops::Not for UseState<'a, T> {
type Output = O;
fn not(self) -> Self::Output {
!*self.get()
}
}
/*
Convenience methods for UseState.
Note!
This is not comprehensive.
This is *just* meant to make common operations easier.
*/
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
impl<'a, T: Copy + Add<T, Output = T>> Add<T> for UseState<'a, T> {
type Output = T;
fn add(self, rhs: T) -> Self::Output {
self.0.current_val.add(rhs)
}
}
impl<'a, T: Copy + Add<T, Output = T>> AddAssign<T> for UseState<'a, T> {
fn add_assign(&mut self, rhs: T) {
self.set(self.0.current_val.add(rhs));
}
}
/// Sub
impl<'a, T: Copy + Sub<T, Output = T>> Sub<T> for UseState<'a, T> {
type Output = T;
fn sub(self, rhs: T) -> Self::Output {
self.0.current_val.sub(rhs)
}
}
impl<'a, T: Copy + Sub<T, Output = T>> SubAssign<T> for UseState<'a, T> {
fn sub_assign(&mut self, rhs: T) {
self.set(self.0.current_val.sub(rhs));
}
}
/// MUL
impl<'a, T: Copy + Mul<T, Output = T>> Mul<T> for UseState<'a, T> {
type Output = T;
fn mul(self, rhs: T) -> Self::Output {
self.0.current_val.mul(rhs)
}
}
impl<'a, T: Copy + Mul<T, Output = T>> MulAssign<T> for UseState<'a, T> {
fn mul_assign(&mut self, rhs: T) {
self.set(self.0.current_val.mul(rhs));
}
}
/// DIV
impl<'a, T: Copy + Div<T, Output = T>> Div<T> for UseState<'a, T> {
type Output = T;
fn div(self, rhs: T) -> Self::Output {
self.0.current_val.div(rhs)
}
}
impl<'a, T: Copy + Div<T, Output = T>> DivAssign<T> for UseState<'a, T> {
fn div_assign(&mut self, rhs: T) {
self.set(self.0.current_val.div(rhs));
}
}
-78
View File
@@ -1,78 +0,0 @@
mod handle;
mod owned;
pub use handle::*;
pub use owned::*;
use dioxus_core::prelude::*;
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
/// Store state between component renders!
///
/// ## Dioxus equivalent of useState, designed for Rust
///
/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and
/// modify state between component renders. When the state is updated, the component will re-render.
///
/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system.
///
/// [`use_state`] exposes a few helper methods to modify the underlying state:
/// - `.set(new)` allows you to override the "work in progress" value with a new value
/// - `.get_mut()` allows you to modify the WIP value
/// - `.get_wip()` allows you to access the WIP value
/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required)
///
/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations
/// will automatically be called on the WIP value.
///
/// ## Combinators
///
/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality:
/// - `.classic()` and `.split()` convert the hook into the classic React-style hook
/// ```rust
/// let (state, set_state) = use_state(&cx, || 10).split()
/// ```
///
///
/// Usage:
///
/// ```ignore
/// const Example: Component = |cx| {
/// let counter = use_state(&cx, || 0);
///
/// cx.render(rsx! {
/// div {
/// h1 { "Counter: {counter}" }
/// button { onclick: move |_| counter += 1, "Increment" }
/// button { onclick: move |_| counter -= 1, "Decrement" }
/// }
/// ))
/// }
/// ```
pub fn use_state<'a, T: 'static>(
cx: &'a ScopeState,
initial_state_fn: impl FnOnce() -> T,
) -> UseState<'a, T> {
let hook = cx.use_hook(move |_| UseStateOwned {
current_val: Rc::new(initial_state_fn()),
update_callback: cx.schedule_update(),
wip: Rc::new(RefCell::new(None)),
update_scheuled: Cell::new(false),
});
hook.update_scheuled.set(false);
let mut new_val = hook.wip.borrow_mut();
if new_val.is_some() {
// if there's only one reference (weak or otherwise), we can just swap the values
if let Some(val) = Rc::get_mut(&mut hook.current_val) {
*val = new_val.take().unwrap();
} else {
hook.current_val = Rc::new(new_val.take().unwrap());
}
}
UseState(hook)
}
-99
View File
@@ -1,99 +0,0 @@
use std::{
cell::{Cell, Ref, RefCell, RefMut},
fmt::{Debug, Display},
rc::Rc,
};
pub struct UseStateOwned<T: 'static> {
// this will always be outdated
pub(crate) current_val: Rc<T>,
pub(crate) wip: Rc<RefCell<Option<T>>>,
pub(crate) update_callback: Rc<dyn Fn()>,
pub(crate) update_scheuled: Cell<bool>,
}
impl<T> UseStateOwned<T> {
pub fn get(&self) -> Ref<Option<T>> {
self.wip.borrow()
}
pub fn set(&self, new_val: T) {
*self.wip.borrow_mut() = Some(new_val);
(self.update_callback)();
}
pub fn modify(&self) -> RefMut<Option<T>> {
(self.update_callback)();
self.wip.borrow_mut()
}
}
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
impl<T: Debug> Debug for UseStateOwned<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.current_val)
}
}
// enable displaty for the handle
impl<'a, T: 'static + Display> std::fmt::Display for UseStateOwned<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.current_val)
}
}
impl<'a, T: Copy + Add<T, Output = T>> Add<T> for UseStateOwned<T> {
type Output = T;
fn add(self, rhs: T) -> Self::Output {
self.current_val.add(rhs)
}
}
impl<'a, T: Copy + Add<T, Output = T>> AddAssign<T> for UseStateOwned<T> {
fn add_assign(&mut self, rhs: T) {
self.set(self.current_val.add(rhs));
}
}
/// Sub
impl<'a, T: Copy + Sub<T, Output = T>> Sub<T> for UseStateOwned<T> {
type Output = T;
fn sub(self, rhs: T) -> Self::Output {
self.current_val.sub(rhs)
}
}
impl<'a, T: Copy + Sub<T, Output = T>> SubAssign<T> for UseStateOwned<T> {
fn sub_assign(&mut self, rhs: T) {
self.set(self.current_val.sub(rhs));
}
}
/// MUL
impl<'a, T: Copy + Mul<T, Output = T>> Mul<T> for UseStateOwned<T> {
type Output = T;
fn mul(self, rhs: T) -> Self::Output {
self.current_val.mul(rhs)
}
}
impl<'a, T: Copy + Mul<T, Output = T>> MulAssign<T> for UseStateOwned<T> {
fn mul_assign(&mut self, rhs: T) {
self.set(self.current_val.mul(rhs));
}
}
/// DIV
impl<'a, T: Copy + Div<T, Output = T>> Div<T> for UseStateOwned<T> {
type Output = T;
fn div(self, rhs: T) -> Self::Output {
self.current_val.div(rhs)
}
}
impl<'a, T: Copy + Div<T, Output = T>> DivAssign<T> for UseStateOwned<T> {
fn div_assign(&mut self, rhs: T) {
self.set(self.current_val.div(rhs));
}
}
+33 -2
View File
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Bug Fixes
- <csr-id-43e78d56f72e23ada939f5688a6bd2fdd9b8292d/> rustfmt
### Commit Statistics
<csr-read-only-do-not-edit/>
- 7 commits contributed to the release over the course of 19 calendar days.
- 1 commit where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
### Commit Details
<csr-read-only-do-not-edit/>
<details><summary>view details</summary>
* **Uncategorized**
- rustfmt ([`43e78d5`](https://github.comgit//DioxusLabs/dioxus/commit/43e78d56f72e23ada939f5688a6bd2fdd9b8292d))
- Add gap and row_gap to style_trait_methods ([`5f4a724`](https://github.comgit//DioxusLabs/dioxus/commit/5f4a72446e63441628e18bb19fd9b2d9f65f9d52))
- Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3))
- Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d))
- Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401))
- feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9))
- feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3))
</details>
## v0.1.4 (2022-01-08)
### Documentation
@@ -42,7 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<csr-read-only-do-not-edit/>
- 60 commits contributed to the release over the course of 184 calendar days.
- 61 commits contributed to the release over the course of 184 calendar days.
- 52 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
@@ -53,8 +83,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<details><summary>view details</summary>
* **Uncategorized**
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111))
- enable prevent_default everywhere ([`9dff700`](https://github.comgit//DioxusLabs/dioxus/commit/9dff700c220dd9e0da2ee028900e82fd24f9d0dd))
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76))
- memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c))
- new versions of everything ([`4ea5c99`](https://github.comgit//DioxusLabs/dioxus/commit/4ea5c990d72b1645724ab0a88ffea2baf28e2835))
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-html"
version = "0.1.4"
version = "0.1.6"
authors = ["Jonathan Kelley"]
edition = "2018"
description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
@@ -11,7 +11,7 @@ documentation = "https://docs.rs/dioxus"
keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies]
dioxus-core = { path = "../core", version ="^0.1.7"}
dioxus-core = { path = "../core", version = "^0.1.9" }
serde = { version = "1", features = ["derive"], optional = true }
serde_repr = { version = "0.1", optional = true }
+1 -6
View File
@@ -1047,7 +1047,7 @@ impl input {
/// - `text`
/// - `time`
/// - `url`
/// - `week`
/// - `week`
pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("type", val, None, false)
}
@@ -1100,11 +1100,6 @@ impl label {
cx.attr("for", val, None, false)
}
}
impl a {
pub fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("dioxus-prevent-default", val, None, false)
}
}
builder_constructors! {
// SVG components
+3
View File
@@ -3,6 +3,8 @@ use dioxus_core::exports::bumpalo;
use dioxus_core::*;
pub mod on {
use std::collections::HashMap;
use super::*;
macro_rules! event_directory {
( $(
@@ -484,6 +486,7 @@ pub mod on {
#[derive(Debug)]
pub struct FormData {
pub value: String,
pub values: HashMap<String, String>,
/* DOMEvent: Send + SyncTarget relatedTarget */
}
+16
View File
@@ -49,6 +49,10 @@ macro_rules! aria_trait_methods {
}
pub trait GlobalAttributes {
/// Prevent the default action for this element.
///
/// For more information, see the MDN docs:
/// <https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault>
fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("dioxus-prevent-default", val, None, false)
}
@@ -603,6 +607,11 @@ pub trait GlobalAttributes {
/// Specifies the speed curve of the transition effect.
transition_timing_function: "transition-timing-function",
/// The user-select CSS property controls whether the user can select text.
/// This doesn't have any effect on content loaded as part of a browser's user interface (its chrome), except in textboxes.
user_select: "user-select",
webkit_user_select: "-webkit-user-select",
/// Sets the vertical positioning of an element relative to the current text baseline.
vertical_align: "vertical-align",
@@ -685,6 +694,13 @@ pub trait GlobalAttributes {
}
pub trait SvgAttributes {
/// Prevent the default action for this element.
///
/// For more information, see the MDN docs:
/// <https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault>
fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("dioxus-prevent-default", val, None, false)
}
aria_trait_methods! {
accent_height: "accent-height",
accumulate: "accumulate",
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "dioxus-interpreter-js"
version = "0.0.0"
edition = "2018"
authors = ["Jonathan Kelley"]
description = "JS Intepreter for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://docs.rs/dioxus"
keywords = ["dom", "ui", "gui", "react", "wasm"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wasm-bindgen = { version = "0.2.79", optional = true }
js-sys = { version = "0.3.56", optional = true }
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
[features]
default = []
web = ["wasm-bindgen", "js-sys", "web-sys"]
@@ -4,5 +4,4 @@ After diffing old and new trees, the Dioxus VirtualDom produces patches that are
In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets.
For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks.
@@ -1,8 +1,10 @@
#![allow(clippy::unused_unit, non_upper_case_globals)]
use js_sys::Function;
use wasm_bindgen::prelude::*;
use web_sys::{Element, Node};
#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")]
#[wasm_bindgen(module = "/src/interpreter.js")]
extern "C" {
pub type Interpreter;
@@ -31,7 +33,7 @@ extern "C" {
pub fn Remove(this: &Interpreter, root: u64);
#[wasm_bindgen(method)]
pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64);
pub fn CreateTextNode(this: &Interpreter, text: JsValue, root: u64);
#[wasm_bindgen(method)]
pub fn CreateElement(this: &Interpreter, tag: &str, root: u64);
@@ -49,10 +51,16 @@ extern "C" {
pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str);
#[wasm_bindgen(method)]
pub fn SetText(this: &Interpreter, root: u64, text: &str);
pub fn SetText(this: &Interpreter, root: u64, text: JsValue);
#[wasm_bindgen(method)]
pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>);
pub fn SetAttribute(
this: &Interpreter,
root: u64,
field: &str,
value: JsValue,
ns: Option<&str>,
);
#[wasm_bindgen(method)]
pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str);
@@ -1,33 +1,12 @@
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"));
}
}
declare global {
interface Window {
interpreter: Interpreter;
rpc: { call: (method: string, args?: any) => void };
}
}
export class Interpreter {
root: Element;
stack: Element[];
listeners: { [key: string]: number };
handlers: { [key: string]: (evt: Event) => void };
lastNodeWasText: boolean;
nodes: Element[];
constructor(root: Element) {
constructor(root) {
this.root = root;
this.stack = [root];
this.listeners = {};
@@ -35,88 +14,70 @@ export class Interpreter {
this.lastNodeWasText = false;
this.nodes = [root];
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
PushRoot(root: number) {
PushRoot(root) {
const node = this.nodes[root];
this.stack.push(node);
}
AppendChildren(many: number) {
AppendChildren(many) {
let root = this.stack[this.stack.length - (1 + many)];
let to_add = this.stack.splice(this.stack.length - many);
for (let i = 0; i < many; i++) {
root.appendChild(to_add[i]);
}
}
ReplaceWith(root_id: number, m: number) {
let root = this.nodes[root_id] as Element;
ReplaceWith(root_id, m) {
let root = this.nodes[root_id];
let els = this.stack.splice(this.stack.length - m);
root.replaceWith(...els);
}
InsertAfter(root: number, n: number) {
let old = this.nodes[root] as Element;
InsertAfter(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.after(...new_nodes);
}
InsertBefore(root: number, n: number) {
let old = this.nodes[root] as Element;
InsertBefore(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.before(...new_nodes);
}
Remove(root: number) {
let node = this.nodes[root] as Element;
Remove(root) {
let node = this.nodes[root];
if (node !== undefined) {
node.remove();
}
}
CreateTextNode(text: string, root: number) {
CreateTextNode(text, root) {
// todo: make it so the types are okay
const node = document.createTextNode(text) as any as Element;
const node = document.createTextNode(text);
this.nodes[root] = node;
this.stack.push(node);
}
CreateElement(tag: string, root: number) {
CreateElement(tag, root) {
const el = document.createElement(tag);
// el.setAttribute("data-dioxus-id", `${root}`);
this.nodes[root] = el;
this.stack.push(el);
}
CreateElementNs(tag: string, root: number, ns: string) {
CreateElementNs(tag, root, ns) {
let el = document.createElementNS(ns, tag);
this.stack.push(el);
this.nodes[root] = el;
}
CreatePlaceholder(root: number) {
CreatePlaceholder(root) {
let el = document.createElement("pre");
el.hidden = true;
this.stack.push(el);
this.nodes[root] = el;
}
NewEventListener(event_name: string, root: number, handler: (evt: Event) => void) {
NewEventListener(event_name, root, handler) {
const element = this.nodes[root];
element.setAttribute("data-dioxus-id", `${root}`);
if (this.listeners[event_name] === undefined) {
this.listeners[event_name] = 0;
this.handlers[event_name] = handler;
@@ -125,55 +86,46 @@ export class Interpreter {
this.listeners[event_name]++;
}
}
RemoveEventListener(root: number, event_name: string) {
RemoveEventListener(root, event_name) {
const element = this.nodes[root];
element.removeAttribute(`data-dioxus-id`);
this.listeners[event_name]--;
if (this.listeners[event_name] === 0) {
this.root.removeEventListener(event_name, this.handlers[event_name]);
delete this.listeners[event_name];
delete this.handlers[event_name];
}
}
SetText(root: number, text: string) {
SetText(root, text) {
this.nodes[root].textContent = text;
}
SetAttribute(root: number, field: string, value: string, ns: string | undefined) {
SetAttribute(root, field, value, ns) {
const name = field;
const node = this.nodes[root];
if (ns == "style") {
if (ns === "style") {
// @ts-ignore
(node as HTMLElement).style[name] = value;
node.style[name] = value;
} else if (ns != null || ns != undefined) {
node.setAttributeNS(ns, name, value);
} else {
switch (name) {
case "value":
if (value != (node as HTMLInputElement).value) {
(node as HTMLInputElement).value = value;
if (value !== node.value) {
node.value = value;
}
break;
case "checked":
(node as HTMLInputElement).checked = value === "true";
node.checked = value === "true";
break;
case "selected":
(node as HTMLOptionElement).selected = value === "true";
node.selected = value === "true";
break;
case "dangerous_inner_html":
node.innerHTML = value;
break;
default:
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
if (value == "false" && bool_attrs.hasOwnProperty(name)) {
if (value === "false" && bool_attrs.hasOwnProperty(name)) {
node.removeAttribute(name);
} else {
node.setAttribute(name, value);
@@ -181,33 +133,28 @@ export class Interpreter {
}
}
}
RemoveAttribute(root: number, name: string) {
RemoveAttribute(root, name) {
const node = this.nodes[root];
node.removeAttribute(name);
if (name === "value") {
(node as HTMLInputElement).value = "";
}
if (name === "checked") {
(node as HTMLInputElement).checked = false;
}
if (name === "selected") {
(node as HTMLOptionElement).selected = false;
node.value = "";
} else if (name === "checked") {
node.checked = false;
} else if (name === "selected") {
node.selected = false;
} else if (name === "dangerous_inner_html") {
node.innerHTML = "";
} else {
node.removeAttribute(name);
}
}
handleEdits(edits: DomEdit[]) {
handleEdits(edits) {
this.stack.push(this.root);
for (let edit of edits) {
this.handleEdit(edit);
}
}
handleEdit(edit: DomEdit) {
handleEdit(edit) {
switch (edit.type) {
case "PushRoot":
this.PushRoot(edit.root);
@@ -243,44 +190,71 @@ export class Interpreter {
this.RemoveEventListener(edit.root, edit.event_name);
break;
case "NewEventListener":
// this handler is only provided on desktop implementations since this
// this handler is only provided on desktop implementations since this
// method is not used by the web implementation
let handler = (event: Event) => {
let target = event.target as Element | null;
let handler = (event) => {
let target = event.target;
if (target != null) {
let realId = target.getAttribute(`data-dioxus-id`);
let shouldPreventDefault = target.getAttribute(
`dioxus-prevent-default`
);
if (event.type === "click") {
// todo call prevent default if it's the right type of event
if (shouldPreventDefault !== `onclick`) {
if (target.tagName === "A") {
event.preventDefault();
const href = target.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.ipc.postMessage(
serializeIpcMessage("browser_open", { href })
);
}
}
}
// also prevent buttons from submitting
if (target.tagName === "BUTTON") {
event.preventDefault();
}
}
// walk the tree to find the real element
while (realId == null && target.parentElement != null) {
while (realId == null) {
// we've reached the root we don't want to send an event
if (target.parentElement === null) {
return;
}
target = target.parentElement;
realId = target.getAttribute(`data-dioxus-id`);
}
const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
shouldPreventDefault = target.getAttribute(
`dioxus-prevent-default`
);
let contents = serialize_event(event);
if (shouldPreventDefault === `on${event.type}`) {
event.preventDefault();
}
if (event.type == "submit") {
if (event.type === "submit") {
event.preventDefault();
}
if (event.type == "click") {
event.preventDefault();
if (shouldPreventDefault !== `onclick`) {
if (target.tagName == "A") {
const href = target.getAttribute("href")
if (href !== "" && href !== null && href !== undefined && realId != null) {
window.rpc.call("browser_open", {
mounted_dom_id: parseInt(realId),
href
});
if (target.tagName === "FORM") {
for (let x = 0; x < target.elements.length; x++) {
let element = target.elements[x];
let name = element.getAttribute("name");
if (name != null) {
if (element.getAttribute("type") === "checkbox") {
// @ts-ignore
contents.values[name] = element.checked ? "true" : "false";
} else {
// @ts-ignore
contents.values[name] =
element.value ?? element.textContent;
}
}
}
@@ -289,12 +263,13 @@ export class Interpreter {
if (realId == null) {
return;
}
window.rpc.call("user_event", {
event: (edit as NewEventListener).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);
@@ -312,25 +287,21 @@ export class Interpreter {
}
}
function serialize_event(event: Event) {
export function serialize_event(event) {
switch (event.type) {
case "copy":
case "cut":
case "past": {
return {};
}
case "compositionend":
case "compositionstart":
case "compositionupdate": {
let { data } = (event as CompositionEvent);
let { data } = event;
return {
data,
};
}
case "keydown":
case "keypress":
case "keyup": {
@@ -345,8 +316,7 @@ function serialize_event(event: Event) {
location,
repeat,
which,
} = (event as KeyboardEvent);
} = event;
return {
char_code: charCode,
key: key,
@@ -361,42 +331,37 @@ function serialize_event(event: Event) {
locale: "locale",
};
}
case "focus":
case "blur": {
return {};
}
case "change": {
let target = event.target as HTMLInputElement;
let target = event.target;
let value;
if (target.type === "checkbox" || target.type === "radio") {
value = target.checked ? "true" : "false";
} else {
value = target.value ?? target.textContent;
}
return {
value: value,
values: {},
};
}
case "input":
case "invalid":
case "reset":
case "submit": {
let target = event.target as HTMLFormElement;
let target = event.target;
let value = target.value ?? target.textContent;
if (target.type == "checkbox") {
if (target.type === "checkbox") {
value = target.checked ? "true" : "false";
}
return {
value: value,
values: {},
};
}
case "click":
case "contextmenu":
case "doubleclick":
@@ -428,8 +393,7 @@ function serialize_event(event: Event) {
screenX,
screenY,
shiftKey,
} = event as MouseEvent;
} = event;
return {
alt_key: altKey,
button: button,
@@ -445,7 +409,6 @@ function serialize_event(event: Event) {
shift_key: shiftKey,
};
}
case "pointerdown":
case "pointermove":
case "pointerup":
@@ -479,7 +442,7 @@ function serialize_event(event: Event) {
twist,
pointerType,
isPrimary,
} = event as PointerEvent;
} = event;
return {
alt_key: altKey,
button: button,
@@ -505,21 +468,14 @@ function serialize_event(event: Event) {
is_primary: isPrimary,
};
}
case "select": {
return {};
}
case "touchcancel":
case "touchend":
case "touchmove":
case "touchstart": {
const {
altKey,
ctrlKey,
metaKey,
shiftKey,
} = event as TouchEvent;
const { altKey, ctrlKey, metaKey, shiftKey } = event;
return {
// changed_touches: event.changedTouches,
// target_touches: event.targetTouches,
@@ -530,18 +486,11 @@ function serialize_event(event: Event) {
shift_key: shiftKey,
};
}
case "scroll": {
return {};
}
case "wheel": {
const {
deltaX,
deltaY,
deltaZ,
deltaMode,
} = event as WheelEvent;
const { deltaX, deltaY, deltaZ, deltaMode } = event;
return {
delta_x: deltaX,
delta_y: deltaY,
@@ -549,35 +498,24 @@ function serialize_event(event: Event) {
delta_mode: deltaMode,
};
}
case "animationstart":
case "animationend":
case "animationiteration": {
const {
animationName,
elapsedTime,
pseudoElement,
} = event as AnimationEvent;
const { animationName, elapsedTime, pseudoElement } = event;
return {
animation_name: animationName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "transitionend": {
const {
propertyName,
elapsedTime,
pseudoElement,
} = event as TransitionEvent;
const { propertyName, elapsedTime, pseudoElement } = event;
return {
property_name: propertyName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "abort":
case "canplay":
case "canplaythrough":
@@ -603,17 +541,17 @@ function serialize_event(event: Event) {
case "waiting": {
return {};
}
case "toggle": {
return {};
}
default: {
return {};
}
}
}
function serializeIpcMessage(method, params = {}) {
return JSON.stringify({ method, params });
}
const bool_attrs = {
allowfullscreen: true,
allowpaymentrequest: true,
@@ -642,39 +580,3 @@ const bool_attrs = {
selected: true,
truespeed: true,
};
type PushRoot = { type: "PushRoot", root: number };
type AppendChildren = { type: "AppendChildren", many: number };
type ReplaceWith = { type: "ReplaceWith", root: number, m: number };
type InsertAfter = { type: "InsertAfter", root: number, n: number };
type InsertBefore = { type: "InsertBefore", root: number, n: number };
type Remove = { type: "Remove", root: number };
type CreateTextNode = { type: "CreateTextNode", text: string, root: number };
type CreateElement = { type: "CreateElement", tag: string, root: number };
type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string };
type CreatePlaceholder = { type: "CreatePlaceholder", root: number };
type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number };
type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number };
type SetText = { type: "SetText", root: number, text: string };
type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined };
type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string };
type DomEdit =
PushRoot |
AppendChildren |
ReplaceWith |
InsertAfter |
InsertBefore |
Remove |
CreateTextNode |
CreateElement |
CreateElementNs |
CreatePlaceholder |
NewEventListener |
RemoveEventListener |
SetText |
SetAttribute |
RemoveAttribute;
+7
View File
@@ -0,0 +1,7 @@
pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
#[cfg(feature = "web")]
mod bindings;
#[cfg(feature = "web")]
pub use bindings::Interpreter;
-499
View File
@@ -1,499 +0,0 @@
export function main() {
let root = window.document.getElementById("main");
if (root != null) {
window.interpreter = new Interpreter(root);
window.rpc.call("initialize");
}
}
export class Interpreter {
root;
stack;
listeners;
handlers;
lastNodeWasText;
nodes;
constructor(root) {
this.root = root;
this.stack = [root];
this.listeners = {};
this.handlers = {};
this.lastNodeWasText = false;
this.nodes = [root];
}
top() {
return this.stack[this.stack.length - 1];
}
pop() {
return this.stack.pop();
}
PushRoot(root) {
const node = this.nodes[root];
this.stack.push(node);
}
AppendChildren(many) {
let root = this.stack[this.stack.length - (1 + many)];
let to_add = this.stack.splice(this.stack.length - many);
for (let i = 0; i < many; i++) {
root.appendChild(to_add[i]);
}
}
ReplaceWith(root_id, m) {
let root = this.nodes[root_id];
let els = this.stack.splice(this.stack.length - m);
root.replaceWith(...els);
}
InsertAfter(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.after(...new_nodes);
}
InsertBefore(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.before(...new_nodes);
}
Remove(root) {
let node = this.nodes[root];
if (node !== undefined) {
node.remove();
}
}
CreateTextNode(text, root) {
// todo: make it so the types are okay
const node = document.createTextNode(text);
this.nodes[root] = node;
this.stack.push(node);
}
CreateElement(tag, root) {
const el = document.createElement(tag);
// el.setAttribute("data-dioxus-id", `${root}`);
this.nodes[root] = el;
this.stack.push(el);
}
CreateElementNs(tag, root, ns) {
let el = document.createElementNS(ns, tag);
this.stack.push(el);
this.nodes[root] = el;
}
CreatePlaceholder(root) {
let el = document.createElement("pre");
el.hidden = true;
this.stack.push(el);
this.nodes[root] = el;
}
NewEventListener(event_name, root, handler) {
const element = this.nodes[root];
element.setAttribute("data-dioxus-id", `${root}`);
if (this.listeners[event_name] === undefined) {
this.listeners[event_name] = 0;
this.handlers[event_name] = handler;
this.root.addEventListener(event_name, handler);
}
else {
this.listeners[event_name]++;
}
}
RemoveEventListener(root, event_name) {
const element = this.nodes[root];
element.removeAttribute(`data-dioxus-id`);
this.listeners[event_name]--;
if (this.listeners[event_name] === 0) {
this.root.removeEventListener(event_name, this.handlers[event_name]);
delete this.listeners[event_name];
delete this.handlers[event_name];
}
}
SetText(root, text) {
this.nodes[root].textContent = text;
}
SetAttribute(root, field, value, ns) {
const name = field;
const node = this.nodes[root];
if (ns == "style") {
// @ts-ignore
node.style[name] = value;
}
else if (ns != null || ns != undefined) {
node.setAttributeNS(ns, name, value);
}
else {
switch (name) {
case "value":
if (value != node.value) {
node.value = value;
}
break;
case "checked":
node.checked = value === "true";
break;
case "selected":
node.selected = value === "true";
break;
case "dangerous_inner_html":
node.innerHTML = value;
break;
default:
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
if (value == "false" && bool_attrs.hasOwnProperty(name)) {
node.removeAttribute(name);
}
else {
node.setAttribute(name, value);
}
}
}
}
RemoveAttribute(root, name) {
const node = this.nodes[root];
node.removeAttribute(name);
if (name === "value") {
node.value = "";
}
if (name === "checked") {
node.checked = false;
}
if (name === "selected") {
node.selected = false;
}
}
handleEdits(edits) {
this.stack.push(this.root);
for (let edit of edits) {
this.handleEdit(edit);
}
}
handleEdit(edit) {
switch (edit.type) {
case "PushRoot":
this.PushRoot(edit.root);
break;
case "AppendChildren":
this.AppendChildren(edit.many);
break;
case "ReplaceWith":
this.ReplaceWith(edit.root, edit.m);
break;
case "InsertAfter":
this.InsertAfter(edit.root, edit.n);
break;
case "InsertBefore":
this.InsertBefore(edit.root, edit.n);
break;
case "Remove":
this.Remove(edit.root);
break;
case "CreateTextNode":
this.CreateTextNode(edit.text, edit.root);
break;
case "CreateElement":
this.CreateElement(edit.tag, edit.root);
break;
case "CreateElementNs":
this.CreateElementNs(edit.tag, edit.root, edit.ns);
break;
case "CreatePlaceholder":
this.CreatePlaceholder(edit.root);
break;
case "RemoveEventListener":
this.RemoveEventListener(edit.root, edit.event_name);
break;
case "NewEventListener":
// this handler is only provided on desktop implementations since this
// method is not used by the web implementation
let handler = (event) => {
let target = event.target;
if (target != null) {
let realId = target.getAttribute(`data-dioxus-id`);
// walk the tree to find the real element
while (realId == null && target.parentElement != null) {
target = target.parentElement;
realId = target.getAttribute(`data-dioxus-id`);
}
const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
let contents = serialize_event(event);
if (shouldPreventDefault === `on${event.type}`) {
event.preventDefault();
}
if (event.type == "submit") {
event.preventDefault();
}
if (event.type == "click") {
event.preventDefault();
if (shouldPreventDefault !== `onclick`) {
if (target.tagName == "A") {
const href = target.getAttribute("href");
if (href !== "" && href !== null && href !== undefined && realId != null) {
window.rpc.call("browser_open", {
mounted_dom_id: parseInt(realId),
href
});
}
}
}
}
if (realId == null) {
return;
}
window.rpc.call("user_event", {
event: edit.event_name,
mounted_dom_id: parseInt(realId),
contents: contents,
});
}
};
this.NewEventListener(edit.event_name, edit.root, handler);
break;
case "SetText":
this.SetText(edit.root, edit.text);
break;
case "SetAttribute":
this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
break;
case "RemoveAttribute":
this.RemoveAttribute(edit.root, edit.name);
break;
}
}
}
function serialize_event(event) {
switch (event.type) {
case "copy":
case "cut":
case "past": {
return {};
}
case "compositionend":
case "compositionstart":
case "compositionupdate": {
let { data } = event;
return {
data,
};
}
case "keydown":
case "keypress":
case "keyup": {
let { charCode, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event;
return {
char_code: charCode,
key: key,
alt_key: altKey,
ctrl_key: ctrlKey,
meta_key: metaKey,
key_code: keyCode,
shift_key: shiftKey,
location: location,
repeat: repeat,
which: which,
locale: "locale",
};
}
case "focus":
case "blur": {
return {};
}
case "change": {
let target = event.target;
let value;
if (target.type === "checkbox" || target.type === "radio") {
value = target.checked ? "true" : "false";
}
else {
value = target.value ?? target.textContent;
}
return {
value: value,
};
}
case "input":
case "invalid":
case "reset":
case "submit": {
let target = event.target;
let value = target.value ?? target.textContent;
if (target.type == "checkbox") {
value = target.checked ? "true" : "false";
}
return {
value: value,
};
}
case "click":
case "contextmenu":
case "doubleclick":
case "drag":
case "dragend":
case "dragenter":
case "dragexit":
case "dragleave":
case "dragover":
case "dragstart":
case "drop":
case "mousedown":
case "mouseenter":
case "mouseleave":
case "mousemove":
case "mouseout":
case "mouseover":
case "mouseup": {
const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, } = event;
return {
alt_key: altKey,
button: button,
buttons: buttons,
client_x: clientX,
client_y: clientY,
ctrl_key: ctrlKey,
meta_key: metaKey,
page_x: pageX,
page_y: pageY,
screen_x: screenX,
screen_y: screenY,
shift_key: shiftKey,
};
}
case "pointerdown":
case "pointermove":
case "pointerup":
case "pointercancel":
case "gotpointercapture":
case "lostpointercapture":
case "pointerenter":
case "pointerleave":
case "pointerover":
case "pointerout": {
const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, pointerId, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, pointerType, isPrimary, } = event;
return {
alt_key: altKey,
button: button,
buttons: buttons,
client_x: clientX,
client_y: clientY,
ctrl_key: ctrlKey,
meta_key: metaKey,
page_x: pageX,
page_y: pageY,
screen_x: screenX,
screen_y: screenY,
shift_key: shiftKey,
pointer_id: pointerId,
width: width,
height: height,
pressure: pressure,
tangential_pressure: tangentialPressure,
tilt_x: tiltX,
tilt_y: tiltY,
twist: twist,
pointer_type: pointerType,
is_primary: isPrimary,
};
}
case "select": {
return {};
}
case "touchcancel":
case "touchend":
case "touchmove":
case "touchstart": {
const { altKey, ctrlKey, metaKey, shiftKey, } = event;
return {
// changed_touches: event.changedTouches,
// target_touches: event.targetTouches,
// touches: event.touches,
alt_key: altKey,
ctrl_key: ctrlKey,
meta_key: metaKey,
shift_key: shiftKey,
};
}
case "scroll": {
return {};
}
case "wheel": {
const { deltaX, deltaY, deltaZ, deltaMode, } = event;
return {
delta_x: deltaX,
delta_y: deltaY,
delta_z: deltaZ,
delta_mode: deltaMode,
};
}
case "animationstart":
case "animationend":
case "animationiteration": {
const { animationName, elapsedTime, pseudoElement, } = event;
return {
animation_name: animationName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "transitionend": {
const { propertyName, elapsedTime, pseudoElement, } = event;
return {
property_name: propertyName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "abort":
case "canplay":
case "canplaythrough":
case "durationchange":
case "emptied":
case "encrypted":
case "ended":
case "error":
case "loadeddata":
case "loadedmetadata":
case "loadstart":
case "pause":
case "play":
case "playing":
case "progress":
case "ratechange":
case "seeked":
case "seeking":
case "stalled":
case "suspend":
case "timeupdate":
case "volumechange":
case "waiting": {
return {};
}
case "toggle": {
return {};
}
default: {
return {};
}
}
}
const bool_attrs = {
allowfullscreen: true,
allowpaymentrequest: true,
async: true,
autofocus: true,
autoplay: true,
checked: true,
controls: true,
default: true,
defer: true,
disabled: true,
formnovalidate: true,
hidden: true,
ismap: true,
itemscope: true,
loop: true,
multiple: true,
muted: true,
nomodule: true,
novalidate: true,
open: true,
playsinline: true,
readonly: true,
required: true,
reversed: true,
selected: true,
truespeed: true,
};
-13
View File
@@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ES2015",
"lib": [
"es2015",
"es5",
"es6",
"dom"
],
"strict": true,
}
}
+22 -1
View File
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Commit Statistics
<csr-read-only-do-not-edit/>
- 1 commit contributed to the release over the course of 7 calendar days.
- 0 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
### Commit Details
<csr-read-only-do-not-edit/>
<details><summary>view details</summary>
* **Uncategorized**
- Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d))
</details>
## v0.0.3 (2022-01-08)
### Documentation
@@ -28,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<csr-read-only-do-not-edit/>
- 32 commits contributed to the release over the course of 151 calendar days.
- 33 commits contributed to the release over the course of 151 calendar days.
- 21 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
@@ -39,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<details><summary>view details</summary>
* **Uncategorized**
- Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49))
- Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111))
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee))
+6 -1
View File
@@ -12,4 +12,9 @@ license = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-desktop = { path = "../desktop", version ="^0.1.5"}
dioxus-desktop = { path = "../desktop", version = "^0.1.6" }
[lib]
doctest = false
# tests suspended until package ready
test = false
+7
View File
@@ -0,0 +1,7 @@
[tasks.test]
command = "cargo"
args = [
"test",
"--no-run",
]
+57 -1
View File
@@ -5,6 +5,54 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### New Features
- <csr-id-e24957fc191476184816fe5dee249f691170d4ae/> enable use_router
- <csr-id-29ed7ebece26e9d53925af55f2f34a8fd8241405/> connect an onchange listener
- <csr-id-d2372717bd01fcff50af0572360e3f763d4c869d/> flatten props attrs
### Bug Fixes
- <csr-id-5ee9d6c4348a2f51adac827f715fb138918d1dc6/> attach router listener to subscriber list
- <csr-id-a21e7d4dd168129da06f535f9dc4b1de724617cb/> use_route should subscribe to changes to the route
### Commit Statistics
<csr-read-only-do-not-edit/>
- 18 commits contributed to the release over the course of 14 calendar days.
- 7 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
### Commit Details
<csr-read-only-do-not-edit/>
<details><summary>view details</summary>
* **Uncategorized**
- Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a))
- enable use_router ([`e24957f`](https://github.comgit//DioxusLabs/dioxus/commit/e24957fc191476184816fe5dee249f691170d4ae))
- add docs to router UseRouteListener ([`79e0993`](https://github.comgit//DioxusLabs/dioxus/commit/79e09934aa685d03d6e0b323723bc1cd537d74d9))
- Merge pull request #166 from DioxusLabs/jk/default-assets-desktop ([`ccbb955`](https://github.comgit//DioxusLabs/dioxus/commit/ccbb955b7b24bd4e1c5aa40e813a2872ae474a69))
- rustfmt ([`9da46eb`](https://github.comgit//DioxusLabs/dioxus/commit/9da46eb7bc207997ca7779c58fcb2a9645dfa9d0))
- Make log message in Link component trace level, not debug ([`72c6bb3`](https://github.comgit//DioxusLabs/dioxus/commit/72c6bb3d0b7253f084f7e3bcf55d458cb4adeedb))
- attach router listener to subscriber list ([`5ee9d6c`](https://github.comgit//DioxusLabs/dioxus/commit/5ee9d6c4348a2f51adac827f715fb138918d1dc6))
- connect an onchange listener ([`29ed7eb`](https://github.comgit//DioxusLabs/dioxus/commit/29ed7ebece26e9d53925af55f2f34a8fd8241405))
- use_route should subscribe to changes to the route ([`a21e7d4`](https://github.comgit//DioxusLabs/dioxus/commit/a21e7d4dd168129da06f535f9dc4b1de724617cb))
- Merge pull request #95 from DioxusLabs/jk/filedragindrop ([`ca0dd4a`](https://github.comgit//DioxusLabs/dioxus/commit/ca0dd4aa7192d483a195d420363f39d771f3e471))
- Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d))
- flatten props attrs ([`d237271`](https://github.comgit//DioxusLabs/dioxus/commit/d2372717bd01fcff50af0572360e3f763d4c869d))
- Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f))
- Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11))
- Merge pull request #138 from mrxiaozhuox/master ([`8c7473d`](https://github.comgit//DioxusLabs/dioxus/commit/8c7473d1943dd133f388ec36116c9d8295861b97))
- Add a warning when Link it called outside of a Router context ([`6408058`](https://github.comgit//DioxusLabs/dioxus/commit/64080588d02c54a1d380116cbecdd17de16d2392))
- Merge pull request #133 from mrxiaozhuox/master ([`887f69d`](https://github.comgit//DioxusLabs/dioxus/commit/887f69d5b47bdcde4fe0eab094c0cd0de23e4f3f))
- Fix handling of re-renders in the Router ([`81c094e`](https://github.comgit//DioxusLabs/dioxus/commit/81c094ed29fcdc5c6099492dd6ab09a59b79252c))
</details>
## v0.1.0 (2022-01-08)
### Documentation
@@ -33,7 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<csr-read-only-do-not-edit/>
- 42 commits contributed to the release over the course of 352 calendar days.
- 50 commits contributed to the release over the course of 352 calendar days.
- 35 commits where understood as [conventional](https://www.conventionalcommits.org).
- 0 issues like '(#ID)' where seen in commit messages
@@ -44,8 +92,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<details><summary>view details</summary>
* **Uncategorized**
- Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49))
- More WIP router implementation ([`e06eac1`](https://github.comgit//DioxusLabs/dioxus/commit/e06eac1ce5c6bb0a6680482574d82b16a141a626))
- Implement UseRoute segment method ([`c9408da`](https://github.comgit//DioxusLabs/dioxus/commit/c9408da7310423b1676fab6d41635f9a8000d89e))
- Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111))
- Implement router matching for path parameters ([`f8a7e1c`](https://github.comgit//DioxusLabs/dioxus/commit/f8a7e1cd8255fd7d0116384247e0e305bb73bf3d))
- Commit WIP on router ([`3c6142f`](https://github.comgit//DioxusLabs/dioxus/commit/3c6142fb9d8b5f715adf0fb30c2f3534b9ecd923))
- Add more trace messages to the RouterService code ([`3a5b417`](https://github.comgit//DioxusLabs/dioxus/commit/3a5b417ad1639a31e9aac9f41b05d8b3f074b128))
- Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307))
- Fix typo in RouterService struct's "registered_routes" field name ([`d367e0f`](https://github.comgit//DioxusLabs/dioxus/commit/d367e0f89f1b31484bde42efaf10c3531aed1d64))
- Add title prop to Link ([`e22ba5b`](https://github.comgit//DioxusLabs/dioxus/commit/e22ba5b1e52fe00e08e0b6ac0abae29b6068b8f0))
- add prevent default attribute and upgrade router ([`427b126`](https://github.comgit//DioxusLabs/dioxus/commit/427b126bc17336d5d14d56eb7fddb8e07752495f))
- memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c))
- bump all versions ([`4f92ba4`](https://github.comgit//DioxusLabs/dioxus/commit/4f92ba41602d706449c1bddabd49829873ee72eb))
+14 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-router"
version = "0.1.0"
version = "0.1.1"
edition = "2018"
description = "Dioxus VirtualDOM renderer for the web browser using websys"
license = "MIT/Apache-2.0"
@@ -11,13 +11,13 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-core = { path = "../core", version ="^0.1.7", default-features = false }
dioxus-html = { path = "../html", version ="^0.1.4", default-features = false }
dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"}
dioxus-core = { path = "../core", version = "^0.1.9", default-features = false }
dioxus-html = { path = "../html", version = "^0.1.6", default-features = false }
dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" }
serde = "1"
url = "2.2.2"
serde_urlencoded = "0.7"
# url = "2.2.2"
# for wasm
web-sys = { version = "0.3", features = [
@@ -43,9 +43,18 @@ web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"]
desktop = []
mobile = []
derive = []
wasm_test = []
[dev-dependencies]
console_error_panic_hook = "0.1.7"
dioxus-web = { path = "../web" }
log = "0.4.14"
wasm-logger = "0.2.0"
wasm-bindgen-test = "0.3"
gloo-utils = "0.1.2"
[dev-dependencies.web-sys]
version = "0.3"
features = [
"Document",
]
+10
View File
@@ -0,0 +1,10 @@
[tasks.test-with-browser]
extend = "core::wasm-pack-base"
command = "wasm-pack"
args = [
"test",
"@@split(DIOXUS_TEST_FLAGS, )",
"--",
"--features",
"${DIOXUS_TEST_FEATURES}",
]
+1 -1
View File
@@ -5,7 +5,7 @@ DioxusRouter adds React-Router style routing to your Dioxus apps. Works in brows
```rust
fn app() {
cx.render(rsx! {
Routes {
Router {
Route { to: "/", Component {} },
Route { to: "/blog", Blog {} },
Route { to: "/blog/:id", BlogPost {} },
+5 -13
View File
@@ -1,8 +1,9 @@
#![allow(non_snake_case)]
use dioxus_core::prelude::*;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
use dioxus_router::*;
use serde::{Deserialize, Serialize};
fn main() {
console_error_panic_hook::set_once();
@@ -25,26 +26,17 @@ static APP: Component = |cx| {
fn Home(cx: Scope) -> Element {
cx.render(rsx! {
div {
h1 { "Home" }
}
h1 { "Home" }
})
}
fn BlogList(cx: Scope) -> Element {
cx.render(rsx! {
div {
}
div { "Blog List" }
})
}
fn BlogPost(cx: Scope) -> Element {
let id = use_route(&cx).segment::<usize>("id")?;
cx.render(rsx! {
div {
}
})
cx.render(rsx! { div { "{id:?}" } })
}
+40 -8
View File
@@ -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::debug!("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
}
+2 -2
View File
@@ -30,7 +30,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
Some(ctx) => ctx.total_route.to_string(),
None => cx.props.to.to_string(),
};
log::trace!("total route for {} is {}", cx.props.to, total_route);
// log::trace!("total route for {} is {}", cx.props.to, total_route);
// provide our route context
let route_context = cx.provide_context(RouteContext {
@@ -48,7 +48,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
Some(RouteInner {})
});
log::trace!("Checking route {}", cx.props.to);
// log::trace!("Checking route {}", cx.props.to);
if router_root.should_render(cx.scope_id()) {
cx.render(rsx!(&cx.props.children))
+14 -6
View File
@@ -11,18 +11,26 @@ use crate::RouterService;
pub struct RouterProps<'a> {
children: Element<'a>,
#[props(default, strip_option)]
onchange: Option<&'a dyn Fn(&'a str)>,
#[props(default)]
onchange: EventHandler<'a, String>,
}
#[allow(non_snake_case)]
pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
cx.use_hook(|_| {
log::debug!("running router {:?}", cx.scope_id());
let svc = cx.use_hook(|_| {
let update = cx.schedule_update_any();
cx.provide_context(RouterService::new(update, cx.scope_id()))
});
cx.render(rsx!(
div { &cx.props.children }
))
let any_pending = svc.pending_events.borrow().len() > 0;
svc.pending_events.borrow_mut().clear();
if any_pending {
let location = svc.current_location();
let path = location.path();
cx.props.onchange.call(path.to_string());
}
cx.render(rsx!(&cx.props.children))
}
+38 -7
View File
@@ -1,4 +1,4 @@
use dioxus_core::ScopeState;
use dioxus_core::{ScopeId, ScopeState};
use gloo::history::{HistoryResult, Location};
use serde::de::DeserializeOwned;
use std::{rc::Rc, str::FromStr};
@@ -8,6 +8,7 @@ use crate::RouterService;
/// This struct provides is a wrapper around the internal router
/// implementation, with methods for getting information about the current
/// route.
#[derive(Clone)]
pub struct UseRoute {
router: Rc<RouterService>,
}
@@ -74,10 +75,40 @@ impl UseRoute {
/// This hook provides access to information about the current location in the
/// context of a [`Router`]. If this function is called outside of a `Router`
/// component it will panic.
pub fn use_route(cx: &ScopeState) -> UseRoute {
let router = cx
.consume_context::<RouterService>()
.expect("Cannot call use_route outside the scope of a Router component")
.clone();
UseRoute { router }
pub fn use_route(cx: &ScopeState) -> &UseRoute {
&cx.use_hook(|_| {
let router = cx
.consume_context::<RouterService>()
.expect("Cannot call use_route outside the scope of a Router component");
router.subscribe_onchange(cx.scope_id());
UseRouteListener {
router: UseRoute { router },
scope: cx.scope_id(),
}
})
.router
}
// The entire purpose of this struct is to unubscribe this component when it is unmounted.
// The UseRoute can be cloned into async contexts, so we can't rely on its drop to unubscribe.
// Instead, we hide the drop implementation on this private type exclusive to the hook,
// and reveal our cached version of UseRoute to the component.
struct UseRouteListener {
router: UseRoute,
scope: ScopeId,
}
impl Drop for UseRouteListener {
fn drop(&mut self) {
self.router.router.unsubscribe_onchange(self.scope)
}
}
/// This hook provides access to the `RouterService` for the app.
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")
})
}
+1 -14
View File
@@ -8,22 +8,9 @@
//!
//! ```rust
//! fn app(cx: Scope) -> Element {
//!
//! }
//!
//!
//!
//!
//!
//! ```
//!
//!
//!
//!
//!
//!
//!
//!
//!
mod hooks {
mod use_route;
+1 -6
View File
@@ -1,9 +1,4 @@
pub trait RouterProvider {
fn get_current_route(&self) -> String;
fn subscribe_to_route_changes(&self, callback: Box<dyn Fn(String)>);
}
enum RouteChange {
LinkTo(String),
Back(String),
fn listen(&self, callback: Box<dyn Fn()>);
}
+50 -46
View File
@@ -1,21 +1,33 @@
use gloo::history::{BrowserHistory, History, HistoryListener, Location};
use std::{
cell::{Cell, Ref, RefCell},
collections::HashMap,
collections::{HashMap, HashSet},
rc::Rc,
};
use dioxus_core::ScopeId;
use crate::platform::RouterProvider;
pub struct RouterService {
pub(crate) regen_route: Rc<dyn Fn(ScopeId)>,
history: Rc<RefCell<BrowserHistory>>,
pub(crate) pending_events: Rc<RefCell<Vec<RouteEvent>>>,
slots: Rc<RefCell<Vec<(ScopeId, String)>>>,
onchange_listeners: Rc<RefCell<HashSet<ScopeId>>>,
root_found: Rc<Cell<Option<ScopeId>>>,
cur_path_params: Rc<RefCell<HashMap<String, String>>>,
// history: Rc<dyn RouterProvider>,
history: Rc<RefCell<BrowserHistory>>,
listener: HistoryListener,
}
pub enum RouteEvent {
Change,
Pop,
Push,
}
enum RouteSlot {
Routes {
// the partial route
@@ -35,58 +47,66 @@ impl RouterService {
let location = history.location();
let path = location.path();
let onchange_listeners = Rc::new(RefCell::new(HashSet::new()));
let slots: Rc<RefCell<Vec<(ScopeId, String)>>> = Default::default();
let _slots = slots.clone();
let pending_events: Rc<RefCell<Vec<RouteEvent>>> = Default::default();
let root_found = Rc::new(Cell::new(None));
let regen = regen_route.clone();
let _root_found = root_found.clone();
let listener = history.listen(move || {
_root_found.set(None);
// checking if the route is valid is cheap, so we do it
for (slot, root) in _slots.borrow_mut().iter().rev() {
log::trace!("regenerating slot {:?} for root '{}'", slot, root);
regen(*slot);
let listener = history.listen({
let pending_events = pending_events.clone();
let regen_route = regen_route.clone();
let root_found = root_found.clone();
let slots = slots.clone();
let onchange_listeners = onchange_listeners.clone();
move || {
root_found.set(None);
// checking if the route is valid is cheap, so we do it
for (slot, root) in slots.borrow_mut().iter().rev() {
regen_route(*slot);
}
for listener in onchange_listeners.borrow_mut().iter() {
regen_route(*listener);
}
// also regenerate the root
regen_route(root_scope);
pending_events.borrow_mut().push(RouteEvent::Change)
}
});
Self {
listener,
root_found,
history: Rc::new(RefCell::new(history)),
regen_route,
slots,
pending_events,
onchange_listeners,
cur_path_params: Rc::new(RefCell::new(HashMap::new())),
listener,
}
}
pub fn push_route(&self, route: &str) {
log::trace!("Pushing route: {}", route);
self.history.borrow_mut().push(route);
}
pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) {
let clean = clean_route(route);
log::trace!("Registered route '{}' with scope id {:?}", clean, scope);
self.slots.borrow_mut().push((scope, clean));
}
pub fn should_render(&self, scope: ScopeId) -> bool {
log::trace!("Should render scope id {:?}?", scope);
if let Some(root_id) = self.root_found.get() {
log::trace!(" we already found a root with scope id {:?}", root_id);
if root_id == scope {
log::trace!(" yes - it's a match");
return true;
}
log::trace!(" no - it's not a match");
return false;
}
let location = self.history.borrow().location();
let path = location.path();
log::trace!(" current path is '{}'", path);
let roots = self.slots.borrow();
@@ -95,23 +115,15 @@ impl RouterService {
// fallback logic
match root {
Some((id, route)) => {
log::trace!(
" matched given scope id {:?} with route root '{}'",
scope,
route,
);
if let Some(params) = route_matches_path(route, path) {
log::trace!(" and it matches the current path '{}'", path);
self.root_found.set(Some(*id));
*self.cur_path_params.borrow_mut() = params;
true
} else {
if route == "" {
log::trace!(" and the route is the root, so we will use that without a better match");
self.root_found.set(Some(*id));
true
} else {
log::trace!(" and the route '{}' is not the root nor does it match the current path", route);
false
}
}
@@ -127,6 +139,14 @@ impl RouterService {
pub fn current_path_params(&self) -> Ref<HashMap<String, String>> {
self.cur_path_params.borrow()
}
pub fn subscribe_onchange(&self, id: ScopeId) {
self.onchange_listeners.borrow_mut().insert(id);
}
pub fn unsubscribe_onchange(&self, id: ScopeId) {
self.onchange_listeners.borrow_mut().remove(&id);
}
}
fn clean_route(route: String) -> String {
@@ -147,36 +167,20 @@ fn route_matches_path(route: &str, path: &str) -> Option<HashMap<String, String>
let route_pieces = route.split('/').collect::<Vec<_>>();
let path_pieces = clean_path(path).split('/').collect::<Vec<_>>();
log::trace!(
" checking route pieces {:?} vs path pieces {:?}",
route_pieces,
path_pieces,
);
if route_pieces.len() != path_pieces.len() {
log::trace!(" the routes are different lengths");
return None;
}
let mut matches = HashMap::new();
for (i, r) in route_pieces.iter().enumerate() {
log::trace!(" checking route piece '{}' vs path", r);
// If this is a parameter then it matches as long as there's
// _any_thing in that spot in the path.
if r.starts_with(':') {
log::trace!(
" route piece '{}' starts with a colon so it matches anything",
r,
);
let param = &r[1..];
matches.insert(param.to_string(), path_pieces[i].to_string());
continue;
}
log::trace!(
" route piece '{}' must be an exact match for path piece '{}'",
r,
path_pieces[i],
);
if path_pieces[i] != *r {
return None;
}
+63
View File
@@ -0,0 +1,63 @@
#![cfg(target_arch = "wasm32")]
use dioxus_core::prelude::*;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
use dioxus_router::*;
use gloo_utils::document;
use serde::{Deserialize, Serialize};
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn simple_test() {
fn main() {
console_error_panic_hook::set_once();
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
dioxus_web::launch(APP);
}
static APP: Component = |cx| {
cx.render(rsx! {
Router {
onchange: move |route| log::info!("route changed to {}", route),
Route { to: "/", Home {} }
Route { to: "blog"
Route { to: "/", BlogList {} }
Route { to: ":id", BlogPost {} }
}
}
})
};
fn Home(cx: Scope) -> Element {
cx.render(rsx! {
div {
h1 { "Home" }
}
})
}
fn BlogList(cx: Scope) -> Element {
cx.render(rsx! {
div {
}
})
}
fn BlogPost(cx: Scope) -> Element {
let id = use_route(&cx).segment::<usize>("id")?;
cx.render(rsx! {
div {
}
})
}
main();
let element = gloo_utils::document();
}
+11
View File
@@ -0,0 +1,11 @@
{
"moz:firefoxOptions": {
"binary": "/usr/bin/firefox",
"prefs": {
"media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true
},
"args": []
}
}
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "dioxus-rsx"
version = "0.0.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
once_cell = "1.8"
proc-macro-error = "1.0.4"
proc-macro2 = { version = "1.0.6" }
quote = "1.0"
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
@@ -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 {
@@ -135,11 +135,14 @@ impl ToTokens for Component {
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
)
})
}
@@ -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 {
Loaded 100 of 213 files, more files were not shown because too many files have changed in this diff. Show more