From 45ebcf6f0e9f1f86601e83d83a55797e21628a85 Mon Sep 17 00:00:00 2001 From: JtotheThree Date: Fri, 7 Jan 2022 20:01:52 -0600 Subject: [PATCH 001/129] form prevent_default --- packages/html/src/elements.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 1a2f36e57..4b53f91f0 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -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) } @@ -1106,6 +1106,12 @@ impl a { } } +impl form { + 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 /// Build a From e69fbb4baa3de3902a746a0cb2b06338e33dcaa9 Mon Sep 17 00:00:00 2001 From: JtotheThree Date: Fri, 7 Jan 2022 20:20:52 -0600 Subject: [PATCH 002/129] login_form example --- examples/login_form.rs | 89 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 examples/login_form.rs diff --git a/examples/login_form.rs b/examples/login_form.rs new file mode 100644 index 000000000..fdd1843ce --- /dev/null +++ b/examples/login_form.rs @@ -0,0 +1,89 @@ +//! This example demonstrates the following: +//! Futures in a callback, Router, and Forms + +use dioxus::events::*; +use dioxus::prelude::*; +use dioxus::router::{Link, Router, Route, RouterService}; + +fn main() { + dioxus::desktop::launch(APP); +} + +static APP: Component = |cx| { + cx.render(rsx!{ + Router { + Route { to: "/", home() } + Route { to: "/login", login() } + } + }) +}; + +fn home(cx: Scope) -> Element { + cx.render(rsx! { + h1 { "Welcome Home" } + Link { to: "/login", "Login" } + }) +} + + +fn login(cx: Scope) -> Element { + let username = use_state(&cx, String::new); + let password = use_state(&cx, String::new); + + let service = cx.consume_context::()?; + + let onsubmit = move |_| { + cx.push_future({ + let (username, password) = (username.get().clone(), password.get().clone()); + let service = service.clone(); + + async move { + let params = [ + ("username", username.to_string()), + ("password", password.to_string()) + ]; + + let resp = reqwest::Client::new() + .post("http://localhost/login") + .form(¶ms) + .send() + .await; + + match resp { + Ok(data) => { + // Parse data from here, such as storing a response token + service.push_route("/"); + } + Err(err) => {} //Handle any errors from the fetch here + } + } + }); + }; + + cx.render(rsx!{ + h1 { "Login" } + form { + onsubmit: onsubmit, + // Prevent the default behavior of
to post + prevent_default: "onsubmit", + input { + oninput: move |evt| username.set(evt.value.clone()) + } + label { + "Username" + } + br {} + input { + oninput: move |evt| password.set(evt.value.clone()), + r#type: "password" + } + label { + "Password" + } + br {} + button { + "Login" + } + } + }) +} \ No newline at end of file From abfac0d59b620cdf3af77c80aadb07266e0d651b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 17 Jan 2022 16:37:44 -0500 Subject: [PATCH 003/129] wip: move macro lib out of proc macro crate --- Cargo.toml | 5 +- examples/rsx_autocomplete.rs | 27 ++++++ packages/core-macro/Cargo.toml | 1 + packages/core-macro/src/lib.rs | 7 +- packages/macro-inner/Cargo.toml | 13 +++ .../{core-macro => macro-inner}/src/htm.rs | 0 .../{core-macro => macro-inner}/src/ifmt.rs | 0 .../src/inlineprops.rs | 0 packages/macro-inner/src/lib.rs | 5 + .../src/props/mod.rs | 0 .../{core-macro => macro-inner}/src/router.rs | 0 .../src/rsx/component.rs | 8 +- .../src/rsx/element.rs | 93 ++++++++++++------- .../src/rsx/mod.rs | 4 +- .../src/rsx/node.rs | 0 .../src/rsxtemplate.rs | 0 16 files changed, 116 insertions(+), 47 deletions(-) create mode 100644 examples/rsx_autocomplete.rs create mode 100644 packages/macro-inner/Cargo.toml rename packages/{core-macro => macro-inner}/src/htm.rs (100%) rename packages/{core-macro => macro-inner}/src/ifmt.rs (100%) rename packages/{core-macro => macro-inner}/src/inlineprops.rs (100%) create mode 100644 packages/macro-inner/src/lib.rs rename packages/{core-macro => macro-inner}/src/props/mod.rs (100%) rename packages/{core-macro => macro-inner}/src/router.rs (100%) rename packages/{core-macro => macro-inner}/src/rsx/component.rs (98%) rename packages/{core-macro => macro-inner}/src/rsx/element.rs (80%) rename packages/{core-macro => macro-inner}/src/rsx/mod.rs (97%) rename packages/{core-macro => macro-inner}/src/rsx/node.rs (100%) rename packages/{core-macro => macro-inner}/src/rsxtemplate.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 884513a5b..98da05bcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "./packages/core", version = "^0.1.7" } dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.6", optional = true } +dioxus-macro-inner = { path = "./packages/macro-inner", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } @@ -27,7 +28,7 @@ dioxus-mobile = { path = "./packages/mobile", version = "^0.0.3", optional = tru [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro"] +macro = ["dioxus-core-macro", "dioxus-macro-inner"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] @@ -35,6 +36,7 @@ web = ["dioxus-web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] + # "dioxus-router/web" # "dioxus-router/desktop" # desktop = ["dioxus-desktop", "dioxus-router/desktop"] @@ -46,6 +48,7 @@ router = ["dioxus-router"] members = [ "packages/core", "packages/core-macro", + "packages/macro-inner", "packages/html", "packages/hooks", "packages/web", diff --git a/examples/rsx_autocomplete.rs b/examples/rsx_autocomplete.rs new file mode 100644 index 000000000..99afe7521 --- /dev/null +++ b/examples/rsx_autocomplete.rs @@ -0,0 +1,27 @@ +//! This example shows that autocomplete works in RSX + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + onclick: move |_| { + } + // class: "asd", + // style { + // media: "Ad", + // } + // div { + + // } + // { + // let t = String::new(); + // t. + // } + } + }) +} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index ee3dc2883..8f2b81cba 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] +dioxus-macro-inner = { path = "../macro-inner" } once_cell = "1.8" proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 4ad7543a2..3579039b5 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -1,13 +1,8 @@ +use dioxus_macro_inner::*; 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; - #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { use ifmt::*; diff --git a/packages/macro-inner/Cargo.toml b/packages/macro-inner/Cargo.toml new file mode 100644 index 000000000..63fcbbd9b --- /dev/null +++ b/packages/macro-inner/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dioxus-macro-inner" +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"] } diff --git a/packages/core-macro/src/htm.rs b/packages/macro-inner/src/htm.rs similarity index 100% rename from packages/core-macro/src/htm.rs rename to packages/macro-inner/src/htm.rs diff --git a/packages/core-macro/src/ifmt.rs b/packages/macro-inner/src/ifmt.rs similarity index 100% rename from packages/core-macro/src/ifmt.rs rename to packages/macro-inner/src/ifmt.rs diff --git a/packages/core-macro/src/inlineprops.rs b/packages/macro-inner/src/inlineprops.rs similarity index 100% rename from packages/core-macro/src/inlineprops.rs rename to packages/macro-inner/src/inlineprops.rs diff --git a/packages/macro-inner/src/lib.rs b/packages/macro-inner/src/lib.rs new file mode 100644 index 000000000..69e4b743f --- /dev/null +++ b/packages/macro-inner/src/lib.rs @@ -0,0 +1,5 @@ +pub mod ifmt; +pub mod inlineprops; +pub mod props; +pub mod router; +pub mod rsx; diff --git a/packages/core-macro/src/props/mod.rs b/packages/macro-inner/src/props/mod.rs similarity index 100% rename from packages/core-macro/src/props/mod.rs rename to packages/macro-inner/src/props/mod.rs diff --git a/packages/core-macro/src/router.rs b/packages/macro-inner/src/router.rs similarity index 100% rename from packages/core-macro/src/router.rs rename to packages/macro-inner/src/router.rs diff --git a/packages/core-macro/src/rsx/component.rs b/packages/macro-inner/src/rsx/component.rs similarity index 98% rename from packages/core-macro/src/rsx/component.rs rename to packages/macro-inner/src/rsx/component.rs index 5257022a4..26305359d 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/macro-inner/src/rsx/component.rs @@ -23,10 +23,10 @@ use syn::{ }; pub struct Component { - name: syn::Path, - body: Vec, - children: Vec, - manual_props: Option, + pub name: syn::Path, + pub body: Vec, + pub children: Vec, + pub manual_props: Option, } impl Parse for Component { diff --git a/packages/core-macro/src/rsx/element.rs b/packages/macro-inner/src/rsx/element.rs similarity index 80% rename from packages/core-macro/src/rsx/element.rs rename to packages/macro-inner/src/rsx/element.rs index ebc401b6e..c612e9f1b 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/macro-inner/src/rsx/element.rs @@ -4,19 +4,18 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, - Expr, Ident, LitStr, Result, Token, + Expr, ExprClosure, Ident, LitStr, Result, Token, }; // ======================================= // Parse the VNode::Element type // ======================================= pub struct Element { - name: Ident, - key: Option, - attributes: Vec, - listeners: Vec, - children: Vec, - _is_static: bool, + pub name: Ident, + pub key: Option, + pub attributes: Vec, + pub children: Vec, + pub _is_static: bool, } impl Parse for Element { @@ -28,7 +27,6 @@ impl Parse for Element { syn::braced!(content in stream); let mut attributes: Vec = vec![]; - let mut listeners: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; let mut _el_ref = None; @@ -54,6 +52,7 @@ impl Parse for Element { }); } else { let value = content.parse::()?; + attributes.push(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr::CustomAttrExpression { name, value }, @@ -82,13 +81,24 @@ impl Parse for Element { content.parse::()?; if name_str.starts_with("on") { - listeners.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: content.parse()?, - }, - }); + if content.fork().parse::().is_ok() { + // + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventClosure { + name, + closure: content.parse()?, + }, + }); + } else { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventTokens { + name, + tokens: content.parse()?, + }, + }); + } } else { match name_str.as_str() { "key" => { @@ -182,7 +192,7 @@ impl Parse for Element { name: el_name, attributes, children, - listeners, + // listeners, _is_static: false, }) } @@ -193,14 +203,29 @@ impl ToTokens for Element { let name = &self.name; let children = &self.children; - let listeners = &self.listeners; - let attr = &self.attributes; + // let listeners = &self.listeners; let key = match &self.key { Some(ty) => quote! { Some(format_args_f!(#ty)) }, None => quote! { None }, }; + let listeners = self.attributes.iter().filter(|f| { + if let ElementAttr::EventTokens { .. } = f.attr { + true + } else { + false + } + }); + + let attr = self.attributes.iter().filter(|f| { + if let ElementAttr::EventTokens { .. } = f.attr { + false + } else { + true + } + }); + tokens.append_all(quote! { __cx.element( dioxus_elements::#name, @@ -213,29 +238,29 @@ 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 |_| {} - // EventClosure { name: Ident, closure: ExprClosure }, + /// 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 { @@ -263,11 +288,11 @@ impl ToTokens for ElementAttrNamed { __cx.attr( #name, format_args_f!(#value), None, false ) } } - // ElementAttr::EventClosure { name, closure } => { - // quote! { - // dioxus_elements::on::#name(__cx, #closure) - // } - // } + ElementAttr::EventClosure { name, closure } => { + quote! { + dioxus_elements::on::#name(__cx, #closure) + } + } ElementAttr::EventTokens { name, tokens } => { quote! { dioxus_elements::on::#name(__cx, #tokens) diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/macro-inner/src/rsx/mod.rs similarity index 97% rename from packages/core-macro/src/rsx/mod.rs rename to packages/macro-inner/src/rsx/mod.rs index 5ff5d85f7..87b324dfc 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/macro-inner/src/rsx/mod.rs @@ -29,8 +29,8 @@ use syn::{ }; pub struct CallBody { - custom_context: Option, - roots: Vec, + pub custom_context: Option, + pub roots: Vec, } impl Parse for CallBody { diff --git a/packages/core-macro/src/rsx/node.rs b/packages/macro-inner/src/rsx/node.rs similarity index 100% rename from packages/core-macro/src/rsx/node.rs rename to packages/macro-inner/src/rsx/node.rs diff --git a/packages/core-macro/src/rsxtemplate.rs b/packages/macro-inner/src/rsxtemplate.rs similarity index 100% rename from packages/core-macro/src/rsxtemplate.rs rename to packages/macro-inner/src/rsxtemplate.rs From 174d2870625c8f052b041207c15c1062e8356c14 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 17 Jan 2022 23:02:36 -0500 Subject: [PATCH 004/129] wip: extract to rsx module --- Cargo.toml | 4 +- .../{macro-inner => core-macro}/src/ifmt.rs | 0 .../src/inlineprops.rs | 0 .../src/props/mod.rs | 0 packages/macro-inner/src/router.rs | 214 ------------------ packages/macro-inner/src/rsxtemplate.rs | 84 ------- packages/{macro-inner => rsx}/Cargo.toml | 2 +- packages/{macro-inner => rsx}/src/htm.rs | 0 packages/{macro-inner => rsx}/src/lib.rs | 1 - .../{macro-inner => rsx}/src/rsx/component.rs | 0 .../{macro-inner => rsx}/src/rsx/element.rs | 0 packages/{macro-inner => rsx}/src/rsx/mod.rs | 0 packages/{macro-inner => rsx}/src/rsx/node.rs | 0 13 files changed, 3 insertions(+), 302 deletions(-) rename packages/{macro-inner => core-macro}/src/ifmt.rs (100%) rename packages/{macro-inner => core-macro}/src/inlineprops.rs (100%) rename packages/{macro-inner => core-macro}/src/props/mod.rs (100%) delete mode 100644 packages/macro-inner/src/router.rs delete mode 100644 packages/macro-inner/src/rsxtemplate.rs rename packages/{macro-inner => rsx}/Cargo.toml (91%) rename packages/{macro-inner => rsx}/src/htm.rs (100%) rename packages/{macro-inner => rsx}/src/lib.rs (79%) rename packages/{macro-inner => rsx}/src/rsx/component.rs (100%) rename packages/{macro-inner => rsx}/src/rsx/element.rs (100%) rename packages/{macro-inner => rsx}/src/rsx/mod.rs (100%) rename packages/{macro-inner => rsx}/src/rsx/node.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 98da05bcc..c8075c545 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "./packages/core", version = "^0.1.7" } dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.6", optional = true } -dioxus-macro-inner = { path = "./packages/macro-inner", optional = true } +dioxus-macro-inner = { path = "./packages/rsx", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } @@ -48,7 +48,7 @@ router = ["dioxus-router"] members = [ "packages/core", "packages/core-macro", - "packages/macro-inner", + "packages/rsx", "packages/html", "packages/hooks", "packages/web", diff --git a/packages/macro-inner/src/ifmt.rs b/packages/core-macro/src/ifmt.rs similarity index 100% rename from packages/macro-inner/src/ifmt.rs rename to packages/core-macro/src/ifmt.rs diff --git a/packages/macro-inner/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs similarity index 100% rename from packages/macro-inner/src/inlineprops.rs rename to packages/core-macro/src/inlineprops.rs diff --git a/packages/macro-inner/src/props/mod.rs b/packages/core-macro/src/props/mod.rs similarity index 100% rename from packages/macro-inner/src/props/mod.rs rename to packages/core-macro/src/props/mod.rs diff --git a/packages/macro-inner/src/router.rs b/packages/macro-inner/src/router.rs deleted file mode 100644 index 3edad75e3..000000000 --- a/packages/macro-inner/src/router.rs +++ /dev/null @@ -1,214 +0,0 @@ -#![allow(dead_code)] - -use proc_macro2::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant}; - -const AT_ATTR_IDENT: &str = "at"; -const NOT_FOUND_ATTR_IDENT: &str = "not_found"; - -pub struct Routable { - ident: Ident, - ats: Vec, - variants: Punctuated, - not_found_route: Option, -} - -impl Parse for Routable { - fn parse(input: ParseStream) -> syn::Result { - let DeriveInput { ident, data, .. } = input.parse()?; - - let data = match data { - Data::Enum(data) => data, - Data::Struct(s) => { - return Err(syn::Error::new( - s.struct_token.span(), - "expected enum, found struct", - )) - } - Data::Union(u) => { - return Err(syn::Error::new( - u.union_token.span(), - "expected enum, found union", - )) - } - }; - - let (not_found_route, ats) = parse_variants_attributes(&data.variants)?; - - Ok(Self { - ident, - variants: data.variants, - ats, - not_found_route, - }) - } -} - -fn parse_variants_attributes( - variants: &Punctuated, -) -> syn::Result<(Option, Vec)> { - let mut not_founds = vec![]; - let mut ats: Vec = vec![]; - - let mut not_found_attrs = vec![]; - - for variant in variants.iter() { - if let Fields::Unnamed(ref field) = variant.fields { - return Err(syn::Error::new( - field.span(), - "only named fields are supported", - )); - } - - let attrs = &variant.attrs; - let at_attrs = attrs - .iter() - .filter(|attr| attr.path.is_ident(AT_ATTR_IDENT)) - .collect::>(); - - let attr = match at_attrs.len() { - 1 => *at_attrs.first().unwrap(), - 0 => { - return Err(syn::Error::new( - variant.span(), - format!( - "{} attribute must be present on every variant", - AT_ATTR_IDENT - ), - )) - } - _ => { - return Err(syn::Error::new_spanned( - quote! { #(#at_attrs)* }, - format!("only one {} attribute must be present", AT_ATTR_IDENT), - )) - } - }; - - let lit = attr.parse_args::()?; - ats.push(lit); - - for attr in attrs.iter() { - if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) { - not_found_attrs.push(attr); - not_founds.push(variant.ident.clone()) - } - } - } - - if not_founds.len() > 1 { - return Err(syn::Error::new_spanned( - quote! { #(#not_found_attrs)* }, - format!("there can only be one {}", NOT_FOUND_ATTR_IDENT), - )); - } - - Ok((not_founds.into_iter().next(), ats)) -} - -impl Routable { - // fn build_from_path(&self) -> TokenStream { - // let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| { - // let ident = &variant.ident; - // let right = match &variant.fields { - // Fields::Unit => quote! { Self::#ident }, - // Fields::Named(field) => { - // let fields = field.named.iter().map(|it| { - // //named fields have idents - // it.ident.as_ref().unwrap() - // }); - // quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } } - // } - // Fields::Unnamed(_) => unreachable!(), // already checked - // }; - - // let left = self.ats.get(i).unwrap(); - // quote! { - // #left => ::std::option::Option::Some(#right) - // } - // }); - - // quote! { - // fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option { - // match path { - // #(#from_path_matches),*, - // _ => ::std::option::Option::None, - // } - // } - // } - // } - - // fn build_to_path(&self) -> TokenStream { - // let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| { - // let ident = &variant.ident; - // let mut right = self.ats.get(i).unwrap().value(); - - // match &variant.fields { - // Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) }, - // Fields::Named(field) => { - // let fields = field - // .named - // .iter() - // .map(|it| it.ident.as_ref().unwrap()) - // .collect::>(); - - // for field in fields.iter() { - // // :param -> {param} - // // so we can pass it to `format!("...", param)` - // right = right.replace(&format!(":{}", field), &format!("{{{}}}", field)) - // } - - // quote! { - // Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*) - // } - // } - // Fields::Unnamed(_) => unreachable!(), // already checked - // } - // }); - - // quote! { - // fn to_path(&self) -> ::std::string::String { - // match self { - // #(#to_path_matches),*, - // } - // } - // } - // } -} - -pub fn routable_derive_impl(input: Routable) -> TokenStream { - let Routable { - // ats, - // not_found_route, - // ident, - .. - } = &input; - - // let from_path = input.build_from_path(); - // let to_path = input.build_to_path(); - - quote! { - // #[automatically_derived] - // impl ::dioxus::router::Routable for #ident { - - // fn recognize(pathname: &str) -> ::std::option::Option { - // todo!() - // // ::std::thread_local! { - // // static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>(); - // // } - // // let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname)); - // // { - // // let route = ::std::clone::Clone::clone(&route); - // // #cache_thread_local_ident.with(move |val| { - // // *val.borrow_mut() = route; - // // }); - // // } - // // route - // } - // } - } -} diff --git a/packages/macro-inner/src/rsxtemplate.rs b/packages/macro-inner/src/rsxtemplate.rs deleted file mode 100644 index 9222d7117..000000000 --- a/packages/macro-inner/src/rsxtemplate.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::{rsx::RsxBody, util::is_valid_svg_tag}; - -use { - proc_macro::TokenStream, - proc_macro2::{Span, TokenStream as TokenStream2}, - quote::{quote, ToTokens, TokenStreamExt}, - syn::{ - ext::IdentExt, - parse::{Parse, ParseStream}, - token, Error, Expr, ExprClosure, Ident, LitBool, LitStr, Path, Result, Token, - }, -}; - -// ============================================== -// Parse any stream coming from the html! macro -// ============================================== -pub struct RsxTemplate { - inner: RsxBody, -} - -impl Parse for RsxTemplate { - fn parse(s: ParseStream) -> Result { - if s.peek(LitStr) { - use std::str::FromStr; - - let lit = s.parse::()?; - let g = lit.span(); - let mut value = lit.value(); - if value.ends_with('\n') { - value.pop(); - if value.ends_with('\r') { - value.pop(); - } - } - let lit = LitStr::new(&value, lit.span()); - - // panic!("{:#?}", lit); - match lit.parse::() { - Ok(r) => Ok(Self { inner: r }), - Err(e) => Err(e), - } - } else { - panic!("Not a str lit") - } - // let t = s.parse::()?; - - // let new_stream = TokenStream::from(t.to_s) - - // let cx: Ident = s.parse()?; - // s.parse::()?; - // if elements are in an array, return a bumpalo::collections::Vec rather than a Node. - // let kind = if s.peek(token::Bracket) { - // let nodes_toks; - // syn::bracketed!(nodes_toks in s); - // let mut nodes: Vec> = vec![nodes_toks.parse()?]; - // while nodes_toks.peek(Token![,]) { - // nodes_toks.parse::()?; - // nodes.push(nodes_toks.parse()?); - // } - // NodeOrList::List(NodeList(nodes)) - // } else { - // NodeOrList::Node(s.parse()?) - // }; - // Ok(HtmlRender { kind }) - } -} - -impl ToTokens for RsxTemplate { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - self.inner.to_tokens(out_tokens); - // let new_toks = ToToksCtx::new(&self.kind).to_token_stream(); - - // // create a lazy tree that accepts a bump allocator - // let final_tokens = quote! { - // dioxus::prelude::LazyNodes::new(move |cx| { - // let bump = &cx.bump(); - - // #new_toks - // }) - // }; - - // final_tokens.to_tokens(out_tokens); - } -} diff --git a/packages/macro-inner/Cargo.toml b/packages/rsx/Cargo.toml similarity index 91% rename from packages/macro-inner/Cargo.toml rename to packages/rsx/Cargo.toml index 63fcbbd9b..4cad9064c 100644 --- a/packages/macro-inner/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dioxus-macro-inner" +name = "dioxus-rsx" version = "0.0.0" edition = "2018" diff --git a/packages/macro-inner/src/htm.rs b/packages/rsx/src/htm.rs similarity index 100% rename from packages/macro-inner/src/htm.rs rename to packages/rsx/src/htm.rs diff --git a/packages/macro-inner/src/lib.rs b/packages/rsx/src/lib.rs similarity index 79% rename from packages/macro-inner/src/lib.rs rename to packages/rsx/src/lib.rs index 69e4b743f..b9911dedc 100644 --- a/packages/macro-inner/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -1,5 +1,4 @@ pub mod ifmt; pub mod inlineprops; pub mod props; -pub mod router; pub mod rsx; diff --git a/packages/macro-inner/src/rsx/component.rs b/packages/rsx/src/rsx/component.rs similarity index 100% rename from packages/macro-inner/src/rsx/component.rs rename to packages/rsx/src/rsx/component.rs diff --git a/packages/macro-inner/src/rsx/element.rs b/packages/rsx/src/rsx/element.rs similarity index 100% rename from packages/macro-inner/src/rsx/element.rs rename to packages/rsx/src/rsx/element.rs diff --git a/packages/macro-inner/src/rsx/mod.rs b/packages/rsx/src/rsx/mod.rs similarity index 100% rename from packages/macro-inner/src/rsx/mod.rs rename to packages/rsx/src/rsx/mod.rs diff --git a/packages/macro-inner/src/rsx/node.rs b/packages/rsx/src/rsx/node.rs similarity index 100% rename from packages/macro-inner/src/rsx/node.rs rename to packages/rsx/src/rsx/node.rs From aef414ac0f48e938bbd93f5dba79aa32aaf444fa Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 01:44:15 -0500 Subject: [PATCH 005/129] wip: use_coroutine new behavior --- packages/hooks/src/usecoroutine.rs | 178 ++++++++++++++--------------- 1 file changed, 86 insertions(+), 92 deletions(-) diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 7faf507ed..593371b23 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -1,115 +1,93 @@ -use dioxus_core::{ScopeState, TaskId}; +use bumpalo::boxed::Box as BumpBox; +use dioxus_core::exports::bumpalo; +use dioxus_core::exports::futures_channel; +use dioxus_core::{LazyNodes, ScopeState, TaskId}; use std::future::Future; use std::{cell::Cell, rc::Rc}; -/* - - -let g = use_coroutine(&cx, || { - // clone the items in - async move { - - } -}) - - - -*/ -pub fn use_coroutine(cx: &ScopeState, create_future: impl FnOnce() -> F) -> CoroutineHandle<'_> +/// Maintain a handle over a future that can be paused, resumed, and canceled. +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +pub fn use_coroutine<'a, F>( + cx: &'a ScopeState, + create_future: impl FnOnce() -> F + 'a, +) -> UseCoroutine where F: Future + 'static, { - let state = cx.use_hook(move |_| { - let f = create_future(); - let id = cx.push_future(f); - State { - running: Default::default(), - _id: id - // pending_fut: Default::default(), - // running_fut: Default::default(), - } + let state = cx.use_hook(move |_| CoroutineInner { + _id: None, + running: Default::default(), }); - // state.pending_fut.set(Some(Box::pin(f))); + let mut bump = None; - // if let Some(fut) = state.running_fut.as_mut() { - // cx.push_future(fut); - // } + // as an optimization, we use the bump arena to allocate the callback instead of boxes + // that way we don't always call the constructor, but it's still efficient + cx.render(LazyNodes::new(move |f| { + bump.replace(f.bump()); + f.static_text("") + })); - // if let Some(fut) = state.running_fut.take() { - // state.running.set(true); - // fut.resume(); - // } + let mut slot = Some(create_future); - // let submit: Box = Box::new(move || { - // let g = async move { - // running.set(true); - // create_future().await; - // running.set(false); - // }; - // let p: Pin>> = Box::pin(g); - // fut_slot - // .borrow_mut() - // .replace(unsafe { std::mem::transmute(p) }); - // }); + // safety: bumpalo is limited in constructing unsized box types, so we have to do it through dynamic dispatch + let boxed: BumpBox<'a, dyn FnMut() -> F + 'a> = unsafe { + BumpBox::from_raw(bump.unwrap().alloc(move || { + let inner = slot.take().unwrap(); + inner() + })) + }; - // let submit = unsafe { std::mem::transmute(submit) }; - // state.submit.get_mut().replace(submit); - - // if state.running.get() { - // // let mut fut = state.fut.borrow_mut(); - // // cx.push_task(|| fut.as_mut().unwrap().as_mut()); - // } else { - // // make sure to drop the old future - // if let Some(fut) = state.fut.borrow_mut().take() { - // drop(fut); - // } - // } - CoroutineHandle { cx, inner: state } -} - -struct State { - running: Rc>, - _id: TaskId, - // the way this is structure, you can toggle the coroutine without re-rendering the comppnent - // this means every render *generates* the future, which is a bit of a waste - // todo: allocate pending futures in the bump allocator and then have a true promotion - // pending_fut: Cell + 'static>>>>, - // running_fut: Option + 'static>>>, - // running_fut: Rc + 'static>>>>> -} - -pub struct CoroutineHandle<'a> { - cx: &'a ScopeState, - inner: &'a State, -} - -impl Clone for CoroutineHandle<'_> { - fn clone(&self) -> Self { - CoroutineHandle { - cx: self.cx, - inner: self.inner, - } + UseCoroutine { + inner: state, + create_fut: Cell::new(Some(boxed)), + cx, } } -impl Copy for CoroutineHandle<'_> {} -impl<'a> CoroutineHandle<'a> { - #[allow(clippy::needless_return)] +struct CoroutineInner { + running: Rc>, + _id: Option, +} + +pub struct UseCoroutine<'a, F: Future + 'static> { + create_fut: Cell F + 'a>>>, + inner: &'a CoroutineInner, + cx: &'a ScopeState, +} + +impl<'a, F: Future + 'static> UseCoroutine<'a, F> { + pub fn auto_start(&self, start: bool) -> &Self { + todo!() + } + pub fn start(&self) { - if self.is_running() { - return; + if !self.is_running() { + if let Some(mut fut) = self.create_fut.take() { + let fut = fut(); + self.cx.push_future(fut); + } } - - // if let Some(submit) = self.inner.pending_fut.take() { - // submit(); - // let inner = self.inner; - // self.cx.push_task(submit()); - // } } pub fn is_running(&self) -> bool { - self.inner.running.get() + // self.inner.running.get() + false } pub fn resume(&self) { @@ -120,3 +98,19 @@ impl<'a> CoroutineHandle<'a> { pub fn restart(&self) {} } + +#[test] +fn it_works() { + use dioxus_core::prelude::*; + fn app(cx: Scope) -> Element { + let poll_tasks = use_coroutine(&cx, || async { + loop { + println!("polling tasks"); + } + }); + + poll_tasks.auto_start(true); + + todo!() + } +} From bd1535a0eda26b448b157d0ff5c8d5f2e10967ed Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 02:29:42 -0500 Subject: [PATCH 006/129] feat: use_coroutine happily compiles --- packages/hooks/Cargo.toml | 6 +- packages/hooks/src/usecoroutine.rs | 98 ++++++++++++++++++++---------- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 375fef087..23af30627 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -12,4 +12,8 @@ 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.7" } + + +[dev-dependencies] +futures-util = { version = "0.3", default-features = false } diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 593371b23..5636b4031 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -1,6 +1,7 @@ +#![warn(clippy::pedantic)] + use bumpalo::boxed::Box as BumpBox; use dioxus_core::exports::bumpalo; -use dioxus_core::exports::futures_channel; use dioxus_core::{LazyNodes, ScopeState, TaskId}; use std::future::Future; use std::{cell::Cell, rc::Rc}; @@ -25,34 +26,35 @@ use std::{cell::Cell, rc::Rc}; pub fn use_coroutine<'a, F>( cx: &'a ScopeState, create_future: impl FnOnce() -> F + 'a, -) -> UseCoroutine +) -> UseCoroutine<'a, F> where F: Future + 'static, { let state = cx.use_hook(move |_| CoroutineInner { - _id: None, - running: Default::default(), + task_id: Cell::new(None), + running: std::rc::Rc::default(), + run_count: Cell::new(0), }); - let mut bump = None; - // as an optimization, we use the bump arena to allocate the callback instead of boxes // that way we don't always call the constructor, but it's still efficient - cx.render(LazyNodes::new(move |f| { - bump.replace(f.bump()); - f.static_text("") - })); - - let mut slot = Some(create_future); - // safety: bumpalo is limited in constructing unsized box types, so we have to do it through dynamic dispatch let boxed: BumpBox<'a, dyn FnMut() -> F + 'a> = unsafe { - BumpBox::from_raw(bump.unwrap().alloc(move || { - let inner = slot.take().unwrap(); + let mut bump = None; + cx.render(LazyNodes::new(move |f| { + bump.replace(f.bump()); + f.static_text("") + })); + let mut slot = Some(create_future); + let bump = bump.expect("bump is assigned during render"); + BumpBox::from_raw(bump.alloc(move || { + let inner = slot.take().expect("closure to not be called twice"); inner() })) }; + state.run_count.set(state.run_count.get() + 1); + UseCoroutine { inner: state, create_fut: Cell::new(Some(boxed)), @@ -62,7 +64,8 @@ where struct CoroutineInner { running: Rc>, - _id: Option, + task_id: Cell>, + run_count: Cell, } pub struct UseCoroutine<'a, F: Future + 'static> { @@ -73,35 +76,50 @@ pub struct UseCoroutine<'a, F: Future + 'static> { impl<'a, F: Future + 'static> UseCoroutine<'a, F> { pub fn auto_start(&self, start: bool) -> &Self { - todo!() + if start && self.inner.run_count.get() == 1 { + self.start(); + } + self + } + + pub fn is_running(&self) -> bool { + self.inner.running.get() } pub fn start(&self) { if !self.is_running() { if let Some(mut fut) = self.create_fut.take() { let fut = fut(); - self.cx.push_future(fut); + let ready_handle = self.inner.running.clone(); + + let task = self.cx.push_future(async move { + ready_handle.set(true); + fut.await; + ready_handle.set(false); + }); + + self.inner.task_id.set(Some(task)); } } } - pub fn is_running(&self) -> bool { - // self.inner.running.get() - false - } + // todo: wire these up, either into the task system or into the coroutine system itself + // we would have change how we poll the coroutine and how its awaken - pub fn resume(&self) { - // self.cx.push_task(fut) - } - - pub fn stop(&self) {} - - pub fn restart(&self) {} + // pub fn resume(&self) {} + // pub fn stop(&self) {} + // pub fn restart(&self) {} } -#[test] -fn it_works() { +#[cfg(test)] +mod tests { + #![allow(unused)] + + use super::*; + use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::prelude::*; + use futures_util::StreamExt; + fn app(cx: Scope) -> Element { let poll_tasks = use_coroutine(&cx, || async { loop { @@ -113,4 +131,22 @@ fn it_works() { todo!() } + + fn app_with_channel(cx: Scope) -> Element { + let (tx, mut rx) = unbounded(); + + let tx = cx.use_hook(|_| tx); + + let poll_tasks = use_coroutine(&cx, move || async move { + while let Some(msg) = rx.next().await { + println!("polling tasks: {}", msg); + } + }); + + poll_tasks.auto_start(true); + + tx.unbounded_send("asd").unwrap(); + + todo!() + } } From 6eaad850eee7074326c37e93c2e2ecc5fe5b20a7 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Thu, 10 Feb 2022 12:35:17 +0800 Subject: [PATCH 007/129] feat: add window title api --- packages/desktop/src/desktop_context.rs | 7 +++++++ packages/desktop/src/lib.rs | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 9c0404477..d2c25a3e7 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -57,6 +57,13 @@ impl DesktopContext { pub fn focus(&self) { let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } + + /// set window title + pub fn title(&self, title: &str) { + let _ = self + .proxy + .send_event(UserWindowEvent::Title(String::from(title))); + } } /// use this function can get the `DesktopContext` context. diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 9181d04dc..6bd95b6ff 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -321,6 +321,12 @@ pub fn launch_with_props( window.set_focus(); } } + UserWindowEvent::Title(content) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_title(&content); + } + } } } Event::MainEventsCleared => {} @@ -338,6 +344,8 @@ pub enum UserWindowEvent { DragWindow, CloseWindow, FocusWindow, + + Title(String), Minimize(bool), Maximize(bool), } From f28fb7165a0158b8ce93aa768c350f5ea2b6aff6 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Thu, 10 Feb 2022 13:47:45 +0800 Subject: [PATCH 008/129] feat: add window api --- packages/desktop/src/desktop_context.rs | 14 ++++++++++++-- packages/desktop/src/lib.rs | 21 ++++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index d2c25a3e7..294cfa23c 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -58,11 +58,21 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } + /// set resizable state + pub fn resizable(&self, resizable: bool) { + let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); + } + /// set window title - pub fn title(&self, title: &str) { + pub fn set_title(&self, title: &str) { let _ = self .proxy - .send_event(UserWindowEvent::Title(String::from(title))); + .send_event(UserWindowEvent::SetTitle(String::from(title))); + } + + /// hide the menu + pub fn hide_menu(&self) { + let _ = self.proxy.send_event(UserWindowEvent::HideMenu); } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 6bd95b6ff..452bbba96 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -321,12 +321,25 @@ pub fn launch_with_props( window.set_focus(); } } - UserWindowEvent::Title(content) => { + + UserWindowEvent::SetTitle(content) => { for webview in desktop.webviews.values() { let window = webview.window(); window.set_title(&content); } } + UserWindowEvent::Resizable(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_resizable(state); + } + } + UserWindowEvent::HideMenu => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.hide_menu(); + } + } } } Event::MainEventsCleared => {} @@ -344,10 +357,12 @@ pub enum UserWindowEvent { DragWindow, CloseWindow, FocusWindow, - - Title(String), Minimize(bool), Maximize(bool), + Resizable(bool), + + SetTitle(String), + HideMenu, } pub struct DesktopController { From 01435d6aff049364e76e34acba29a08d36c18c00 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Thu, 10 Feb 2022 16:59:28 +0800 Subject: [PATCH 009/129] feat: add window api --- packages/desktop/src/desktop_context.rs | 6 ++++-- packages/desktop/src/lib.rs | 27 ++++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 294cfa23c..0b9c323c1 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -71,8 +71,10 @@ impl DesktopContext { } /// hide the menu - pub fn hide_menu(&self) { - let _ = self.proxy.send_event(UserWindowEvent::HideMenu); + pub fn set_decorations(&self, decoration: bool) { + let _ = self + .proxy + .send_event(UserWindowEvent::SetDecorations(decoration)); } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 452bbba96..5d45895a9 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -72,7 +72,7 @@ use tao::{ pub use wry; pub use wry::application as tao; use wry::{ - application::event_loop::EventLoopProxy, + application::{event_loop::EventLoopProxy, window::Fullscreen}, webview::RpcRequest, webview::{WebView, WebViewBuilder}, }; @@ -315,12 +315,24 @@ pub fn launch_with_props( window.set_maximized(state); } } + UserWindowEvent::Fullscreen(fullscreen) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_fullscreen(*fullscreen.clone()); + } + } UserWindowEvent::FocusWindow => { for webview in desktop.webviews.values() { let window = webview.window(); window.set_focus(); } } + UserWindowEvent::Resizable(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_resizable(state); + } + } UserWindowEvent::SetTitle(content) => { for webview in desktop.webviews.values() { @@ -328,16 +340,10 @@ pub fn launch_with_props( window.set_title(&content); } } - UserWindowEvent::Resizable(state) => { + UserWindowEvent::SetDecorations(state) => { for webview in desktop.webviews.values() { let window = webview.window(); - window.set_resizable(state); - } - } - UserWindowEvent::HideMenu => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.hide_menu(); + window.set_decorations(state); } } } @@ -360,9 +366,10 @@ pub enum UserWindowEvent { Minimize(bool), Maximize(bool), Resizable(bool), + Fullscreen(Box>), SetTitle(String), - HideMenu, + SetDecorations(bool), } pub struct DesktopController { From 81ea7a14287c79c1658e8b3be4c7ab6ebf12d219 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Thu, 10 Feb 2022 23:33:02 +0800 Subject: [PATCH 010/129] feat: add window api --- packages/desktop/src/desktop_context.rs | 14 ++++++++++++++ packages/desktop/src/lib.rs | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 0b9c323c1..4d09bb966 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -48,6 +48,10 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); } + pub fn visible(&self, visible: bool) { + let _ = self.proxy.send_event(UserWindowEvent::Visible(visible)); + } + /// close window pub fn close(&self) { let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); @@ -63,6 +67,16 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); } + pub fn always_on_top(&self, top: bool) { + let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top)); + } + + pub fn cursor_visible(&self, visible: bool) { + let _ = self + .proxy + .send_event(UserWindowEvent::CursorVisible(visible)); + } + /// set window title pub fn set_title(&self, title: &str) { let _ = self diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 5d45895a9..a0344c75a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -299,6 +299,12 @@ pub fn launch_with_props( // close window *control_flow = ControlFlow::Exit; } + UserWindowEvent::Visible(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_visible(state); + } + } UserWindowEvent::Minimize(state) => { // this loop just run once, because dioxus-desktop is unsupport multi-window. for webview in desktop.webviews.values() { @@ -333,6 +339,19 @@ pub fn launch_with_props( window.set_resizable(state); } } + UserWindowEvent::AlwaysOnTop(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_always_on_top(state); + } + } + + UserWindowEvent::CursorVisible(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_cursor_visible(state); + } + } UserWindowEvent::SetTitle(content) => { for webview in desktop.webviews.values() { @@ -363,11 +382,15 @@ pub enum UserWindowEvent { DragWindow, CloseWindow, FocusWindow, + Visible(bool), Minimize(bool), Maximize(bool), Resizable(bool), + AlwaysOnTop(bool), Fullscreen(Box>), + CursorVisible(bool), + SetTitle(String), SetDecorations(bool), } From 332ec30954d7d7edb89a306ebb8d16f1c063692a Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Thu, 10 Feb 2022 23:38:57 +0800 Subject: [PATCH 011/129] fix: change method name --- packages/desktop/src/desktop_context.rs | 26 +++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 4d09bb966..2f2c41d28 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use dioxus_core::ScopeState; -use wry::application::event_loop::EventLoopProxy; +use wry::application::{event_loop::EventLoopProxy, window::Fullscreen}; use crate::UserWindowEvent; @@ -34,44 +34,50 @@ impl DesktopContext { /// ```rust /// onmousedown: move |_| { desktop.drag_window(); } /// ``` - pub fn drag(&self) { + pub fn drag_window(&self) { let _ = self.proxy.send_event(UserWindowEvent::DragWindow); } /// set window minimize state - pub fn minimize(&self, minimized: bool) { + pub fn set_minimized(&self, minimized: bool) { let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized)); } /// set window maximize state - pub fn maximize(&self, maximized: bool) { + pub fn set_maximized(&self, maximized: bool) { let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); } - pub fn visible(&self, visible: bool) { + pub fn set_visible(&self, visible: bool) { let _ = self.proxy.send_event(UserWindowEvent::Visible(visible)); } /// close window - pub fn close(&self) { + pub fn close_window(&self) { let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); } /// set window to focus - pub fn focus(&self) { + pub fn set_focus(&self) { let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } + pub fn set_fullscreen(&self, fullscreen: Option) { + let _ = self + .proxy + .send_event(UserWindowEvent::Fullscreen(Box::new(fullscreen))); + } + /// set resizable state - pub fn resizable(&self, resizable: bool) { + pub fn set_resizable(&self, resizable: bool) { let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); } - pub fn always_on_top(&self, top: bool) { + pub fn set_always_on_top(&self, top: bool) { let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top)); } - pub fn cursor_visible(&self, visible: bool) { + pub fn set_cursor_visible(&self, visible: bool) { let _ = self .proxy .send_event(UserWindowEvent::CursorVisible(visible)); From 62f8e71f7f36add69d041d22138dbc0746c64b7a Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 09:05:32 +0800 Subject: [PATCH 012/129] fix: borderless example --- examples/borderless.rs | 6 +++--- packages/desktop/src/desktop_context.rs | 16 +++++++++++++++- packages/desktop/src/lib.rs | 18 +++++++++++++++++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/examples/borderless.rs b/examples/borderless.rs index d52e9fb5a..3122c91fa 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -13,7 +13,7 @@ fn app(cx: Scope) -> Element { link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } header { class: "text-gray-400 bg-gray-900 body-font", - onmousedown: move |_| window.drag(), + onmousedown: move |_| window.drag_window(), div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", @@ -23,13 +23,13 @@ fn app(cx: Scope) -> Element { button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| window.minimize(true), + onclick: move |_| window.set_minimized(true), "Minimize" } button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| window.close(), + onclick: move |_| window.close_window(), "Close" } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 2f2c41d28..c3f193629 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -48,6 +48,7 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); } + /// set window visible or not pub fn set_visible(&self, visible: bool) { let _ = self.proxy.send_event(UserWindowEvent::Visible(visible)); } @@ -62,6 +63,7 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } + /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: Option) { let _ = self .proxy @@ -73,16 +75,23 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); } + /// set the window always on top pub fn set_always_on_top(&self, top: bool) { let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top)); } + // set cursor visible or not pub fn set_cursor_visible(&self, visible: bool) { let _ = self .proxy .send_event(UserWindowEvent::CursorVisible(visible)); } + // set cursor grab + pub fn set_cursor_grab(&self, grab: bool) { + let _ = self.proxy.send_event(UserWindowEvent::CursorGrab(grab)); + } + /// set window title pub fn set_title(&self, title: &str) { let _ = self @@ -90,12 +99,17 @@ impl DesktopContext { .send_event(UserWindowEvent::SetTitle(String::from(title))); } - /// hide the menu + /// change window to borderless pub fn set_decorations(&self, decoration: bool) { let _ = self .proxy .send_event(UserWindowEvent::SetDecorations(decoration)); } + + /// skip/hidden the taskbar icon + pub fn set_skip_taskbar(&self, skip: bool) { + let _ = self.proxy.send_event(UserWindowEvent::SkipTaskBar(skip)); + } } /// use this function can get the `DesktopContext` context. diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index a0344c75a..1835e6876 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -72,7 +72,9 @@ use tao::{ pub use wry; pub use wry::application as tao; use wry::{ - application::{event_loop::EventLoopProxy, window::Fullscreen}, + application::{ + event_loop::EventLoopProxy, platform::windows::WindowExtWindows, window::Fullscreen, + }, webview::RpcRequest, webview::{WebView, WebViewBuilder}, }; @@ -352,6 +354,12 @@ pub fn launch_with_props( window.set_cursor_visible(state); } } + UserWindowEvent::CursorGrab(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + let _ = window.set_cursor_grab(state); + } + } UserWindowEvent::SetTitle(content) => { for webview in desktop.webviews.values() { @@ -365,6 +373,12 @@ pub fn launch_with_props( window.set_decorations(state); } } + UserWindowEvent::SkipTaskBar(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_skip_taskbar(state); + } + } } } Event::MainEventsCleared => {} @@ -390,9 +404,11 @@ pub enum UserWindowEvent { Fullscreen(Box>), CursorVisible(bool), + CursorGrab(bool), SetTitle(String), SetDecorations(bool), + SkipTaskBar(bool), } pub struct DesktopController { From c9fa19d009407874e9c581096c58c51ee75f83c7 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 09:10:27 +0800 Subject: [PATCH 013/129] fix: ci problem --- packages/desktop/src/desktop_context.rs | 5 ----- packages/desktop/src/lib.rs | 11 +---------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index c3f193629..5ae38adff 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -105,11 +105,6 @@ impl DesktopContext { .proxy .send_event(UserWindowEvent::SetDecorations(decoration)); } - - /// skip/hidden the taskbar icon - pub fn set_skip_taskbar(&self, skip: bool) { - let _ = self.proxy.send_event(UserWindowEvent::SkipTaskBar(skip)); - } } /// use this function can get the `DesktopContext` context. diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 1835e6876..101ab91b7 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -72,9 +72,7 @@ use tao::{ pub use wry; pub use wry::application as tao; use wry::{ - application::{ - event_loop::EventLoopProxy, platform::windows::WindowExtWindows, window::Fullscreen, - }, + application::{event_loop::EventLoopProxy, window::Fullscreen}, webview::RpcRequest, webview::{WebView, WebViewBuilder}, }; @@ -373,12 +371,6 @@ pub fn launch_with_props( window.set_decorations(state); } } - UserWindowEvent::SkipTaskBar(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_skip_taskbar(state); - } - } } } Event::MainEventsCleared => {} @@ -408,7 +400,6 @@ pub enum UserWindowEvent { SetTitle(String), SetDecorations(bool), - SkipTaskBar(bool), } pub struct DesktopController { From 30bb92f09afe1de588d644b5e2060524407f718f Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 09:46:23 +0800 Subject: [PATCH 014/129] fix: change method name --- packages/desktop/src/desktop_context.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 5ae38adff..d33ff5abe 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -34,7 +34,7 @@ impl DesktopContext { /// ```rust /// onmousedown: move |_| { desktop.drag_window(); } /// ``` - pub fn drag_window(&self) { + pub fn drag(&self) { let _ = self.proxy.send_event(UserWindowEvent::DragWindow); } @@ -54,12 +54,12 @@ impl DesktopContext { } /// close window - pub fn close_window(&self) { + pub fn close(&self) { let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); } /// set window to focus - pub fn set_focus(&self) { + pub fn focus(&self) { let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } From 557201816b1bd18e92173902d110534f304628c5 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 09:51:12 +0800 Subject: [PATCH 015/129] fix: borderless example --- examples/borderless.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/borderless.rs b/examples/borderless.rs index 3122c91fa..1b7e2e0f5 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -13,7 +13,7 @@ fn app(cx: Scope) -> Element { link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } header { class: "text-gray-400 bg-gray-900 body-font", - onmousedown: move |_| window.drag_window(), + onmousedown: move |_| window.drag(), div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", @@ -29,7 +29,7 @@ fn app(cx: Scope) -> Element { button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| window.close_window(), + onclick: move |_| window.close(), "Close" } } From da0f596cdee27ba3d32aa314b8bc4a3388fd2210 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 10:36:29 +0800 Subject: [PATCH 016/129] feat: commit code --- examples/borderless.rs | 2 ++ packages/desktop/src/desktop_context.rs | 6 +++--- packages/desktop/src/lib.rs | 24 +++++++++++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/examples/borderless.rs b/examples/borderless.rs index 1b7e2e0f5..55f44d592 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -9,6 +9,8 @@ fn main() { fn app(cx: Scope) -> Element { let window = dioxus::desktop::use_window(&cx); + // window.set_fullscreen(true); + cx.render(rsx!( link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } header { diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index d33ff5abe..d180e1235 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use dioxus_core::ScopeState; -use wry::application::{event_loop::EventLoopProxy, window::Fullscreen}; +use wry::application::event_loop::EventLoopProxy; use crate::UserWindowEvent; @@ -64,10 +64,10 @@ impl DesktopContext { } /// change window to fullscreen - pub fn set_fullscreen(&self, fullscreen: Option) { + pub fn set_fullscreen(&self, fullscreen: bool) { let _ = self .proxy - .send_event(UserWindowEvent::Fullscreen(Box::new(fullscreen))); + .send_event(UserWindowEvent::Fullscreen(fullscreen)); } /// set resizable state diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 101ab91b7..143225f6a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -292,6 +292,11 @@ pub fn launch_with_props( let window = webview.window(); // start to drag the window. // if the drag_window have any err. we don't do anything. + + if window.fullscreen().is_some() { + return; + } + let _ = window.drag_window(); } } @@ -321,10 +326,23 @@ pub fn launch_with_props( window.set_maximized(state); } } - UserWindowEvent::Fullscreen(fullscreen) => { + UserWindowEvent::Fullscreen(state) => { for webview in desktop.webviews.values() { let window = webview.window(); - window.set_fullscreen(*fullscreen.clone()); + + let current_monitor = window.current_monitor(); + + if current_monitor.is_none() { + return; + } + + let fullscreen = if state { + Some(Fullscreen::Borderless(current_monitor)) + } else { + None + }; + + window.set_fullscreen(fullscreen); } } UserWindowEvent::FocusWindow => { @@ -393,7 +411,7 @@ pub enum UserWindowEvent { Maximize(bool), Resizable(bool), AlwaysOnTop(bool), - Fullscreen(Box>), + Fullscreen(bool), CursorVisible(bool), CursorGrab(bool), From 69abb898239428435ad770448d4c3e768ceb3328 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 10:57:53 +0800 Subject: [PATCH 017/129] feat: commit code --- examples/borderless.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/borderless.rs b/examples/borderless.rs index 55f44d592..f81028dbd 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -9,7 +9,9 @@ fn main() { fn app(cx: Scope) -> Element { let window = dioxus::desktop::use_window(&cx); + // if you want to make window fullscreen, you need close the resizable. // window.set_fullscreen(true); + // window.set_resizable(false); cx.render(rsx!( link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } From 04d3098c9b4e8478f0fafad36b667780de5ac829 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 11:21:25 +0800 Subject: [PATCH 018/129] feat: add more `window api` example --- examples/borderless.rs | 42 ----------------- examples/window_event.rs | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 42 deletions(-) delete mode 100644 examples/borderless.rs create mode 100644 examples/window_event.rs diff --git a/examples/borderless.rs b/examples/borderless.rs deleted file mode 100644 index f81028dbd..000000000 --- a/examples/borderless.rs +++ /dev/null @@ -1,42 +0,0 @@ -use dioxus::prelude::*; - -fn main() { - dioxus::desktop::launch_cfg(app, |cfg| { - cfg.with_window(|w| w.with_title("BorderLess Demo").with_decorations(false)) - }); -} - -fn app(cx: Scope) -> Element { - let window = dioxus::desktop::use_window(&cx); - - // if you want to make window fullscreen, you need close the resizable. - // window.set_fullscreen(true); - // window.set_resizable(false); - - cx.render(rsx!( - link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } - header { - class: "text-gray-400 bg-gray-900 body-font", - onmousedown: move |_| window.drag(), - div { - class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", - a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", - span { class: "ml-3 text-xl", "Dioxus"} - } - nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" } - button { - class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", - onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| window.set_minimized(true), - "Minimize" - } - button { - class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", - onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| window.close(), - "Close" - } - } - } - )) -} diff --git a/examples/window_event.rs b/examples/window_event.rs new file mode 100644 index 000000000..525f0d841 --- /dev/null +++ b/examples/window_event.rs @@ -0,0 +1,97 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch_cfg(app, |cfg| { + cfg.with_window(|w| w.with_title("BorderLess Demo").with_decorations(false)) + }); +} + +fn app(cx: Scope) -> Element { + let window = dioxus::desktop::use_window(&cx); + + // if you want to make window fullscreen, you need close the resizable. + // window.set_fullscreen(true); + // window.set_resizable(false); + + let (fullscreen, set_fullscreen) = use_state(&cx, || false); + let (always_on_top, set_always_on_top) = use_state(&cx, || false); + let (decorations, set_decorations) = use_state(&cx, || false); + + cx.render(rsx!( + link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } + header { + class: "text-gray-400 bg-gray-900 body-font", + onmousedown: move |_| window.drag(), + div { + class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", + a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", + span { class: "ml-3 text-xl", "Dioxus"} + } + nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| window.set_minimized(true), + "Minimize" + } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + + window.set_fullscreen(!fullscreen); + window.set_resizable(*fullscreen); + + set_fullscreen(!fullscreen); + }, + "Fullscreen" + } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| window.close(), + "Close" + } + } + } + br {} + div { + class: "container mx-auto", + div { + class: "grid grid-cols-5", + div { + button { + class: "inline-flex items-center text-white bg-green-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + window.set_always_on_top(!always_on_top); + set_always_on_top(!always_on_top); + }, + "Always On Top" + } + } + div { + button { + class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + window.set_decorations(!decorations); + set_decorations(!decorations); + }, + "Set Decorations" + } + } + div { + button { + class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + window.set_title("Oh Dioxus !!!"); + }, + "Change Title" + } + } + } + } + )) +} From 8801e4be0028ec445919ec9a9f3208bf5c5071ba Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 11:29:00 +0800 Subject: [PATCH 019/129] feat: change example --- examples/window_event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/window_event.rs b/examples/window_event.rs index 525f0d841..cb578829f 100644 --- a/examples/window_event.rs +++ b/examples/window_event.rs @@ -86,7 +86,7 @@ fn app(cx: Scope) -> Element { class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", onmousedown: |evt| evt.cancel_bubble(), onclick: move |_| { - window.set_title("Oh Dioxus !!!"); + window.set_title("Dioxus Application"); }, "Change Title" } From eb39f360e3f334eb9e4fb7f5eb4628c72d6222bf Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 11 Feb 2022 17:59:59 +0100 Subject: [PATCH 020/129] fix some uncaught error during runtime test --- packages/web/src/dom.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index c722ab737..48fb25e6e 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -87,7 +87,15 @@ impl WebsysDom { } }); - let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); + let document = load_document(); + let root = match document.get_element_by_id(&cfg.rootname) { + Some(root) => root, + // a match here in order to avoid some error during runtime browser test + None => { + let body = document.create_element("body").ok().unwrap(); + body + } + }; Self { interpreter: Interpreter::new(root.clone()), From f8c78f8ca9e6375712e088c50fc122f6297a922b Mon Sep 17 00:00:00 2001 From: Benjamin Lemelin Date: Fri, 11 Feb 2022 14:47:29 -0500 Subject: [PATCH 021/129] Transparent window means transparent WebView --- packages/desktop/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 143225f6a..a58996f89 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -177,6 +177,7 @@ pub fn launch_with_props( 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| { From d8bfc41662790ce076d8b291a486e61b435315d6 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 11 Feb 2022 21:58:44 +0100 Subject: [PATCH 022/129] fix code coverage action --- .github/workflows/main.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d8e62ade3..b1a6acede 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -95,12 +95,10 @@ jobs: image: xd009642/tarpaulin:develop-nightly options: --security-opt seccomp=unconfined steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Generate code coverage - run: | - apt-get update &&\ - apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ - cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml - - name: Upload to codecov.io - uses: codecov/codecov-action@v2 + - uses: actions/checkout@v2 + - run: apt-get update + - run: apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y + - run: cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + - uses: codecov/codecov-action@v2 + with: + fail_ci_if_error: false From c0753913ea6715e7d64be6d43b186761572982a4 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 11 Feb 2022 22:01:22 +0100 Subject: [PATCH 023/129] add names for clarity --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b1a6acede..3e84047d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -95,10 +95,13 @@ jobs: image: xd009642/tarpaulin:develop-nightly options: --security-opt seccomp=unconfined steps: + - name: Checkout repository - uses: actions/checkout@v2 + - name: Generate code coverage - run: apt-get update - run: apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y - run: cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + - name: Upload to codecov.io - uses: codecov/codecov-action@v2 with: fail_ci_if_error: false From e54b5aab04e1091de099d20f396adffa1f5071c7 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 11 Feb 2022 22:49:48 +0100 Subject: [PATCH 024/129] try fix docs workflow --- .github/workflows/docs.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d9bae48d6..f0d2a4b80 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,11 +2,12 @@ name: github pages on: push: - branches: - - master paths: - docs/** - .github/workflows/docs.yml + pull_request: + branches: + - master jobs: build-deploy: From 6e91b1d54e8da78b59dba8811d49f960141286d8 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sat, 12 Feb 2022 00:11:33 +0100 Subject: [PATCH 025/129] try fix syntax workflow --- .github/workflows/main.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e84047d0..029e50e88 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -96,12 +96,13 @@ jobs: options: --security-opt seccomp=unconfined steps: - name: Checkout repository - - uses: actions/checkout@v2 + uses: actions/checkout@v2 - name: Generate code coverage - - run: apt-get update - - run: apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y - - run: cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + run: | + apt-get update &&\ + apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ + cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml - name: Upload to codecov.io - - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v2 with: fail_ci_if_error: false From b64574810d56a5968919e44686e997219d8cd279 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sat, 12 Feb 2022 01:43:36 +0100 Subject: [PATCH 026/129] Clean up desktop's index.html Initially I wanted to delete only the duplicated `` opening tag, but then decided to make it a fully valid HTML document. Passes https://validator.w3.org/nu/#textarea check. --- packages/desktop/src/index.html | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 88fcba12a..e244b6faa 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -1,22 +1,15 @@ - - - - - - - - - -
-
- - - - + + Dioxus app + + + +
+ + From 2d1371167f05f3e5ffc53470093ff03c984a44d9 Mon Sep 17 00:00:00 2001 From: Aster Date: Sat, 12 Feb 2022 21:22:05 +0800 Subject: [PATCH 027/129] Use `===` when rhs is string --- packages/interpreter/src/interpreter.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 95c88d294..f0acd0802 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -102,15 +102,15 @@ export class Interpreter { SetAttribute(root, field, value, ns) { const name = field; const node = this.nodes[root]; - if (ns == "style") { + if (ns === "style") { // @ts-ignore node.style[name] = value; - } else if (ns != null || ns != undefined) { + } else if (ns != null || ns !== undefined) { node.setAttributeNS(ns, name, value); } else { switch (name) { case "value": - if (value != node.value) { + if (value !== node.value) { node.value = value; } break; @@ -125,7 +125,7 @@ export class Interpreter { break; default: // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { + if (value === "false" && bool_attrs.hasOwnProperty(name)) { node.removeAttribute(name); } else { node.setAttribute(name, value); @@ -142,7 +142,7 @@ export class Interpreter { node.checked = false; } else if (name === "selected") { node.selected = false; - } else if (name == "dangerous_inner_html") { + } else if (name === "dangerous_inner_html") { node.innerHTML = ""; } else { node.removeAttribute(name); @@ -200,10 +200,10 @@ export class Interpreter { `dioxus-prevent-default` ); - if (event.type == "click") { + if (event.type === "click") { // todo call prevent default if it's the right type of event if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { + if (target.tagName === "A") { event.preventDefault(); const href = target.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { @@ -213,7 +213,7 @@ export class Interpreter { } // also prevent buttons from submitting - if (target.tagName == "BUTTON") { + if (target.tagName === "BUTTON") { event.preventDefault(); } } @@ -237,16 +237,16 @@ export class Interpreter { if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); } - if (event.type == "submit") { + if (event.type === "submit") { event.preventDefault(); } - if (target.tagName == "FORM") { + if (target.tagName === "FORM") { for (let x = 0; x < target.elements.length; x++) { let element = target.elements[x]; let name = element.getAttribute("name"); if (name != null) { - if (element.getAttribute("type") == "checkbox") { + if (element.getAttribute("type") === "checkbox") { // @ts-ignore contents.values[name] = element.checked ? "true" : "false"; } else { @@ -349,7 +349,7 @@ export function serialize_event(event) { case "submit": { let target = event.target; let value = target.value ?? target.textContent; - if (target.type == "checkbox") { + if (target.type === "checkbox") { value = target.checked ? "true" : "false"; } return { From aad055cd84e7d592aba9606b0ff6447b620bcaf1 Mon Sep 17 00:00:00 2001 From: Aster Date: Sat, 12 Feb 2022 21:45:42 +0800 Subject: [PATCH 028/129] Fix typo `WryProtocol` --- packages/desktop/src/cfg.rs | 14 ++++++++++---- packages/desktop/src/lib.rs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 5baa46f5d..25e280a0f 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,3 +1,4 @@ +use wry::application::window::Icon; use wry::{ application::{ event_loop::EventLoop, @@ -13,12 +14,12 @@ pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); pub struct DesktopConfig { pub window: WindowBuilder, pub file_drop_handler: Option bool>>, - pub protocos: Vec, + pub protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) event_handler: Option>, } -pub type WryProtocl = ( +pub type WryProtocol = ( String, Box WryResult + '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, } @@ -75,7 +76,12 @@ impl DesktopConfig { where F: Fn(&HttpRequest) -> WryResult + '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 } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index a58996f89..596d9c260 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -261,7 +261,7 @@ pub fn launch_with_props( .unwrap_or_default() }); - for (name, handler) in cfg.protocos.drain(..) { + for (name, handler) in cfg.protocols.drain(..) { webview = webview.with_custom_protocol(name, handler) } From 0596088312b32a3e9279cb09df119c82ead0b376 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 12 Feb 2022 10:57:57 -0500 Subject: [PATCH 029/129] docs: fix lifetimes on children --- docs/guide/src/components/component_children.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/src/components/component_children.md b/docs/guide/src/components/component_children.md index ea9b1fc45..809ae3d9b 100644 --- a/docs/guide/src/components/component_children.md +++ b/docs/guide/src/components/component_children.md @@ -30,7 +30,7 @@ struct ClickableProps<'a> { title: &'a str } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}" @@ -64,7 +64,7 @@ struct ClickableProps<'a> { body: Element<'a> } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -98,7 +98,7 @@ struct ClickableProps<'a> { children: Element<'a> } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -125,7 +125,7 @@ While technically allowed, it's an antipattern to pass children more than once i However, because the `Element` is transparently a `VNode`, we can actually match on it to extract the nodes themselves, in case we are expecting a specific format: ```rust -fn clickable(cx: Scope) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { match cx.props.children { Some(VNode::Text(text)) => { // ... @@ -184,7 +184,7 @@ struct ClickableProps<'a> { onclick: EventHandler<'a, MouseEvent> } -fn clickable(cx: Scope) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { cx.render(rsx!( a { onclick: move |evt| cx.props.onclick.call(evt) From 4981e4c26b17ec20950cb895d31a44c76aceeb73 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 12 Feb 2022 11:02:05 -0500 Subject: [PATCH 030/129] ci: make codecov be quieter --- codecov.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..69cb76019 --- /dev/null +++ b/codecov.yml @@ -0,0 +1 @@ +comment: false From a325c03dd93b0cfb042487d228ad493adc7aebd0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 12 Feb 2022 11:02:55 -0500 Subject: [PATCH 031/129] fix: lifetimes again --- docs/guide/src/components/component_children.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/src/components/component_children.md b/docs/guide/src/components/component_children.md index 809ae3d9b..1259422af 100644 --- a/docs/guide/src/components/component_children.md +++ b/docs/guide/src/components/component_children.md @@ -30,7 +30,7 @@ struct ClickableProps<'a> { title: &'a str } -fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}" @@ -98,7 +98,7 @@ struct ClickableProps<'a> { children: Element<'a> } -fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -125,7 +125,7 @@ While technically allowed, it's an antipattern to pass children more than once i However, because the `Element` is transparently a `VNode`, we can actually match on it to extract the nodes themselves, in case we are expecting a specific format: ```rust -fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { match cx.props.children { Some(VNode::Text(text)) => { // ... @@ -160,7 +160,7 @@ struct ClickableProps<'a> { attributes: Attributes<'a> } -fn clickable(cx: Scope) -> Element { +fn clickable(cx: Scope>) -> Element { cx.render(rsx!( a { ..cx.props.attributes, @@ -184,7 +184,7 @@ struct ClickableProps<'a> { onclick: EventHandler<'a, MouseEvent> } -fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { onclick: move |evt| cx.props.onclick.call(evt) From 78ac592c0aaec89041c6e0210edf3573b5334ce4 Mon Sep 17 00:00:00 2001 From: Aster Date: Sun, 13 Feb 2022 00:34:45 +0800 Subject: [PATCH 032/129] Custom for icon --- packages/desktop/Cargo.toml | 1 + packages/desktop/src/cfg.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index fa1bc88dc..fcb3a19e9 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -32,6 +32,7 @@ dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } +image = "0.24.0" [features] diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 25e280a0f..a177c8a4b 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,3 +1,6 @@ +use image::io::Reader as ImageReader; +use image::ImageFormat; +use std::io::Cursor; use wry::application::window::Icon; use wry::{ application::{ @@ -86,6 +89,19 @@ impl DesktopConfig { } } +impl DesktopConfig { + pub(crate) fn with_default_icon(mut self) -> Self { + 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().expect("image parse failed"); + let rgba = Icon::from_rgba(icon.as_bytes().to_owned(), icon.width(), icon.height()) + .expect("image parse failed"); + self.window.window.window_icon = Some(rgba); + self + } +} + impl Default for DesktopConfig { fn default() -> Self { Self::new() From f6b30d26b969e77df57fd20084d11057d7307e6b Mon Sep 17 00:00:00 2001 From: Aster Date: Sun, 13 Feb 2022 00:35:02 +0800 Subject: [PATCH 033/129] Add default desktop icon --- packages/desktop/src/default_icon.png | Bin 0 -> 77196 bytes packages/desktop/src/lib.rs | 56 ++------------------------ packages/desktop/src/readme.md | 51 +++++++++++++++++++++++ 3 files changed, 55 insertions(+), 52 deletions(-) create mode 100644 packages/desktop/src/default_icon.png create mode 100644 packages/desktop/src/readme.md diff --git a/packages/desktop/src/default_icon.png b/packages/desktop/src/default_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d0742ea8a067fdcdbd24b45866ec7794dfa4a399 GIT binary patch literal 77196 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

M;Ig0xsnp{B?j30@}Tazxg}1i|Tbn@eJK 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 E0YnYs?f?J) literal 0 HcmV?d00001 diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 596d9c260..f4e28ca2a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -1,54 +1,6 @@ -//! 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; pub mod desktop_context; @@ -152,7 +104,7 @@ pub fn launch_with_props( 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(); diff --git a/packages/desktop/src/readme.md b/packages/desktop/src/readme.md new file mode 100644 index 000000000..dc034fe67 --- /dev/null +++ b/packages/desktop/src/readme.md @@ -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! \ No newline at end of file From 21b436b7bfac41894d01066abcdb8734cc55b5af Mon Sep 17 00:00:00 2001 From: Aster Date: Sun, 13 Feb 2022 01:34:41 +0800 Subject: [PATCH 034/129] Remove image at runtime --- packages/desktop/Cargo.toml | 2 +- packages/desktop/src/cfg.rs | 36 +++++++++++++++++++------- packages/desktop/src/default_icon.bin | Bin 0 -> 846400 bytes 3 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 packages/desktop/src/default_icon.bin diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index fcb3a19e9..805c46f3b 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -32,7 +32,6 @@ 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" } -image = "0.24.0" [features] @@ -42,3 +41,4 @@ tokio_runtime = ["tokio"] [dev-dependencies] dioxus-hooks = { path = "../hooks" } +image = "0.24.0" \ No newline at end of file diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index a177c8a4b..84786a133 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,6 +1,3 @@ -use image::io::Reader as ImageReader; -use image::ImageFormat; -use std::io::Cursor; use wry::application::window::Icon; use wry::{ application::{ @@ -91,12 +88,8 @@ impl DesktopConfig { impl DesktopConfig { pub(crate) fn with_default_icon(mut self) -> Self { - 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().expect("image parse failed"); - let rgba = Icon::from_rgba(icon.as_bytes().to_owned(), icon.width(), icon.height()) - .expect("image parse failed"); + let bin: &[u8] = include_bytes!("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 } @@ -107,3 +100,28 @@ impl Default for DesktopConfig { Self::new() } } + +// dirty trick, avoid introducing `image` at runtime +// TODO: use serde when `Icon` impl serde +#[test] +#[ignore] +fn prepare_default_icon() { + use image::io::Reader as ImageReader; + use image::ImageFormat; + use std::fs::File; + use std::io::Cursor; + use std::io::Write; + use std::path::PathBuf; + let png: &[u8] = include_bytes!("default_icon.png"); + let mut reader = ImageReader::new(Cursor::new(png)); + reader.set_format(ImageFormat::Png); + let icon = reader.decode().unwrap(); + let bin = PathBuf::from(file!()) + .parent() + .unwrap() + .join("default_icon.bin"); + println!("{:?}", bin); + let mut file = File::create(bin).unwrap(); + file.write_all(icon.as_bytes()).unwrap(); + println!("({}, {})", icon.width(), icon.height()) +} diff --git a/packages/desktop/src/default_icon.bin b/packages/desktop/src/default_icon.bin new file mode 100644 index 0000000000000000000000000000000000000000..2d4898f0cabc546d8fd1dc1127f8fe8e648f5aaa GIT binary patch literal 846400 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 V2#A0Ph=2%)fCz|y2-GKme*uRv2jc($ literal 0 HcmV?d00001 From ab99bc6c8d44d0788a124f41d67e45ff254f16f6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:35:07 -0500 Subject: [PATCH 035/129] chore: comment out image generator when not in use --- packages/desktop/Cargo.toml | 2 +- packages/desktop/src/cfg.rs | 47 ++++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 805c46f3b..0d7c13e3f 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -41,4 +41,4 @@ tokio_runtime = ["tokio"] [dev-dependencies] dioxus-hooks = { path = "../hooks" } -image = "0.24.0" \ No newline at end of file +# image = "0.24.0" # enable this when generating a new desktop image diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 84786a133..49a26ae78 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -103,25 +103,28 @@ impl Default for DesktopConfig { // dirty trick, avoid introducing `image` at runtime // TODO: use serde when `Icon` impl serde -#[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()) -} +// +// 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()) +// } From e06f6dc0305d90b4a3dc53ec876e879612c5dd79 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:37:39 -0500 Subject: [PATCH 036/129] fix: docs deploys should only be done on master commits --- .github/workflows/docs.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f0d2a4b80..0e19ecaec 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,7 +5,6 @@ on: paths: - docs/** - .github/workflows/docs.yml - pull_request: branches: - master From 542b48a59cb57aec0813f41cb54928199a7c62d9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:40:25 -0500 Subject: [PATCH 037/129] ci: disable on draft prs --- .github/workflows/macos.yml | 3 +++ .github/workflows/main.yml | 4 ++++ .github/workflows/windows.yml | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 7003b3925..16e306f6c 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -10,10 +10,13 @@ on: - lib.rs - Cargo.toml pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - master jobs: + JOB: + if: github.event.pull_request.draft == false test: name: Test Suite runs-on: macos-latest diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 029e50e88..d4ebdd48e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,10 +10,14 @@ on: - lib.rs - Cargo.toml pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - master jobs: + JOB: + if: github.event.pull_request.draft == false + check: name: Check runs-on: ubuntu-latest diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index cee707dde..41e98f9ee 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -10,10 +10,14 @@ on: - lib.rs - Cargo.toml pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - master jobs: + JOB: + if: github.event.pull_request.draft == false + test: runs-on: windows-latest name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }}) From 8d2e23bde626eaed6e5b13384cc7ac38bab5ea66 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:41:38 -0500 Subject: [PATCH 038/129] ci: fix draft request --- .github/workflows/macos.yml | 3 +-- .github/workflows/main.yml | 4 +--- .github/workflows/windows.yml | 4 +--- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 16e306f6c..d1351712a 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -15,9 +15,8 @@ on: - master jobs: - JOB: - if: github.event.pull_request.draft == false test: + if: github.event.pull_request.draft == false name: Test Suite runs-on: macos-latest steps: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d4ebdd48e..3e1ec15c0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,10 +15,8 @@ on: - master jobs: - JOB: - if: github.event.pull_request.draft == false - check: + if: github.event.pull_request.draft == false name: Check runs-on: ubuntu-latest steps: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 41e98f9ee..518da37b3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -15,10 +15,8 @@ on: - master jobs: - JOB: - if: github.event.pull_request.draft == false - test: + if: github.event.pull_request.draft == false runs-on: windows-latest name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }}) env: From 3c54ce06f2c2359aadf6f1c4ad0a4eeca4bcb403 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:43:15 -0500 Subject: [PATCH 039/129] fix: disable more --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e1ec15c0..07ccd8069 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,6 +34,7 @@ jobs: command: check test: + if: github.event.pull_request.draft == false name: Test Suite runs-on: ubuntu-latest steps: @@ -55,6 +56,7 @@ jobs: args: tests fmt: + if: github.event.pull_request.draft == false name: Rustfmt runs-on: ubuntu-latest steps: @@ -72,6 +74,7 @@ jobs: args: --all -- --check clippy: + if: github.event.pull_request.draft == false name: Clippy runs-on: ubuntu-latest steps: @@ -91,6 +94,7 @@ jobs: args: -- -D warnings coverage: + if: github.event.pull_request.draft == false name: Coverage runs-on: ubuntu-latest container: From e03f8ca90c324be7a9051ca3c138a5171a5ebbd5 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:44:17 -0500 Subject: [PATCH 040/129] ci: no more codecov failing --- codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/codecov.yml b/codecov.yml index 69cb76019..325c05ec8 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1 +1,2 @@ comment: false +fail_ci_if_error: false From c5025357247465fc9ca2e85f97e81b2c35da8805 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 18:59:15 +0100 Subject: [PATCH 041/129] Update dependencies Specifically set wry to 0.13; this has breaking changes (notably: RPC -> IPC). --- Cargo.toml | 12 ++++++------ packages/desktop/Cargo.toml | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e3a0a032..e1561cb59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,15 +57,15 @@ members = [ ] [dev-dependencies] -futures-util = "0.3.17" +futures-util = "0.3.21" log = "0.4.14" num-format = "0.4.0" separator = "0.4.1" -serde = { version = "1.0.131", features = ["derive"] } +serde = { version = "1.0.136", features = ["derive"] } im-rc = "15.0.0" -anyhow = "1.0.51" -serde_json = "1.0.73" +anyhow = "1.0.53" +serde_json = "1.0.79" rand = { version = "0.8.4", features = ["small_rng"] } -tokio = { version = "1.14.0", features = ["full"] } -reqwest = { version = "0.11.8", features = ["json"] } +tokio = { version = "1.16.1", features = ["full"] } +reqwest = { version = "0.11.9", features = ["json"] } dioxus = { path = ".", features = ["desktop", "ssr", "router"] } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 0d7c13e3f..d1f8b8697 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -13,15 +13,15 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } -argh = "0.1.4" -serde = "1.0.120" -serde_json = "1.0.61" -thiserror = "1.0.23" -log = "0.4.13" +argh = "0.1.7" +serde = "1.0.136" +serde_json = "1.0.79" +thiserror = "1.0.30" +log = "0.4.14" html-escape = "0.2.9" -wry = "0.12.2" -futures-channel = "0.3" -tokio = { version = "1.12.0", features = [ +wry = { version = "0.13.1" } +futures-channel = "0.3.21" +tokio = { version = "1.16.1", features = [ "sync", "rt-multi-thread", "rt", From ee2b869e9957312f1141065102f9fa00e3405ee7 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:39:47 +0100 Subject: [PATCH 042/129] Add optional feature flags of wry Check wry's documentation for each. Some of them are platform dependent or have platform dependent effects. (mostly MacOS and Linux) --- Cargo.toml | 7 +++++++ packages/desktop/Cargo.toml | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e1561cb59..3f0d50187 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,13 @@ web = ["dioxus-web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] +devtool = ["dioxus-desktop/devtool"] +fullscreen = ["dioxus-desktop/fullscreen"] +transparent = ["dioxus-desktop/transparent"] + +tray = ["dioxus-desktop/tray"] +ayatana = ["dioxus-desktop/ayatana"] + # "dioxus-router/web" # "dioxus-router/desktop" # desktop = ["dioxus-desktop", "dioxus-router/desktop"] diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index d1f8b8697..2622355eb 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -38,6 +38,13 @@ dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } default = ["tokio_runtime"] tokio_runtime = ["tokio"] +devtool = ["wry/devtool"] +fullscreen = ["wry/fullscreen"] +transparent = ["wry/transparent"] + +tray = ["wry/tray"] +ayatana = ["wry/ayatana"] + [dev-dependencies] dioxus-hooks = { path = "../hooks" } From c40d225d7db2490f47967d1fea5c65e1a524954d Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:50:29 +0100 Subject: [PATCH 043/129] Fix typo --- packages/desktop/src/cfg.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 49a26ae78..210ec9637 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -12,9 +12,9 @@ use wry::{ pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); pub struct DesktopConfig { - pub window: WindowBuilder, - pub file_drop_handler: Option bool>>, - pub protocols: Vec, + pub(crate) window: WindowBuilder, + pub(crate) file_drop_handler: Option bool>>, + pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) event_handler: Option>, } From a5bf25ce18d97ad58fa10fd0e9be5cb69f632cbd Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:51:37 +0100 Subject: [PATCH 044/129] Adjust visibility --- packages/desktop/src/cfg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 210ec9637..90fd0f1ae 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -19,7 +19,7 @@ pub struct DesktopConfig { pub(crate) event_handler: Option>, } -pub type WryProtocol = ( +pub(crate) type WryProtocol = ( String, Box WryResult + 'static>, ); From afa5a301c7ddae707130f0f8fb27a9446f4a879d Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:52:05 +0100 Subject: [PATCH 045/129] Fix typo --- packages/interpreter/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index ebca3cbf4..076a89197 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -1,4 +1,4 @@ -pub static INTERPRTER_JS: &str = include_str!("./interpreter.js"); +pub static INTERPRETER_JS: &str = include_str!("./interpreter.js"); #[cfg(feature = "web")] mod bindings; From 594a794f0538749f56f8fb56d011e46aead7ca1e Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:56:11 +0100 Subject: [PATCH 046/129] Switch from RPC to IPC --- packages/desktop/src/events.rs | 40 +++++++++++++++++------ packages/desktop/src/lib.rs | 42 ++++++++++++------------- packages/interpreter/src/interpreter.js | 12 ++++--- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 760bf7662..a991ec788 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -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,50 @@ 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 { + let mm = serde_json::from_str(payload); + match mm { + 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 = 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 +127,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc &'static str { +fn event_name_from_type(typ: &str) -> &'static str { match typ { "copy" => "copy", "cut" => "cut", diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index f4e28ca2a..90fb4a3b0 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -132,23 +132,24 @@ pub fn launch_with_props( .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() - .with_rpc_handler(move |_window: &Window, req: RpcRequest| { - match req.method.as_str() { - "user_event" => { - let event = events::trigger_from_serialized(req.params.unwrap()); - log::trace!("User event: {:?}", event); - sender.unbounded_send(SchedulerMsg::Event(event)).unwrap(); - } - "initialize" => { - is_ready.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = proxy.send_event(UserWindowEvent::Update); - } - "browser_open" => { - println!("browser_open"); - let data = req.params.unwrap(); - log::trace!("Open browser: {:?}", data); - if let Some(arr) = data.as_array() { - if let Some(temp) = arr[0].as_object() { + .with_ipc_handler(move |_window: &Window, payload: String| { + parse_ipc_message(&payload) + .map(|message| match message.method() { + "user_event" => { + let event = trigger_from_serialized(message.params()); + log::trace!("User event: {:?}", event); + sender.unbounded_send(SchedulerMsg::Event(event)).unwrap(); + } + "initialize" => { + is_ready.store(true, std::sync::atomic::Ordering::Relaxed); + let _ = proxy + .send_event(user_window_events::UserWindowEvent::Update); + } + "browser_open" => { + println!("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) { @@ -157,11 +158,8 @@ pub fn launch_with_props( } } } - } - _ => {} - } - 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. diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index f0acd0802..f042b74c9 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -2,7 +2,7 @@ export function main() { let root = window.document.getElementById("main"); if (root != null) { window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); + window.ipc.postMessage(serializeIpcMessage("initialize")) } } export class Interpreter { @@ -207,7 +207,7 @@ export class Interpreter { event.preventDefault(); const href = target.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { - window.rpc.call("browser_open", { href }); + window.ipc.postMessage(serializeIpcMessage("browser_open", { href })) } } } @@ -261,11 +261,12 @@ export class Interpreter { if (realId == null) { return; } - window.rpc.call("user_event", { + 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); @@ -544,6 +545,9 @@ export function serialize_event(event) { } } } +function serializeIpcMessage(method, params = {}) { + return JSON.stringify({ method, params }); +} const bool_attrs = { allowfullscreen: true, allowpaymentrequest: true, From 73ce79bd2a31e409fc34d0673c8efbd08fc01d31 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:57:46 +0100 Subject: [PATCH 047/129] Extract protocol hander into module --- packages/desktop/src/lib.rs | 47 +++----------------------------- packages/desktop/src/protocol.rs | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 packages/desktop/src/protocol.rs diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 90fb4a3b0..31812f680 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -160,50 +160,11 @@ pub fn launch_with_props( } _ => (), }) - .with_custom_protocol(String::from("dioxus"), move |request| { - // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case" - // For now, we only serve two pieces of content which get included as bytes into the final binary. - let path = request.uri().replace("dioxus://", ""); - - // all assets shouldbe called from index.html - let trimmed = path.trim_start_matches("index.html/"); - - if trimmed.is_empty() { - wry::http::ResponseBuilder::new() - .mimetype("text/html") - .body(include_bytes!("./index.html").to_vec()) - } else if trimmed == "index.js" { - wry::http::ResponseBuilder::new() - .mimetype("text/javascript") - .body(dioxus_interpreter_js::INTERPRTER_JS.as_bytes().to_vec()) - } else { - // Read the file content from file path - use std::fs::read; - - let path_buf = std::path::Path::new(trimmed).canonicalize()?; - let cur_path = std::path::Path::new(".").canonicalize()?; - - if !path_buf.starts_with(cur_path) { - return wry::http::ResponseBuilder::new() - .status(wry::http::status::StatusCode::FORBIDDEN) - .body(String::from("Forbidden").into_bytes()); - } - - if !path_buf.exists() { - return wry::http::ResponseBuilder::new() - .status(wry::http::status::StatusCode::NOT_FOUND) - .body(String::from("Not Found").into_bytes()); - } - - let mime = mime_guess::from_path(&path_buf).first_or_octet_stream(); - - // do not let path searching to go two layers beyond the caller level - let data = read(path_buf)?; - let meta = format!("{}", mime); - - wry::http::ResponseBuilder::new().mimetype(&meta).body(data) - } + .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() diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs new file mode 100644 index 000000000..c8f007c1d --- /dev/null +++ b/packages/desktop/src/protocol.rs @@ -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 { + // 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) + } +} From cf543ab1dfe0a0af423c37d3850af04432cb075a Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:58:37 +0100 Subject: [PATCH 048/129] Extract controller into module --- packages/desktop/src/controller.rs | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/desktop/src/controller.rs diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs new file mode 100644 index 000000000..7d9a2f432 --- /dev/null +++ b/packages/desktop/src/controller.rs @@ -0,0 +1,106 @@ +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, + pub(super) sender: futures_channel::mpsc::UnboundedSender, + pub(super) pending_edits: Arc>>, + pub(super) quit_app_on_close: bool, + pub(super) is_ready: Arc, +} + +impl DesktopController { + // Launch the virtualdom on its own thread managed by tokio + // returns the desktop state + pub(super) fn new_on_tokio( + root: Component

, + props: P, + proxy: EventLoopProxy, + ) -> Self { + let edit_queue = Arc::new(RwLock::new(VecDeque::new())); + let pending_edits = edit_queue.clone(); + + let (sender, receiver) = futures_channel::mpsc::unbounded::(); + 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()); + + 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(); + } + } else { + println!("waiting for ready"); + } + } +} From e7a0e5f1d9fb03e9aa1ef8aa9978898f4c6ba56f Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:58:50 +0100 Subject: [PATCH 049/129] Extract user window events into module --- packages/desktop/src/user_window_events.rs | 72 ++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 packages/desktop/src/user_window_events.rs diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs new file mode 100644 index 000000000..93eeaccf1 --- /dev/null +++ b/packages/desktop/src/user_window_events.rs @@ -0,0 +1,72 @@ +use wry::application::event_loop::ControlFlow; +use wry::application::window::Fullscreen as WryFullscreen; + +use crate::controller::DesktopController; + +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) => { + window.current_monitor().map(|handle| { + 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(), + } +} From 2828f45e12e129a29006bdacbf0ccbec38fefbfe Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:59:25 +0100 Subject: [PATCH 050/129] Clean up desktop's lib.rs --- packages/desktop/src/lib.rs | 246 ++---------------------------------- 1 file changed, 11 insertions(+), 235 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 31812f680..371adced5 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -3,31 +3,28 @@ #![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 desktop_context::DesktopContext; 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, window::Fullscreen}, - webview::RpcRequest, - webview::{WebView, WebViewBuilder}, -}; +use wry::webview::WebViewBuilder; + +use crate::events::trigger_from_serialized; /// Launch the WebView and run the event loop. /// @@ -194,114 +191,8 @@ pub fn launch_with_props( _ => {} }, - Event::UserEvent(_evt) => { - // - match _evt { - UserWindowEvent::Update => desktop.try_load_ready_webviews(), - UserWindowEvent::DragWindow => { - // this loop just run once, because dioxus-desktop is unsupport multi-window. - for webview in desktop.webviews.values() { - let window = webview.window(); - // start to drag the window. - // if the drag_window have any err. we don't do anything. - - if window.fullscreen().is_some() { - return; - } - - let _ = window.drag_window(); - } - } - UserWindowEvent::CloseWindow => { - // close window - *control_flow = ControlFlow::Exit; - } - UserWindowEvent::Visible(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_visible(state); - } - } - UserWindowEvent::Minimize(state) => { - // this loop just run once, because dioxus-desktop is unsupport multi-window. - for webview in desktop.webviews.values() { - let window = webview.window(); - // change window minimized state. - window.set_minimized(state); - } - } - UserWindowEvent::Maximize(state) => { - // this loop just run once, because dioxus-desktop is unsupport multi-window. - for webview in desktop.webviews.values() { - let window = webview.window(); - // change window maximized state. - window.set_maximized(state); - } - } - UserWindowEvent::Fullscreen(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - - let current_monitor = window.current_monitor(); - - if current_monitor.is_none() { - return; - } - - let fullscreen = if state { - Some(Fullscreen::Borderless(current_monitor)) - } else { - None - }; - - window.set_fullscreen(fullscreen); - } - } - UserWindowEvent::FocusWindow => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_focus(); - } - } - UserWindowEvent::Resizable(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_resizable(state); - } - } - UserWindowEvent::AlwaysOnTop(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_always_on_top(state); - } - } - - UserWindowEvent::CursorVisible(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_cursor_visible(state); - } - } - UserWindowEvent::CursorGrab(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - let _ = window.set_cursor_grab(state); - } - } - - UserWindowEvent::SetTitle(content) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_title(&content); - } - } - UserWindowEvent::SetDecorations(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_decorations(state); - } - } - } + Event::UserEvent(user_event) => { + user_window_events::handler(user_event, &mut desktop, control_flow) } Event::MainEventsCleared => {} Event::Resumed => {} @@ -312,118 +203,3 @@ pub fn launch_with_props( } }) } - -pub enum UserWindowEvent { - Update, - DragWindow, - CloseWindow, - FocusWindow, - Visible(bool), - Minimize(bool), - Maximize(bool), - Resizable(bool), - AlwaysOnTop(bool), - Fullscreen(bool), - - CursorVisible(bool), - CursorGrab(bool), - - SetTitle(String), - SetDecorations(bool), -} - -pub struct DesktopController { - pub proxy: EventLoopProxy, - pub webviews: HashMap, - pub sender: futures_channel::mpsc::UnboundedSender, - pub pending_edits: Arc>>, - pub quit_app_on_close: bool, - pub is_ready: Arc, -} - -impl DesktopController { - // Launch the virtualdom on its own thread managed by tokio - // returns the desktop state - pub fn new_on_tokio( - root: Component

, - props: P, - evt: EventLoopProxy, - ) -> Self { - let edit_queue = Arc::new(RwLock::new(VecDeque::new())); - let pending_edits = edit_queue.clone(); - - let (sender, receiver) = futures_channel::mpsc::unbounded::(); - let return_sender = sender.clone(); - let proxy = evt.clone(); - - let desktop_context_proxy = proxy.clone(); - std::thread::spawn(move || { - // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - - runtime.block_on(async move { - let mut dom = - VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver)); - - let window_context = DesktopContext::new(desktop_context_proxy); - - dom.base_scope().provide_context(window_context); - - let edits = dom.rebuild(); - - edit_queue - .write() - .unwrap() - .push_front(serde_json::to_string(&edits.edits).unwrap()); - - loop { - dom.wait_for_work().await; - let mut muts = dom.work_with_deadline(|| false); - - while let Some(edit) = muts.pop() { - edit_queue - .write() - .unwrap() - .push_front(serde_json::to_string(&edit.edits).unwrap()); - } - - let _ = evt.send_event(UserWindowEvent::Update); - } - }) - }); - - Self { - pending_edits, - sender: return_sender, - proxy, - webviews: HashMap::new(), - is_ready: Arc::new(AtomicBool::new(false)), - quit_app_on_close: true, - } - } - - pub fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { - self.webviews.remove(&window_id); - - if self.webviews.is_empty() && self.quit_app_on_close { - *control_flow = ControlFlow::Exit; - } - } - - pub fn try_load_ready_webviews(&mut self) { - if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { - let mut queue = self.pending_edits.write().unwrap(); - let (_id, view) = self.webviews.iter_mut().next().unwrap(); - - while let Some(edit) = queue.pop_back() { - view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) - .unwrap(); - } - } else { - println!("waiting for ready"); - } - } -} From 934d5998dbc5bc8e14cce10d3b4f1332205f6ae8 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 20:00:45 +0100 Subject: [PATCH 051/129] Support maximize toggle and devtool --- packages/desktop/src/desktop_context.rs | 47 +++++++++++++------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index d180e1235..e1b726d73 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,7 +3,8 @@ use std::rc::Rc; use dioxus_core::ScopeState; use wry::application::event_loop::EventLoopProxy; -use crate::UserWindowEvent; +use crate::user_window_events::UserWindowEvent; +use UserWindowEvent::*; type ProxyType = EventLoopProxy; @@ -35,75 +36,77 @@ impl DesktopContext { /// onmousedown: move |_| { desktop.drag_window(); } /// ``` pub fn drag(&self) { - let _ = self.proxy.send_event(UserWindowEvent::DragWindow); + let _ = self.proxy.send_event(DragWindow); } /// set window minimize state pub fn set_minimized(&self, minimized: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized)); + let _ = self.proxy.send_event(Minimize(minimized)); } /// set window maximize state pub fn set_maximized(&self, maximized: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); + let _ = self.proxy.send_event(Maximize(maximized)); + } + + /// toggle window maximize state + pub fn toggle_maximized(&self) { + let _ = self.proxy.send_event(MaximizeToggle); } /// set window visible or not pub fn set_visible(&self, visible: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Visible(visible)); + let _ = self.proxy.send_event(Visible(visible)); } /// close window pub fn close(&self) { - let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); + let _ = self.proxy.send_event(CloseWindow); } /// set window to focus pub fn focus(&self) { - let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); + let _ = self.proxy.send_event(FocusWindow); } /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { - let _ = self - .proxy - .send_event(UserWindowEvent::Fullscreen(fullscreen)); + let _ = self.proxy.send_event(Fullscreen(fullscreen)); } /// set resizable state pub fn set_resizable(&self, resizable: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); + let _ = self.proxy.send_event(Resizable(resizable)); } /// set the window always on top pub fn set_always_on_top(&self, top: bool) { - let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top)); + let _ = self.proxy.send_event(AlwaysOnTop(top)); } // set cursor visible or not pub fn set_cursor_visible(&self, visible: bool) { - let _ = self - .proxy - .send_event(UserWindowEvent::CursorVisible(visible)); + let _ = self.proxy.send_event(CursorVisible(visible)); } // set cursor grab pub fn set_cursor_grab(&self, grab: bool) { - let _ = self.proxy.send_event(UserWindowEvent::CursorGrab(grab)); + let _ = self.proxy.send_event(CursorGrab(grab)); } /// set window title pub fn set_title(&self, title: &str) { - let _ = self - .proxy - .send_event(UserWindowEvent::SetTitle(String::from(title))); + let _ = self.proxy.send_event(SetTitle(String::from(title))); } /// change window to borderless pub fn set_decorations(&self, decoration: bool) { - let _ = self - .proxy - .send_event(UserWindowEvent::SetDecorations(decoration)); + let _ = self.proxy.send_event(SetDecorations(decoration)); + } + + /// opens DevTool window + pub fn devtool(&self) { + let _ = self.proxy.send_event(DevTool); } } From 932ad0164454c65335f7c62078d3a7250a8608c3 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 20:57:30 +0100 Subject: [PATCH 052/129] Make clippy happy --- packages/desktop/src/user_window_events.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs index 93eeaccf1..aab14561b 100644 --- a/packages/desktop/src/user_window_events.rs +++ b/packages/desktop/src/user_window_events.rs @@ -51,9 +51,9 @@ pub(super) fn handler( Maximize(state) => window.set_maximized(state), MaximizeToggle => window.set_maximized(!window.is_maximized()), Fullscreen(state) => { - window.current_monitor().map(|handle| { + 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), From dd9f0f362ec8e7193a4920c2ee46246f1cbe67c9 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 14 Feb 2022 16:52:16 +0800 Subject: [PATCH 053/129] fix: statement problem --- packages/interpreter/src/interpreter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index f0acd0802..a631d0368 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -102,10 +102,11 @@ export class Interpreter { SetAttribute(root, field, value, ns) { const name = field; const node = this.nodes[root]; + console.log(ns); if (ns === "style") { // @ts-ignore node.style[name] = value; - } else if (ns != null || ns !== undefined) { + } else if (ns != null || ns != undefined) { node.setAttributeNS(ns, name, value); } else { switch (name) { From e3e5f22bc7605b7019d5f92eeff719abcbf512c5 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 14 Feb 2022 16:53:35 +0800 Subject: [PATCH 054/129] feat: move `default_icon` to assets --- packages/desktop/src/{ => assets}/default_icon.bin | Bin packages/desktop/src/{ => assets}/default_icon.png | Bin packages/desktop/src/cfg.rs | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/desktop/src/{ => assets}/default_icon.bin (100%) rename packages/desktop/src/{ => assets}/default_icon.png (100%) diff --git a/packages/desktop/src/default_icon.bin b/packages/desktop/src/assets/default_icon.bin similarity index 100% rename from packages/desktop/src/default_icon.bin rename to packages/desktop/src/assets/default_icon.bin diff --git a/packages/desktop/src/default_icon.png b/packages/desktop/src/assets/default_icon.png similarity index 100% rename from packages/desktop/src/default_icon.png rename to packages/desktop/src/assets/default_icon.png diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 49a26ae78..2a1067ded 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -88,7 +88,7 @@ impl DesktopConfig { impl DesktopConfig { pub(crate) fn with_default_icon(mut self) -> Self { - let bin: &[u8] = include_bytes!("default_icon.bin"); + 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 From 3317b3aac51f2e1225ffc873cfa0ed25b3096f3d Mon Sep 17 00:00:00 2001 From: dazmaks Date: Mon, 14 Feb 2022 17:13:09 +0300 Subject: [PATCH 055/129] fix: typo --- docs/reference/src/mobile/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/src/mobile/index.md b/docs/reference/src/mobile/index.md index 2e8344ed1..aa3470761 100644 --- a/docs/reference/src/mobile/index.md +++ b/docs/reference/src/mobile/index.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo moble init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. From 8ca505b65beba1edf74eddf47d4da5a8e7be7033 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 14 Feb 2022 09:30:08 -0500 Subject: [PATCH 056/129] feat: better link --- packages/router/src/components/link.rs | 48 +++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 18e9fd92e..b2b69108f 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -34,6 +34,17 @@ pub struct LinkProps<'a> { #[props(default, strip_option)] title: Option<&'a str>, + #[props(default = true)] + autodetect: bool, + + /// Is this link an external link? + #[props(default = false)] + external: bool, + + /// New tab? + #[props(default = false)] + new_tab: bool, + children: Element<'a>, #[props(default)] @@ -41,17 +52,38 @@ pub struct LinkProps<'a> { } pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - // log::trace!("render Link to {}", cx.props.to); if let Some(service) = cx.consume_context::() { + 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 } From 5a908d1e8b92d712c35c7f05af5956926acec29e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 14 Feb 2022 12:23:30 -0500 Subject: [PATCH 057/129] fix: remove console log --- packages/interpreter/src/interpreter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index a631d0368..5b81d3a9f 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -102,7 +102,6 @@ export class Interpreter { SetAttribute(root, field, value, ns) { const name = field; const node = this.nodes[root]; - console.log(ns); if (ns === "style") { // @ts-ignore node.style[name] = value; From 1413631d306065b76a9d2dce1c6e709c0def0bd4 Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Mon, 14 Feb 2022 19:08:10 -0500 Subject: [PATCH 058/129] update: match DioxusLabs/docs#3 --- README.md | 8 ++++---- docs/guide/src/README.md | 12 ++++++------ docs/guide/src/setup.md | 2 +- docs/reference/src/SUMMARY.md | 11 ++++++----- .../src/{desktop/index.md => platforms/desktop.md} | 2 +- docs/reference/src/platforms/index.md | 10 ++++++++++ .../src/{mobile/index.md => platforms/mobile.md} | 2 +- .../reference/src/{ssr/index.md => platforms/ssr.md} | 0 .../reference/src/{tui/index.md => platforms/tui.md} | 2 +- .../reference/src/{web/index.md => platforms/web.md} | 2 +- 10 files changed, 31 insertions(+), 20 deletions(-) rename docs/reference/src/{desktop/index.md => platforms/desktop.md} (98%) create mode 100644 docs/reference/src/platforms/index.md rename docs/reference/src/{mobile/index.md => platforms/mobile.md} (99%) rename docs/reference/src/{ssr/index.md => platforms/ssr.md} (100%) rename docs/reference/src/{tui/index.md => platforms/tui.md} (98%) rename docs/reference/src/{web/index.md => platforms/web.md} (98%) diff --git a/README.md b/README.md index 02857ed18..23cd9c516 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,10 @@ cargo run --example EXAMPLE - - - - + + + +
TutorialWebDesktopSSRMobileWebDesktopSSRMobile State
diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index bbbd9f85f..da6c0156f 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -18,7 +18,7 @@ fn app(cx: Scope) -> Element { In general, Dioxus and React share many functional similarities. If this guide is lacking in any general concept or an error message is confusing, React's documentation might be more helpful. We are dedicated to providing a *familiar* toolkit for UI in Rust, so we've chosen to follow in the footsteps of popular UI frameworks (React, Redux, etc). If you know React, then you already know Dioxus. If you don't know either, this guide will still help you! -> This is an introduction book! For advanced topics, check out the [Reference](https://dioxuslabs.com/reference) instead. +> This is an introduction book! For advanced topics, check out the [Reference](/reference) instead. ## Multiplatform @@ -37,7 +37,7 @@ The Web is the best-supported target platform for Dioxus. To run on the Web, you Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features. -[Jump to the getting started guide for the web.]() +[Jump to the getting started guide for the web.](/reference/platforms/web) Examples: - [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc) @@ -55,7 +55,7 @@ For rendering statically to an `.html` file or from a WebServer, then you'll wan let contents = dioxus::ssr::render_vdom(&dom); ``` -[Jump to the getting started guide for SSR.]() +[Jump to the getting started guide for SSR.](/reference/platforms/ssr) Examples: - [Example DocSite](https://github.com/dioxusLabs/docsite) @@ -68,13 +68,13 @@ The desktop is a powerful target for Dioxus, but is currently limited in capabil Desktop APIs will likely be in flux as we figure out better patterns than our ElectronJS counterpart. -[Jump to the getting started guide for Desktop.]() +[Jump to the getting started guide for Desktop.](/reference/platforms/desktop) Examples: - [File explorer](https://github.com/dioxusLabs/file-explorer/) - [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) -[![File ExplorerExample](https://github.com/DioxusLabs/file-explorer-example/raw/master/image.png)](https://github.com/dioxusLabs/file-explorer/) +[![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer) ### Mobile Support --- @@ -82,7 +82,7 @@ Mobile is currently the least-supported renderer target for Dioxus. Mobile apps Mobile support is currently best suited for CRUD-style apps, ideally for internal teams who need to develop quickly but don't care much about animations or native widgets. -[Jump to the getting started guide for Mobile.]() +[Jump to the getting started guide for Mobile.](/reference/platforms/mobile) Examples: - [Todo App](https://github.com/DioxusLabs/example-projects/blob/master/ios_demo) diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 1dc9434c5..75cfc6606 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -8,7 +8,7 @@ We'll learn about: - Suggested cargo extensions -For platform-specific guides, check out the [Platform Specific Guides](../platforms/00-index.md). +For platform-specific guides, check out the [Platform Specific Guides](/reference/platforms/index.md). # Setting up Dioxus diff --git a/docs/reference/src/SUMMARY.md b/docs/reference/src/SUMMARY.md index 3feb9fe36..18f351bfa 100644 --- a/docs/reference/src/SUMMARY.md +++ b/docs/reference/src/SUMMARY.md @@ -2,11 +2,12 @@ - [Introduction](README.md) -- [Web](web/index.md) -- [Desktop](desktop/index.md) -- [Mobile](mobile/index.md) -- [SSR](ssr/index.md) -- [TUI](tui/index.md) +- [Platforms](platforms/index.md) + - [Web](platforms/web.md) + - [Server Side Rendering](platforms/ssr.md) + - [Desktop](platforms/desktop.md) + - [Mobile](platforms/mobile.md) + - [TUI](platforms/tui.md) - [Advanced Guides](guide/index.md) - [RSX in Depth](guide/rsx_in_depth.md) diff --git a/docs/reference/src/desktop/index.md b/docs/reference/src/platforms/desktop.md similarity index 98% rename from docs/reference/src/desktop/index.md rename to docs/reference/src/platforms/desktop.md index 8011f1eed..18393f132 100644 --- a/docs/reference/src/desktop/index.md +++ b/docs/reference/src/platforms/desktop.md @@ -1,4 +1,4 @@ -# Desktop +# Getting Started: 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. diff --git a/docs/reference/src/platforms/index.md b/docs/reference/src/platforms/index.md new file mode 100644 index 000000000..fbbabd35d --- /dev/null +++ b/docs/reference/src/platforms/index.md @@ -0,0 +1,10 @@ +# Platforms + +Dioxus supports many different platforms. Below are a list of guides that walk you through setting up Dioxus for a specific platform. + +### Setup Guides +- [Web](web.md) +- [Server Side Rendering](ssr.md) +- [Desktop](desktop.md) +- [Mobile](mobile.md) +- [TUI](tui.md) \ No newline at end of file diff --git a/docs/reference/src/mobile/index.md b/docs/reference/src/platforms/mobile.md similarity index 99% rename from docs/reference/src/mobile/index.md rename to docs/reference/src/platforms/mobile.md index 2e8344ed1..5a41ffe60 100644 --- a/docs/reference/src/mobile/index.md +++ b/docs/reference/src/platforms/mobile.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo moble init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. diff --git a/docs/reference/src/ssr/index.md b/docs/reference/src/platforms/ssr.md similarity index 100% rename from docs/reference/src/ssr/index.md rename to docs/reference/src/platforms/ssr.md diff --git a/docs/reference/src/tui/index.md b/docs/reference/src/platforms/tui.md similarity index 98% rename from docs/reference/src/tui/index.md rename to docs/reference/src/platforms/tui.md index 3947883d4..e646bd4ba 100644 --- a/docs/reference/src/tui/index.md +++ b/docs/reference/src/platforms/tui.md @@ -1,4 +1,4 @@ -# TUI +# Getting Started: TUI TUI support is currently quite experimental. Even the project name will change. But, if you're willing to venture into the realm of the unknown, this guide will get you started. diff --git a/docs/reference/src/web/index.md b/docs/reference/src/platforms/web.md similarity index 98% rename from docs/reference/src/web/index.md rename to docs/reference/src/platforms/web.md index 062cba177..2ff5f621e 100644 --- a/docs/reference/src/web/index.md +++ b/docs/reference/src/platforms/web.md @@ -1,4 +1,4 @@ -# Getting Started: Dioxus for Web +# Getting Started: Web [*"Pack your things, we're going on an adventure!"*](https://trunkrs.dev) From 7c2d911fbf1abb23a216f3ef5c235bc75766ee4a Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 15 Feb 2022 09:54:32 -0600 Subject: [PATCH 059/129] Fix closure signature for use_ref example in interactivity docs The example showed the closure accepting a single argument, but it accepts none. --- docs/guide/src/interactivity/hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/interactivity/hooks.md b/docs/guide/src/interactivity/hooks.md index a90afe2dc..a84a0b129 100644 --- a/docs/guide/src/interactivity/hooks.md +++ b/docs/guide/src/interactivity/hooks.md @@ -140,7 +140,7 @@ fn Child(cx: Scope, name: String) -> Element { // ✅ Or, use a hashmap with use_ref ```rust -let ages = use_ref(&cx, |_| HashMap::new()); +let ages = use_ref(&cx, || HashMap::new()); names.iter().map(|name| { let age = ages.get(name).unwrap(); From e43a8a9b6a68bd2344bc86c6613ab046d89da40b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 11:03:13 -0500 Subject: [PATCH 060/129] feat: remove old async channel for new channel in ric raf web code --- packages/web/Cargo.toml | 2 +- packages/web/src/dom.rs | 7 ++----- packages/web/src/lib.rs | 2 +- packages/web/src/ric_raf.rs | 21 +++++++++++---------- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7a3baa955..31a7ff6be 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -23,13 +23,13 @@ wasm-logger = "0.2.0" console_error_panic_hook = { version = "0.1.7", optional = true } wasm-bindgen-test = "0.3.29" once_cell = "1.9.0" -async-channel = "1.6.1" anyhow = "1.0.53" gloo-timers = { version = "0.2.3", features = ["futures"] } futures-util = "0.3.19" smallstr = "0.2.0" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0", features = ["web"] } serde-wasm-bindgen = "0.4.2" +futures-channel = "0.3.21" [dependencies.web-sys] version = "0.3.56" diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 48fb25e6e..b2eb65d08 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -87,14 +87,11 @@ impl WebsysDom { } }); + // a match here in order to avoid some error during runtime browser test let document = load_document(); let root = match document.get_element_by_id(&cfg.rootname) { Some(root) => root, - // a match here in order to avoid some error during runtime browser test - None => { - let body = document.create_element("body").ok().unwrap(); - body - } + None => document.create_element("body").ok().unwrap(), }; Self { diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index cef1bc14c..90fe84d04 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -211,7 +211,7 @@ pub async fn run_with_props(root: Component, root_props: T websys_dom.apply_edits(edits.edits); } - let work_loop = ric_raf::RafLoop::new(); + let mut work_loop = ric_raf::RafLoop::new(); loop { log::trace!("waiting for work"); diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 2ac0668b9..89819fff0 100644 --- a/packages/web/src/ric_raf.rs +++ b/packages/web/src/ric_raf.rs @@ -7,6 +7,7 @@ //! Because RIC doesn't work on Safari, we polyfill using the "ricpolyfill.js" file and use some basic detection to see //! if RIC is available. +use futures_util::StreamExt; use gloo_timers::future::TimeoutFuture; use js_sys::Function; use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; @@ -14,21 +15,21 @@ use web_sys::{window, Window}; pub(crate) struct RafLoop { window: Window, - ric_receiver: async_channel::Receiver, - raf_receiver: async_channel::Receiver<()>, + ric_receiver: futures_channel::mpsc::UnboundedReceiver, + raf_receiver: futures_channel::mpsc::UnboundedReceiver<()>, ric_closure: Closure, raf_closure: Closure, } impl RafLoop { pub fn new() -> Self { - let (raf_sender, raf_receiver) = async_channel::unbounded(); + let (raf_sender, raf_receiver) = futures_channel::mpsc::unbounded(); let raf_closure: Closure = Closure::wrap(Box::new(move |_v: JsValue| { - raf_sender.try_send(()).unwrap() + raf_sender.unbounded_send(()).unwrap() })); - let (ric_sender, ric_receiver) = async_channel::unbounded(); + let (ric_sender, ric_receiver) = futures_channel::mpsc::unbounded(); let has_idle_callback = { let bo = window().unwrap().dyn_into::().unwrap(); @@ -45,7 +46,7 @@ impl RafLoop { 10 }; - ric_sender.try_send(time_remaining).unwrap() + ric_sender.unbounded_send(time_remaining).unwrap() })); // execute the polyfill for safari @@ -64,16 +65,16 @@ impl RafLoop { } } /// waits for some idle time and returns a timeout future that expires after the idle time has passed - pub async fn wait_for_idle_time(&self) -> TimeoutFuture { + pub async fn wait_for_idle_time(&mut self) -> TimeoutFuture { let ric_fn = self.ric_closure.as_ref().dyn_ref::().unwrap(); let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap(); - let deadline = self.ric_receiver.recv().await.unwrap(); + let deadline = self.ric_receiver.next().await.unwrap(); TimeoutFuture::new(deadline) } - pub async fn wait_for_raf(&self) { + pub async fn wait_for_raf(&mut self) { let raf_fn = self.raf_closure.as_ref().dyn_ref::().unwrap(); let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap(); - self.raf_receiver.recv().await.unwrap(); + self.raf_receiver.next().await.unwrap(); } } From 06ea624eecc132dc2a27fa8f6b2e2605126caeb4 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 15 Feb 2022 10:27:11 -0600 Subject: [PATCH 061/129] Make all packages which require futures-channel ask for the same version If they aren't the same then Cargo cannot resolve a working version for some reason. --- packages/core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 03b378e88..4126eb47f 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -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" From a6cbe233b7f40763faa8e247b4264fba07f09c08 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 13:38:06 -0500 Subject: [PATCH 062/129] fix: empty values on desktop --- packages/desktop/src/controller.rs | 2 -- packages/desktop/src/events.rs | 3 +-- packages/html/src/events.rs | 2 ++ 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 7d9a2f432..8497a42e9 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -99,8 +99,6 @@ impl DesktopController { view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) .unwrap(); } - } else { - println!("waiting for ready"); } } } diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index a991ec788..7be4273e9 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -23,8 +23,7 @@ impl IpcMessage { } pub(crate) fn parse_ipc_message(payload: &str) -> Option { - let mm = serde_json::from_str(payload); - match mm { + match serde_json::from_str(payload) { Ok(message) => Some(message), Err(e) => { log::error!("could not parse IPC message, error: {e}"); diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 8067e6314..4336d2476 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -486,6 +486,8 @@ pub mod on { #[derive(Debug)] pub struct FormData { pub value: String, + + #[serde(default)] pub values: HashMap, /* DOMEvent: Send + SyncTarget relatedTarget */ } From ba17b57cdd244946ca780e3183cd7c9a43648380 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 13:44:05 -0500 Subject: [PATCH 063/129] fix: also include values in onchange --- packages/interpreter/src/interpreter.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index b1271b303..76da73752 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -2,7 +2,7 @@ export function main() { let root = window.document.getElementById("main"); if (root != null) { window.interpreter = new Interpreter(root); - window.ipc.postMessage(serializeIpcMessage("initialize")) + window.ipc.postMessage(serializeIpcMessage("initialize")); } } export class Interpreter { @@ -207,7 +207,9 @@ export class Interpreter { event.preventDefault(); const href = target.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage(serializeIpcMessage("browser_open", { href })) + window.ipc.postMessage( + serializeIpcMessage("browser_open", { href }) + ); } } } @@ -261,12 +263,13 @@ export class Interpreter { if (realId == null) { return; } - window.ipc.postMessage(serializeIpcMessage( - "user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - })); + window.ipc.postMessage( + serializeIpcMessage("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }) + ); } }; this.NewEventListener(edit.event_name, edit.root, handler); @@ -342,6 +345,7 @@ export function serialize_event(event) { } return { value: value, + values: {}, }; } case "input": From 61f9b9dd83a3151b3ee4054792e7b7bc5eb2af95 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 13:46:14 -0500 Subject: [PATCH 064/129] fix: remove serde attr --- packages/html/src/events.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 4336d2476..8067e6314 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -486,8 +486,6 @@ pub mod on { #[derive(Debug)] pub struct FormData { pub value: String, - - #[serde(default)] pub values: HashMap, /* DOMEvent: Send + SyncTarget relatedTarget */ } From ee2e986a307b385e88ad82ca604dd2d34e06ec29 Mon Sep 17 00:00:00 2001 From: Denis Richartz Date: Wed, 16 Feb 2022 15:34:43 +0100 Subject: [PATCH 065/129] fix unnecessary div --- packages/router/src/components/router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index bf23645f5..df1daab78 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -33,6 +33,6 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { } cx.render(rsx!( - div { &cx.props.children } + &cx.props.children )) } From b71cf6ed4ad4367da264827b55fbec537f631d41 Mon Sep 17 00:00:00 2001 From: Denis Richartz Date: Wed, 16 Feb 2022 16:11:57 +0100 Subject: [PATCH 066/129] cargo fmt --- packages/router/src/components/router.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index df1daab78..c117c6650 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -32,7 +32,5 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { cx.props.onchange.call(path.to_string()); } - cx.render(rsx!( - &cx.props.children - )) + cx.render(rsx!(&cx.props.children)) } From aa60971c5c9209d42b3f39b636e757c910a6f61e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:03:53 -0500 Subject: [PATCH 067/129] fix: remove preventdefault on form --- packages/html/src/elements.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 3d160211e..c6b1cb7e0 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1101,12 +1101,6 @@ impl label { } } -impl form { - 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 /// Build a From c6bdb5db76629e7fae77c110a21509d07ece2e89 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:08:57 -0500 Subject: [PATCH 068/129] fix: login form --- examples/login_form.rs | 68 +++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index fdd1843ce..62c6f982b 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -3,14 +3,14 @@ use dioxus::events::*; use dioxus::prelude::*; -use dioxus::router::{Link, Router, Route, RouterService}; +use dioxus::router::{Link, Route, Router, RouterService}; fn main() { dioxus::desktop::launch(APP); } static APP: Component = |cx| { - cx.render(rsx!{ + cx.render(rsx! { Router { Route { to: "/", home() } Route { to: "/login", login() } @@ -25,65 +25,45 @@ fn home(cx: Scope) -> Element { }) } - fn login(cx: Scope) -> Element { - let username = use_state(&cx, String::new); - let password = use_state(&cx, String::new); - let service = cx.consume_context::()?; - let onsubmit = move |_| { - cx.push_future({ - let (username, password) = (username.get().clone(), password.get().clone()); - let service = service.clone(); + let onsubmit = move |evt: FormEvent| { + to_owned![service]; + let username = evt.values["username"].clone(); + let password = evt.values["password"].clone(); - async move { - let params = [ - ("username", username.to_string()), - ("password", password.to_string()) - ]; + cx.spawn(async move { + let resp = reqwest::Client::new() + .post("http://localhost/login") + .form(&[("username", username), ("password", password)]) + .send() + .await; - let resp = reqwest::Client::new() - .post("http://localhost/login") - .form(¶ms) - .send() - .await; + match resp { + // Parse data from here, such as storing a response token + Ok(data) => service.push_route("/"), - match resp { - Ok(data) => { - // Parse data from here, such as storing a response token - service.push_route("/"); - } - Err(err) => {} //Handle any errors from the fetch here - } + //Handle any errors from the fetch here + Err(err) => {} } }); }; - cx.render(rsx!{ + cx.render(rsx! { h1 { "Login" } form { onsubmit: onsubmit, - // Prevent the default behavior of to post - prevent_default: "onsubmit", - input { - oninput: move |evt| username.set(evt.value.clone()) - } - label { - "Username" - } + prevent_default: "onsubmit", // Prevent the default behavior of to post + input { r#type: "text" } + label { "Username" } br {} - input { - oninput: move |evt| password.set(evt.value.clone()), - r#type: "password" - } + input { r#type: "password" } label { "Password" } br {} - button { - "Login" - } + button { "Login" } } }) -} \ No newline at end of file +} From babe8627394c47491348b7e3cec54ce02a31e3b2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:11:31 -0500 Subject: [PATCH 069/129] fix: login example to use proper methods --- examples/login_form.rs | 23 +++++++++++------------ packages/router/src/hooks/use_route.rs | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index 62c6f982b..cdf05d7c3 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -3,20 +3,20 @@ use dioxus::events::*; use dioxus::prelude::*; -use dioxus::router::{Link, Route, Router, RouterService}; +use dioxus::router::{use_router, Link, Route, Router}; fn main() { - dioxus::desktop::launch(APP); + dioxus::desktop::launch(app); } -static APP: Component = |cx| { +fn app(cx: Scope) -> Element { cx.render(rsx! { Router { Route { to: "/", home() } Route { to: "/login", login() } } }) -}; +} fn home(cx: Scope) -> Element { cx.render(rsx! { @@ -26,7 +26,7 @@ fn home(cx: Scope) -> Element { } fn login(cx: Scope) -> Element { - let service = cx.consume_context::()?; + let service = use_router(&cx); let onsubmit = move |evt: FormEvent| { to_owned![service]; @@ -42,10 +42,10 @@ fn login(cx: Scope) -> Element { match resp { // Parse data from here, such as storing a response token - Ok(data) => service.push_route("/"), + Ok(_data) => service.push_route("/"), //Handle any errors from the fetch here - Err(err) => {} + Err(_err) => {} } }); }; @@ -55,13 +55,12 @@ fn login(cx: Scope) -> Element { form { onsubmit: onsubmit, prevent_default: "onsubmit", // Prevent the default behavior of to post - input { r#type: "text" } + + input { "type": "text" } label { "Username" } br {} - input { r#type: "password" } - label { - "Password" - } + input { "type": "password" } + label { "Password" } br {} button { "Login" } } diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 31a157a8f..4de6dff12 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -106,7 +106,7 @@ impl Drop for UseRouteListener { } /// This hook provides access to the `RouterService` for the app. -pub fn use_router(cx: &ScopeState) -> &RouterService { +pub fn use_router(cx: &ScopeState) -> &Rc { cx.use_hook(|_| { cx.consume_context::() .expect("Cannot call use_route outside the scope of a Router component") From 4d965c8571774c59c5c6d6a3223a747301da4949 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:16:03 -0500 Subject: [PATCH 070/129] feat: simplify login example --- examples/login_form.rs | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index cdf05d7c3..1613cdc63 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -10,39 +10,22 @@ fn main() { } fn app(cx: Scope) -> Element { - cx.render(rsx! { - Router { - Route { to: "/", home() } - Route { to: "/login", login() } - } - }) -} - -fn home(cx: Scope) -> Element { - cx.render(rsx! { - h1 { "Welcome Home" } - Link { to: "/login", "Login" } - }) -} - -fn login(cx: Scope) -> Element { - let service = use_router(&cx); - let onsubmit = move |evt: FormEvent| { - to_owned![service]; - let username = evt.values["username"].clone(); - let password = evt.values["password"].clone(); - cx.spawn(async move { let resp = reqwest::Client::new() .post("http://localhost/login") - .form(&[("username", username), ("password", password)]) + .form(&[ + ("username", &evt.values["username"]), + ("password", &evt.values["password"]), + ]) .send() .await; match resp { // Parse data from here, such as storing a response token - Ok(_data) => service.push_route("/"), + Ok(_data) => { + println!("Login successful"); + } //Handle any errors from the fetch here Err(_err) => {} From b57987cfb1964d761eeb85f4019cabfcd7625f2c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:17:47 -0500 Subject: [PATCH 071/129] chore: clean up example a bit --- examples/login_form.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index 1613cdc63..7fd91262d 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -3,7 +3,6 @@ use dioxus::events::*; use dioxus::prelude::*; -use dioxus::router::{use_router, Link, Route, Router}; fn main() { dioxus::desktop::launch(app); @@ -23,12 +22,10 @@ fn app(cx: Scope) -> Element { match resp { // Parse data from here, such as storing a response token - Ok(_data) => { - println!("Login successful"); - } + Ok(_data) => println!("Login successful"), //Handle any errors from the fetch here - Err(_err) => {} + Err(_err) => println!("Login failed"), } }); }; From 3873cd1a60e6bc6371e73fe7c378cc2fb2c60407 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:40:45 -0500 Subject: [PATCH 072/129] fix: remove unused depds --- packages/core-macro/Cargo.toml | 1 - packages/desktop/Cargo.toml | 4 +--- packages/router/Cargo.toml | 2 +- packages/web/Cargo.toml | 5 ++--- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index eb8210923..8026bcdaf 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,7 +15,6 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] -once_cell = "1.8" proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } quote = "1.0" diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 2622355eb..6fd277ba4 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -13,12 +13,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } -argh = "0.1.7" serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" log = "0.4.14" -html-escape = "0.2.9" wry = { version = "0.13.1" } futures-channel = "0.3.21" tokio = { version = "1.16.1", features = [ @@ -27,7 +25,6 @@ tokio = { version = "1.16.1", features = [ "rt", "time", ], optional = true, default-features = false } -dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" @@ -47,5 +44,6 @@ 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 diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 239a3c5d8..221372b50 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -16,8 +16,8 @@ dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } serde = "1" -url = "2.2.2" serde_urlencoded = "0.7" +# url = "2.2.2" # for wasm web-sys = { version = "0.3", features = [ diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 31a7ff6be..333838398 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -15,13 +15,10 @@ dioxus-core = { path = "../core", version = "^0.1.9" } dioxus-html = { path = "../html", version = "^0.1.6" } js-sys = "0.3.56" wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] } -lazy_static = "1.4.0" wasm-bindgen-futures = "0.4.29" log = { version = "0.4.14", features = ["release_max_level_off"] } fxhash = "0.2.1" -wasm-logger = "0.2.0" console_error_panic_hook = { version = "0.1.7", optional = true } -wasm-bindgen-test = "0.3.29" once_cell = "1.9.0" anyhow = "1.0.53" gloo-timers = { version = "0.2.3", features = ["futures"] } @@ -80,3 +77,5 @@ panic_hook = ["console_error_panic_hook"] dioxus-core-macro = { path = "../core-macro" } wasm-bindgen-test = "0.3.29" dioxus-ssr = { path = "../ssr" } +wasm-logger = "0.2.0" + From d461ffc01131e76e8dc8044f3476db056ee127e7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 10:38:51 -0500 Subject: [PATCH 073/129] feat: integrate fermi --- Cargo.toml | 18 +--- docs/fermi/.gitignore | 1 + docs/fermi/book.toml | 6 ++ docs/fermi/src/SUMMARY.md | 3 + docs/fermi/src/chapter_1.md | 1 + examples/fermi.rs | 30 ++++++ packages/fermi/Cargo.toml | 14 +++ packages/fermi/README.md | 92 ++++++++++++++++++ packages/fermi/src/atoms/atom.rs | 28 ++++++ packages/fermi/src/atoms/atomfamily.rs | 25 +++++ packages/fermi/src/atoms/atomref.rs | 25 +++++ packages/fermi/src/atoms/selector.rs | 0 packages/fermi/src/atoms/selectorfamily.rs | 0 packages/fermi/src/callback.rs | 51 ++++++++++ packages/fermi/src/hooks/atom_ref.rs | 62 +++++++++++++ packages/fermi/src/hooks/atom_root.rs | 11 +++ packages/fermi/src/hooks/init_atom_root.rs | 11 +++ packages/fermi/src/hooks/read.rs | 36 +++++++ packages/fermi/src/hooks/set.rs | 13 +++ packages/fermi/src/lib.rs | 59 ++++++++++++ packages/fermi/src/root.rs | 103 +++++++++++++++++++++ src/lib.rs | 3 + tests/fermi.rs | 53 +++++++++++ 23 files changed, 630 insertions(+), 15 deletions(-) create mode 100644 docs/fermi/.gitignore create mode 100644 docs/fermi/book.toml create mode 100644 docs/fermi/src/SUMMARY.md create mode 100644 docs/fermi/src/chapter_1.md create mode 100644 examples/fermi.rs create mode 100644 packages/fermi/Cargo.toml create mode 100644 packages/fermi/README.md create mode 100644 packages/fermi/src/atoms/atom.rs create mode 100644 packages/fermi/src/atoms/atomfamily.rs create mode 100644 packages/fermi/src/atoms/atomref.rs create mode 100644 packages/fermi/src/atoms/selector.rs create mode 100644 packages/fermi/src/atoms/selectorfamily.rs create mode 100644 packages/fermi/src/callback.rs create mode 100644 packages/fermi/src/hooks/atom_ref.rs create mode 100644 packages/fermi/src/hooks/atom_root.rs create mode 100644 packages/fermi/src/hooks/init_atom_root.rs create mode 100644 packages/fermi/src/hooks/read.rs create mode 100644 packages/fermi/src/hooks/set.rs create mode 100644 packages/fermi/src/lib.rs create mode 100644 packages/fermi/src/root.rs create mode 100644 tests/fermi.rs diff --git a/Cargo.toml b/Cargo.toml index 3f0d50187..bd15590ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ dioxus-core = { path = "./packages/core", version = "^0.1.9" } dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } +fermi = { path = "./packages/fermi", version = "^0.1.0", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } dioxus-desktop = { path = "./packages/desktop", version = "^0.1.6", optional = true } @@ -36,20 +37,6 @@ web = ["dioxus-web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] -devtool = ["dioxus-desktop/devtool"] -fullscreen = ["dioxus-desktop/fullscreen"] -transparent = ["dioxus-desktop/transparent"] - -tray = ["dioxus-desktop/tray"] -ayatana = ["dioxus-desktop/ayatana"] - -# "dioxus-router/web" -# "dioxus-router/desktop" -# desktop = ["dioxus-desktop", "dioxus-router/desktop"] -# mobile = ["dioxus-mobile"] -# liveview = ["dioxus-liveview"] - - [workspace] members = [ "packages/core", @@ -61,6 +48,7 @@ members = [ "packages/desktop", "packages/mobile", "packages/interpreter", + "packages/fermi", ] [dev-dependencies] @@ -75,4 +63,4 @@ serde_json = "1.0.79" rand = { version = "0.8.4", features = ["small_rng"] } tokio = { version = "1.16.1", features = ["full"] } reqwest = { version = "0.11.9", features = ["json"] } -dioxus = { path = ".", features = ["desktop", "ssr", "router"] } +dioxus = { path = ".", features = ["desktop", "ssr", "router", "fermi"] } diff --git a/docs/fermi/.gitignore b/docs/fermi/.gitignore new file mode 100644 index 000000000..7585238ef --- /dev/null +++ b/docs/fermi/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/fermi/book.toml b/docs/fermi/book.toml new file mode 100644 index 000000000..dd6d2eacb --- /dev/null +++ b/docs/fermi/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Jonathan Kelley"] +language = "en" +multilingual = false +src = "src" +title = "Fermi Guide" diff --git a/docs/fermi/src/SUMMARY.md b/docs/fermi/src/SUMMARY.md new file mode 100644 index 000000000..7390c8289 --- /dev/null +++ b/docs/fermi/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/docs/fermi/src/chapter_1.md b/docs/fermi/src/chapter_1.md new file mode 100644 index 000000000..b743fda35 --- /dev/null +++ b/docs/fermi/src/chapter_1.md @@ -0,0 +1 @@ +# Chapter 1 diff --git a/examples/fermi.rs b/examples/fermi.rs new file mode 100644 index 000000000..2be01d019 --- /dev/null +++ b/examples/fermi.rs @@ -0,0 +1,30 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use fermi::prelude::*; + +fn main() { + dioxus::desktop::launch(app) +} + +static NAME: Atom = |_| "world".to_string(); + +fn app(cx: Scope) -> Element { + let name = use_read(&cx, NAME); + + cx.render(rsx! { + div { "hello {name}!" } + Child {} + }) +} + +fn Child(cx: Scope) -> Element { + let set_name = use_set(&cx, NAME); + + cx.render(rsx! { + button { + onclick: move |_| set_name("dioxus".to_string()), + "reset name" + } + }) +} diff --git a/packages/fermi/Cargo.toml b/packages/fermi/Cargo.toml new file mode 100644 index 000000000..48fe377c7 --- /dev/null +++ b/packages/fermi/Cargo.toml @@ -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" diff --git a/packages/fermi/README.md b/packages/fermi/README.md new file mode 100644 index 000000000..0923aa6f6 --- /dev/null +++ b/packages/fermi/README.md @@ -0,0 +1,92 @@ + +

+

Fermi âš›

+

+ Atom-based global state management solution for Dioxus +

+
+ + +
+ +----- + +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 diff --git a/packages/fermi/src/atoms/atom.rs b/packages/fermi/src/atoms/atom.rs new file mode 100644 index 000000000..2b99174b5 --- /dev/null +++ b/packages/fermi/src/atoms/atom.rs @@ -0,0 +1,28 @@ +use crate::{AtomId, AtomRoot, Readable, Writable}; + +pub type Atom = fn(AtomBuilder) -> T; +pub struct AtomBuilder; + +impl Readable for Atom { + fn read(&self, _root: AtomRoot) -> Option { + todo!() + } + fn init(&self) -> V { + (*self)(AtomBuilder) + } + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +impl Writable for Atom { + fn write(&self, _root: AtomRoot, _value: V) { + todo!() + } +} + +#[test] +fn atom_compiles() { + static TEST_ATOM: Atom<&str> = |_| "hello"; + dbg!(TEST_ATOM.init()); +} diff --git a/packages/fermi/src/atoms/atomfamily.rs b/packages/fermi/src/atoms/atomfamily.rs new file mode 100644 index 000000000..60fc22bcb --- /dev/null +++ b/packages/fermi/src/atoms/atomfamily.rs @@ -0,0 +1,25 @@ +use crate::{AtomId, AtomRoot, Readable, Writable}; +use im_rc::HashMap as ImMap; + +pub struct AtomFamilyBuilder; +pub type AtomFamily = fn(AtomFamilyBuilder) -> ImMap; + +impl Readable> for AtomFamily { + fn read(&self, _root: AtomRoot) -> Option> { + todo!() + } + + fn init(&self) -> ImMap { + (*self)(AtomFamilyBuilder) + } + + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +impl Writable> for AtomFamily { + fn write(&self, _root: AtomRoot, _value: ImMap) { + todo!() + } +} diff --git a/packages/fermi/src/atoms/atomref.rs b/packages/fermi/src/atoms/atomref.rs new file mode 100644 index 000000000..59e412031 --- /dev/null +++ b/packages/fermi/src/atoms/atomref.rs @@ -0,0 +1,25 @@ +use crate::{AtomId, AtomRoot, Readable}; +use std::cell::RefCell; + +pub struct AtomRefBuilder; +pub type AtomRef = fn(AtomRefBuilder) -> T; + +impl Readable> for AtomRef { + fn read(&self, _root: AtomRoot) -> Option> { + todo!() + } + + fn init(&self) -> RefCell { + RefCell::new((*self)(AtomRefBuilder)) + } + + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +#[test] +fn atom_compiles() { + static TEST_ATOM: AtomRef> = |_| vec![]; + dbg!(TEST_ATOM.init()); +} diff --git a/packages/fermi/src/atoms/selector.rs b/packages/fermi/src/atoms/selector.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/fermi/src/atoms/selectorfamily.rs b/packages/fermi/src/atoms/selectorfamily.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/fermi/src/callback.rs b/packages/fermi/src/callback.rs new file mode 100644 index 000000000..284fb6439 --- /dev/null +++ b/packages/fermi/src/callback.rs @@ -0,0 +1,51 @@ +use std::{future::Future, rc::Rc}; + +use dioxus_core::prelude::*; + +use crate::{Atom, AtomRoot, Readable, Writable}; + +#[derive(Clone)] +pub struct CallbackApi { + root: Rc, +} + +impl CallbackApi { + // get the current value of the atom + pub fn get(&self, atom: impl Readable) -> &V { + todo!() + } + + // get the current value of the atom in its RC container + pub fn get_rc(&self, atom: impl Readable) -> &Rc { + todo!() + } + + // set the current value of the atom + pub fn set(&self, atom: impl Writable, 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(); + )*} +} diff --git a/packages/fermi/src/hooks/atom_ref.rs b/packages/fermi/src/hooks/atom_ref.rs new file mode 100644 index 000000000..da9ad9367 --- /dev/null +++ b/packages/fermi/src/hooks/atom_ref.rs @@ -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(cx: &ScopeState, atom: AtomRef) -> &UseAtomRef { + 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 { + ptr: AtomId, + value: Rc>, + root: Rc, + scope_id: ScopeId, +} + +impl UseAtomRef { + pub fn read(&self) -> Ref { + self.value.borrow() + } + + pub fn write(&self) -> RefMut { + self.root.force_update(self.ptr); + self.value.borrow_mut() + } + + pub fn write_silent(&self) -> RefMut { + 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 Drop for UseAtomRef { + fn drop(&mut self) { + self.root.unsubscribe(self.ptr, self.scope_id) + } +} diff --git a/packages/fermi/src/hooks/atom_root.rs b/packages/fermi/src/hooks/atom_root.rs new file mode 100644 index 000000000..d2945025b --- /dev/null +++ b/packages/fermi/src/hooks/atom_root.rs @@ -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 { + cx.use_hook(|_| match cx.consume_context::() { + Some(root) => root, + None => cx.provide_root_context(AtomRoot::new(cx.schedule_update_any())), + }) +} diff --git a/packages/fermi/src/hooks/init_atom_root.rs b/packages/fermi/src/hooks/init_atom_root.rs new file mode 100644 index 000000000..edb8aafb8 --- /dev/null +++ b/packages/fermi/src/hooks/init_atom_root.rs @@ -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 { + cx.use_hook(|_| match cx.consume_context::() { + Some(ctx) => ctx, + None => cx.provide_context(AtomRoot::new(cx.schedule_update_any())), + }) +} diff --git a/packages/fermi/src/hooks/read.rs b/packages/fermi/src/hooks/read.rs new file mode 100644 index 000000000..b231c74d3 --- /dev/null +++ b/packages/fermi/src/hooks/read.rs @@ -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) -> &'a V { + use_read_rc(cx, f).as_ref() +} + +pub fn use_read_rc<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable) -> &'a Rc { + let root = use_atom_root(cx); + + struct UseReadInner { + root: Rc, + id: AtomId, + scope_id: ScopeId, + value: Option>, + } + + impl Drop for UseReadInner { + 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() +} diff --git a/packages/fermi/src/hooks/set.rs b/packages/fermi/src/hooks/set.rs new file mode 100644 index 000000000..6d77fbf8c --- /dev/null +++ b/packages/fermi/src/hooks/set.rs @@ -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) -> &'a Rc { + 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 + }) +} diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs new file mode 100644 index 000000000..023b5d286 --- /dev/null +++ b/packages/fermi/src/lib.rs @@ -0,0 +1,59 @@ +#![doc = include_str!("../README.md")] + +pub mod prelude { + pub use crate::*; +} + +mod root; +mod callback; + + +pub use atoms::*; +pub use hooks::*; +pub use root::*; +pub use callback::*; + +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 { + fn read(&self, root: AtomRoot) -> Option; + 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: Readable { + fn write(&self, root: AtomRoot, value: V); +} diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs new file mode 100644 index 000000000..5c07083be --- /dev/null +++ b/packages/fermi/src/root.rs @@ -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>, + pub update_any: Rc, +} + +pub struct Slot { + pub value: Rc, + pub subscribers: HashSet, +} + +impl AtomRoot { + pub fn new(update_any: Rc) -> Self { + Self { + update_any, + atoms: RefCell::new(HashMap::new()), + } + } + + pub fn initialize(&self, f: impl Readable) { + 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(&self, f: impl Readable, scope: ScopeId) -> Rc { + 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(&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(&self, _f: impl Readable) -> &V { + todo!() + } +} diff --git a/src/lib.rs b/src/lib.rs index d7b479a07..2db210bee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -340,6 +340,9 @@ pub use dioxus_web as web; #[cfg(feature = "desktop")] pub use dioxus_desktop as desktop; +#[cfg(feature = "fermi")] +pub use fermi; + // #[cfg(feature = "mobile")] // pub use dioxus_mobile as mobile; diff --git a/tests/fermi.rs b/tests/fermi.rs new file mode 100644 index 000000000..efef5c641 --- /dev/null +++ b/tests/fermi.rs @@ -0,0 +1,53 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use fermi::*; + +#[test] +fn test_fermi() { + let mut app = VirtualDom::new(App); + app.rebuild(); +} + +static TITLE: Atom = |_| "".to_string(); +static USERS: AtomFamily = |_| Default::default(); + +fn App(cx: Scope) -> Element { + cx.render(rsx!( + Leaf { id: 0 } + Leaf { id: 1 } + Leaf { id: 2 } + )) +} + +#[derive(Debug, PartialEq, Props)] +struct LeafProps { + id: u32, +} + +fn Leaf(cx: Scope) -> Element { + let user = use_read(&cx, TITLE); + let user = use_read(&cx, USERS); + + use_coroutine(&cx, || { + // + async move { + // + } + }); + + rsx!(cx, div { + button { + onclick: move |_| {}, + "Start" + } + button { + onclick: move |_| {}, + "Stop" + } + button { + onclick: move |_| {}, + "Reverse" + } + }) +} From d095d8c407ed8bf3b57a4944922b61918dd17995 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 10:43:34 -0500 Subject: [PATCH 074/129] fix: clippy is happy --- packages/fermi/src/callback.rs | 6 ++++-- tests/fermi.rs | 11 ++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/fermi/src/callback.rs b/packages/fermi/src/callback.rs index 284fb6439..a7ca9155f 100644 --- a/packages/fermi/src/callback.rs +++ b/packages/fermi/src/callback.rs @@ -1,8 +1,10 @@ -use std::{future::Future, rc::Rc}; +#![allow(clippy::all, unused)] + +use std::rc::Rc; use dioxus_core::prelude::*; -use crate::{Atom, AtomRoot, Readable, Writable}; +use crate::{AtomRoot, Readable, Writable}; #[derive(Clone)] pub struct CallbackApi { diff --git a/tests/fermi.rs b/tests/fermi.rs index efef5c641..1149539f0 100644 --- a/tests/fermi.rs +++ b/tests/fermi.rs @@ -26,15 +26,8 @@ struct LeafProps { } fn Leaf(cx: Scope) -> Element { - let user = use_read(&cx, TITLE); - let user = use_read(&cx, USERS); - - use_coroutine(&cx, || { - // - async move { - // - } - }); + let _user = use_read(&cx, TITLE); + let _user = use_read(&cx, USERS); rsx!(cx, div { button { From a4df07f338cc08a3b71cd5730abf4a31e7d9766a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:16:04 -0500 Subject: [PATCH 075/129] fix: rustmft --- packages/fermi/src/atoms/selector.rs | 1 + packages/fermi/src/atoms/selectorfamily.rs | 1 + packages/fermi/src/lib.rs | 5 ++--- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/fermi/src/atoms/selector.rs b/packages/fermi/src/atoms/selector.rs index e69de29bb..8b1378917 100644 --- a/packages/fermi/src/atoms/selector.rs +++ b/packages/fermi/src/atoms/selector.rs @@ -0,0 +1 @@ + diff --git a/packages/fermi/src/atoms/selectorfamily.rs b/packages/fermi/src/atoms/selectorfamily.rs index e69de29bb..8b1378917 100644 --- a/packages/fermi/src/atoms/selectorfamily.rs +++ b/packages/fermi/src/atoms/selectorfamily.rs @@ -0,0 +1 @@ + diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs index 023b5d286..8ac4f1ccf 100644 --- a/packages/fermi/src/lib.rs +++ b/packages/fermi/src/lib.rs @@ -4,14 +4,13 @@ pub mod prelude { pub use crate::*; } -mod root; mod callback; - +mod root; pub use atoms::*; +pub use callback::*; pub use hooks::*; pub use root::*; -pub use callback::*; mod atoms { mod atom; From f4686150d7e2d62b6607a6a761c72cfe1b451479 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:19:28 -0500 Subject: [PATCH 076/129] ci: make workflows only run on PRs --- .github/workflows/macos.yml | 9 ++++----- .github/workflows/main.yml | 9 ++++----- .github/workflows/windows.yml | 9 ++++----- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d1351712a..718922a57 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,7 +1,10 @@ name: macOS tests on: - push: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master paths: - packages/** - examples/** @@ -9,10 +12,6 @@ on: - .github/** - lib.rs - Cargo.toml - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: - - master jobs: test: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e1ec15c0..9f8e0bac3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,10 @@ name: Rust CI on: - push: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master paths: - packages/** - examples/** @@ -9,10 +12,6 @@ on: - .github/** - lib.rs - Cargo.toml - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: - - master jobs: check: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 518da37b3..75953807a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,7 +1,10 @@ name: windows on: - push: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master paths: - packages/** - examples/** @@ -9,10 +12,6 @@ on: - .github/** - lib.rs - Cargo.toml - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: - - master jobs: test: From 6bc9dd36f9ae1d698282147f1a6fe08a46f8f6e5 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:27:13 -0500 Subject: [PATCH 077/129] fix: coverage --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9f8e0bac3..8521dd176 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -101,8 +101,9 @@ jobs: - name: Generate code coverage run: | apt-get update &&\ + apt-get install build-essential &&\ apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ - cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v2 with: From 7aeed4ccae47d0e28c667dde9779de8e4babdf47 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:30:25 -0500 Subject: [PATCH 078/129] ci: disable coverage --- .github/workflows/main.yml | 39 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8521dd176..128dc5179 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,22 +89,23 @@ jobs: command: clippy args: -- -D warnings - coverage: - name: Coverage - runs-on: ubuntu-latest - container: - image: xd009642/tarpaulin:develop-nightly - options: --security-opt seccomp=unconfined - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Generate code coverage - run: | - apt-get update &&\ - apt-get install build-essential &&\ - apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ - cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml - - name: Upload to codecov.io - uses: codecov/codecov-action@v2 - with: - fail_ci_if_error: false + # Coverage is disabled until we can fix it + # coverage: + # name: Coverage + # runs-on: ubuntu-latest + # container: + # image: xd009642/tarpaulin:develop-nightly + # options: --security-opt seccomp=unconfined + # steps: + # - name: Checkout repository + # uses: actions/checkout@v2 + # - name: Generate code coverage + # run: | + # apt-get update &&\ + # apt-get install build-essential &&\ + # apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ + # cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml + # - name: Upload to codecov.io + # uses: codecov/codecov-action@v2 + # with: + # fail_ci_if_error: false From d897d2148ab9afcfb484f7d1d6e9bc19dc56a4cb Mon Sep 17 00:00:00 2001 From: Malcolm Rebughini <9681621+malcolmrebughini@users.noreply.github.com> Date: Thu, 17 Feb 2022 19:17:32 -0800 Subject: [PATCH 079/129] docs(reference/platforms/ssr): fixing example and commands --- docs/reference/src/platforms/ssr.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/reference/src/platforms/ssr.md b/docs/reference/src/platforms/ssr.md index aef223dfd..09e32ba43 100644 --- a/docs/reference/src/platforms/ssr.md +++ b/docs/reference/src/platforms/ssr.md @@ -2,11 +2,8 @@ The Dioxus VirtualDom can be rendered to a string by traversing the Element Tree. This is implemented in the `ssr` crate where your Dioxus app can be directly rendered to HTML to be served by a web server. - - ## Setup - If you just want to render `rsx!` or a VirtualDom to HTML, check out the API docs. It's pretty simple: ```rust @@ -19,8 +16,7 @@ println!("{}", dioxus::ssr::render_vdom(&vdom)); println!( "{}", dioxus::ssr::render_lazy(rsx! { h1 { "Hello, world!" } } ); ``` - -However, for this guide, we're going to show how to use Dioxus SSR with `Axum`. +However, for this guide, we're going to show how to use Dioxus SSR with `Axum`. Make sure you have Rust and Cargo installed, and then create a new project: @@ -29,15 +25,16 @@ $ cargo new --bin demo $ cd app ``` -Add Dioxus with the `desktop` feature: +Add Dioxus with the `ssr` feature: ```shell $ cargo add dioxus --features ssr ``` Next, add all the Axum dependencies. This will be different if you're using a different Web Framework + ``` -$ cargo add dioxus tokio --features full +$ cargo add tokio --features full $ cargo add axum ``` @@ -45,12 +42,11 @@ Your dependencies should look roughly like this: ```toml [dependencies] -axum = "0.4.3" +axum = "0.4.5" dioxus = { version = "*", features = ["ssr"] } tokio = { version = "1.15.0", features = ["full"] } ``` - Now, setup your Axum app to respond on an endpoint. ```rust @@ -63,7 +59,11 @@ async fn main() { println!("listening on http://{}", addr); axum::Server::bind(&addr) - .serve(Router::new().route("/", get(app_endpoint))) + .serve( + Router::new() + .route("/", get(app_endpoint)) + .into_make_service(), + ) .await .unwrap(); } @@ -88,14 +88,14 @@ async fn app_endpoint() -> Html { } let mut app = VirtualDom::new(app); let _ = app.rebuild(); - + Html(dioxus::ssr::render_vdom(&app)) } ``` And that's it! -> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it *must* remain on the thread it started. We are working on loosening this requirement. +> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it _must_ remain on the thread it started. We are working on loosening this requirement. ## Future Steps From b93487282de4faaac3db5b830215c4b1dbdeb16f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 20:54:30 -0500 Subject: [PATCH 080/129] feat: move files around --- Cargo.toml | 4 +- examples/rsx_autocomplete.rs | 27 -- packages/core-macro/Cargo.toml | 2 +- packages/rsx/src/{rsx => }/component.rs | 0 packages/rsx/src/{rsx => }/element.rs | 0 packages/rsx/src/htm.rs | 443 ------------------------ packages/rsx/src/lib.rs | 101 +++++- packages/rsx/src/{rsx => }/node.rs | 0 packages/rsx/src/pretty.rs | 1 + packages/rsx/src/rsx/mod.rs | 95 ----- 10 files changed, 101 insertions(+), 572 deletions(-) delete mode 100644 examples/rsx_autocomplete.rs rename packages/rsx/src/{rsx => }/component.rs (100%) rename packages/rsx/src/{rsx => }/element.rs (100%) delete mode 100644 packages/rsx/src/htm.rs rename packages/rsx/src/{rsx => }/node.rs (100%) create mode 100644 packages/rsx/src/pretty.rs delete mode 100644 packages/rsx/src/rsx/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 871810a76..63f1c938c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ dioxus-core = { path = "./packages/core", version = "^0.1.9" } dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } -dioxus-macro-inner = { path = "./packages/rsx", optional = true } +dioxus-rsx = { path = "./packages/rsx", optional = true } fermi = { path = "./packages/fermi", version = "^0.1.0", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } @@ -30,7 +30,7 @@ dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.0.0", o [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro", "dioxus-macro-inner"] +macro = ["dioxus-core-macro", "dioxus-rsx"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] diff --git a/examples/rsx_autocomplete.rs b/examples/rsx_autocomplete.rs deleted file mode 100644 index 99afe7521..000000000 --- a/examples/rsx_autocomplete.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! This example shows that autocomplete works in RSX - -use dioxus::prelude::*; - -fn main() { - dioxus::desktop::launch(app); -} - -fn app(cx: Scope) -> Element { - cx.render(rsx! { - div { - onclick: move |_| { - } - // class: "asd", - // style { - // media: "Ad", - // } - // div { - - // } - // { - // let t = String::new(); - // t. - // } - } - }) -} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index 94f8e6e87..ed5d34e53 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,7 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] -dioxus-macro-inner = { path = "../macro-inner" } +dioxus-rsx = { path = "../rsx" } proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } quote = "1.0" diff --git a/packages/rsx/src/rsx/component.rs b/packages/rsx/src/component.rs similarity index 100% rename from packages/rsx/src/rsx/component.rs rename to packages/rsx/src/component.rs diff --git a/packages/rsx/src/rsx/element.rs b/packages/rsx/src/element.rs similarity index 100% rename from packages/rsx/src/rsx/element.rs rename to packages/rsx/src/element.rs diff --git a/packages/rsx/src/htm.rs b/packages/rsx/src/htm.rs deleted file mode 100644 index ca53096f6..000000000 --- a/packages/rsx/src/htm.rs +++ /dev/null @@ -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 { - if input.peek(LitStr) { - return input.parse::()?.parse::(); - } - - // let __cx: Ident = s.parse()?; - // s.parse::()?; - // if elements are in an array, return a bumpalo::collections::Vec rather than a Node. - let kind = if input.peek(token::Bracket) { - let nodes_toks; - syn::bracketed!(nodes_toks in input); - let mut nodes: Vec> = vec![nodes_toks.parse()?]; - while nodes_toks.peek(Token![,]) { - nodes_toks.parse::()?; - nodes.push(nodes_toks.parse()?); - } - NodeOrList::List(NodeList(nodes)) - } else { - NodeOrList::Node(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>); - -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 { - 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, - children: MaybeExpr>, -} - -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 { - s.parse::()?; - let name = Ident::parse_any(s)?; - let mut attrs = vec![]; - let _children: Vec = vec![]; - - // keep looking for attributes - while !s.peek(Token![>]) { - // self-closing - if s.peek(Token![/]) { - s.parse::()?; - s.parse::]>()?; - return Ok(Self { - name, - attrs, - children: MaybeExpr::Literal(vec![]), - }); - } - attrs.push(s.parse()?); - } - s.parse::]>()?; - - // 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::()?; - s.parse::()?; - let close = Ident::parse_any(s)?; - if close != name { - return Err(Error::new_spanned( - close, - "closing element does not match opening", - )); - } - s.parse::]>()?; - - 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 { - let mut name = Ident::parse_any(s)?; - let name_str = name.to_string(); - s.parse::()?; - - // 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), - Event(ExprClosure), - // todo Bool(MaybeExpr) -} - -/// ======================================= -/// Parse just plain text -/// ======================================= -/// - [ ] Perform formatting automatically -/// -/// -struct TextNode(MaybeExpr); - -impl Parse for TextNode { - fn parse(s: ParseStream) -> Result { - 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 { - Literal(T), - Expr(Expr), -} - -impl Parse for MaybeExpr { - fn parse(s: ParseStream) -> Result { - 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> -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 { - inner: T, -} - -impl<'a, T> ToToksCtx { - fn new(inner: T) -> Self { - ToToksCtx { inner } - } - - fn recurse(&self, inner: U) -> ToToksCtx { - 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 { - syn::parse_str(input) - } - - #[test] - fn div() { - parse("bump,
").unwrap(); - } - - #[test] - fn nested() { - parse("bump,
\"text\"
").unwrap(); - } - - #[test] - fn complex() { - parse( - "bump, -
{contact_details}
- ", - ) - .unwrap(); - } -} diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index b9911dedc..e20d80dec 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -1,4 +1,97 @@ -pub mod ifmt; -pub mod inlineprops; -pub mod props; -pub mod rsx; +//! Parse the root tokens in the rsx!{} macro +//! ========================================= +//! +//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing. +//! This feature must support: +//! - [x] Optionally rendering if the `in XYZ` pattern is present +//! - [x] Fragments as top-level element (through ambiguous) +//! - [x] Components as top-level element (through ambiguous) +//! - [x] Tags as top-level elements (through ambiguous) +//! - [x] Good errors if parsing fails +//! +//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. + +mod component; +mod element; +mod node; + +pub mod pretty; + +// Re-export the namespaces into each other +pub use component::*; +pub use element::*; +pub use node::*; + +// imports +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + Ident, Result, Token, +}; + +pub struct CallBody { + pub custom_context: Option, + pub roots: Vec, +} + +impl Parse for CallBody { + fn parse(input: ParseStream) -> Result { + let custom_context = if input.peek(Ident) && input.peek2(Token![,]) { + let name = input.parse::()?; + input.parse::()?; + + Some(name) + } else { + None + }; + + let mut roots = Vec::new(); + + while !input.is_empty() { + let node = input.parse::()?; + + if input.peek(Token![,]) { + let _ = input.parse::(); + } + + roots.push(node); + } + + Ok(Self { + custom_context, + roots, + }) + } +} + +/// Serialize the same way, regardless of flavor +impl ToTokens for CallBody { + fn to_tokens(&self, out_tokens: &mut TokenStream2) { + let inner = if self.roots.len() == 1 { + let inner = &self.roots[0]; + quote! { #inner } + } else { + let childs = &self.roots; + quote! { __cx.fragment_root([ #(#childs),* ]) } + }; + + match &self.custom_context { + // The `in cx` pattern allows directly rendering + Some(ident) => out_tokens.append_all(quote! { + #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + #inner + })) + }), + + // Otherwise we just build the LazyNode wrapper + None => out_tokens.append_all(quote! { + LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + #inner + }) + }), + }; + } +} diff --git a/packages/rsx/src/rsx/node.rs b/packages/rsx/src/node.rs similarity index 100% rename from packages/rsx/src/rsx/node.rs rename to packages/rsx/src/node.rs diff --git a/packages/rsx/src/pretty.rs b/packages/rsx/src/pretty.rs new file mode 100644 index 000000000..26ad54006 --- /dev/null +++ b/packages/rsx/src/pretty.rs @@ -0,0 +1 @@ +//! pretty printer for rsx! diff --git a/packages/rsx/src/rsx/mod.rs b/packages/rsx/src/rsx/mod.rs deleted file mode 100644 index 87b324dfc..000000000 --- a/packages/rsx/src/rsx/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Parse the root tokens in the rsx!{} macro -//! ========================================= -//! -//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing. -//! This feature must support: -//! - [x] Optionally rendering if the `in XYZ` pattern is present -//! - [x] Fragments as top-level element (through ambiguous) -//! - [x] Components as top-level element (through ambiguous) -//! - [x] Tags as top-level elements (through ambiguous) -//! - [x] Good errors if parsing fails -//! -//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. - -mod component; -mod element; -mod node; - -// Re-export the namespaces into each other -pub use component::*; -pub use element::*; -pub use node::*; - -// imports -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseStream}, - Ident, Result, Token, -}; - -pub struct CallBody { - pub custom_context: Option, - pub roots: Vec, -} - -impl Parse for CallBody { - fn parse(input: ParseStream) -> Result { - let custom_context = if input.peek(Ident) && input.peek2(Token![,]) { - let name = input.parse::()?; - input.parse::()?; - - Some(name) - } else { - None - }; - - let mut roots = Vec::new(); - - while !input.is_empty() { - let node = input.parse::()?; - - if input.peek(Token![,]) { - let _ = input.parse::(); - } - - roots.push(node); - } - - Ok(Self { - custom_context, - roots, - }) - } -} - -/// Serialize the same way, regardless of flavor -impl ToTokens for CallBody { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let inner = if self.roots.len() == 1 { - let inner = &self.roots[0]; - quote! { #inner } - } else { - let childs = &self.roots; - quote! { __cx.fragment_root([ #(#childs),* ]) } - }; - - match &self.custom_context { - // The `in cx` pattern allows directly rendering - Some(ident) => out_tokens.append_all(quote! { - #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner - })) - }), - - // Otherwise we just build the LazyNode wrapper - None => out_tokens.append_all(quote! { - LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner - }) - }), - }; - } -} From 185902f935b545b6af70fcff53f130031d98fa50 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:14:17 -0500 Subject: [PATCH 081/129] fix: macro compiles --- packages/core-macro/src/lib.rs | 37 +++++----------------------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index b2e4ffbb1..96ea0ce87 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -1,8 +1,11 @@ -use dioxus_macro_inner::*; use proc_macro::TokenStream; use quote::ToTokens; use syn::parse_macro_input; +mod ifmt; +mod inlineprops; +mod props; + #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { use ifmt::*; @@ -175,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::(s) { + match syn::parse::(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

`, From 9438cc14bc8c2dd8781908a598684e723d45ec45 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:17:33 -0500 Subject: [PATCH 082/129] fix: make listeners up to date --- packages/rsx/src/element.rs | 84 ++++++++++++++++++------------------- src/lib.rs | 2 +- 2 files changed, 42 insertions(+), 44 deletions(-) diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index c612e9f1b..2c295d9a0 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, - Expr, ExprClosure, Ident, LitStr, Result, Token, + Expr, Ident, LitStr, Result, Token, }; // ======================================= @@ -81,24 +81,31 @@ impl Parse for Element { content.parse::()?; if name_str.starts_with("on") { - if content.fork().parse::().is_ok() { - // - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventClosure { - name, - closure: content.parse()?, - }, - }); - } else { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: content.parse()?, - }, - }); - } + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventTokens { + name, + tokens: content.parse()?, + }, + }); + // if content.fork().parse::().is_ok() { + // // + // attributes.push(ElementAttrNamed { + // el_name: el_name.clone(), + // attr: ElementAttr::EventClosure { + // name, + // closure: content.parse()?, + // }, + // }); + // } else { + // attributes.push(ElementAttrNamed { + // el_name: el_name.clone(), + // attr: ElementAttr::EventTokens { + // name, + // tokens: content.parse()?, + // }, + // }); + // } } else { match name_str.as_str() { "key" => { @@ -203,28 +210,20 @@ impl ToTokens for Element { let name = &self.name; let children = &self.children; - // let listeners = &self.listeners; - let key = match &self.key { Some(ty) => quote! { Some(format_args_f!(#ty)) }, None => quote! { None }, }; - let listeners = self.attributes.iter().filter(|f| { - if let ElementAttr::EventTokens { .. } = f.attr { - true - } else { - false - } - }); + let listeners = self + .attributes + .iter() + .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); - let attr = self.attributes.iter().filter(|f| { - if let ElementAttr::EventTokens { .. } = f.attr { - false - } else { - true - } - }); + let attr = self + .attributes + .iter() + .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); tokens.append_all(quote! { __cx.element( @@ -251,9 +250,8 @@ pub enum ElementAttr { /// "attribute": true, CustomAttrExpression { name: LitStr, value: Expr }, - /// onclick: move |_| {} - EventClosure { name: Ident, closure: ExprClosure }, - + // /// onclick: move |_| {} + // EventClosure { name: Ident, closure: ExprClosure }, /// onclick: {} EventTokens { name: Ident, tokens: Expr }, } @@ -288,11 +286,11 @@ impl ToTokens for ElementAttrNamed { __cx.attr( #name, format_args_f!(#value), None, false ) } } - ElementAttr::EventClosure { name, closure } => { - quote! { - dioxus_elements::on::#name(__cx, #closure) - } - } + // ElementAttr::EventClosure { name, closure } => { + // quote! { + // dioxus_elements::on::#name(__cx, #closure) + // } + // } ElementAttr::EventTokens { name, tokens } => { quote! { dioxus_elements::on::#name(__cx, #tokens) diff --git a/src/lib.rs b/src/lib.rs index 2db210bee..488b1ee9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -353,7 +353,7 @@ pub mod events { pub mod prelude { pub use dioxus_core::prelude::*; - pub use dioxus_core_macro::{format_args_f, inline_props, rsx, Props, Routable}; + pub use dioxus_core_macro::{format_args_f, inline_props, rsx, Props}; pub use dioxus_elements::{GlobalAttributes, SvgAttributes}; pub use dioxus_hooks::*; pub use dioxus_html as dioxus_elements; From 5d56326f74e07dbc70141100bb71826ed7033152 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:18:52 -0500 Subject: [PATCH 083/129] feat: collapse rsx --- packages/rsx/src/element.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 2c295d9a0..fa8fc9f51 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -88,24 +88,6 @@ impl Parse for Element { tokens: content.parse()?, }, }); - // if content.fork().parse::().is_ok() { - // // - // attributes.push(ElementAttrNamed { - // el_name: el_name.clone(), - // attr: ElementAttr::EventClosure { - // name, - // closure: content.parse()?, - // }, - // }); - // } else { - // attributes.push(ElementAttrNamed { - // el_name: el_name.clone(), - // attr: ElementAttr::EventTokens { - // name, - // tokens: content.parse()?, - // }, - // }); - // } } else { match name_str.as_str() { "key" => { @@ -199,7 +181,6 @@ impl Parse for Element { name: el_name, attributes, children, - // listeners, _is_static: false, }) } From 7ce34ad97ed926c4fa313d4a881ceb78a5800485 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:22:35 -0500 Subject: [PATCH 084/129] fix: enable desktop devtol --- packages/desktop/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 371adced5..197735ddc 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -173,6 +173,10 @@ pub fn launch_with_props( webview = webview.with_custom_protocol(name, handler) } + if cfg!(debug_assertions) { + webview = webview.with_dev_tool(true); + } + desktop.webviews.insert(window_id, webview.build().unwrap()); } From 237f036a801244f5ae3dc664e58f244c2cf1d561 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:26:47 -0500 Subject: [PATCH 085/129] wip: add meta to rsx --- packages/rsx/src/node.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index d5d701767..eeee421b0 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseStream}, - token, Expr, LitStr, Result, Token, + token, Attribute, Expr, LitStr, Result, Token, }; /* @@ -20,6 +20,7 @@ pub enum BodyNode { Component(Component), Text(LitStr), RawExpr(Expr), + Meta(Attribute), } impl Parse for BodyNode { @@ -79,6 +80,7 @@ impl ToTokens for BodyNode { BodyNode::RawExpr(exp) => tokens.append_all(quote! { __cx.fragment_from_iter(#exp) }), + BodyNode::Meta(_) => {} } } } From 5a6d1c2a310aff3ec996f0f02d2ef09d5e3a54a3 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:30:09 -0500 Subject: [PATCH 086/129] fix: compile for rust 1.57 --- packages/desktop/src/events.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 7be4273e9..44126dd63 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -26,7 +26,7 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option { match serde_json::from_str(payload) { Ok(message) => Some(message), Err(e) => { - log::error!("could not parse IPC message, error: {e}"); + log::error!("could not parse IPC message, error: {}", e); None } } From c1d692dd974d9122120e20b69e56b1c36b9d1e26 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:33:48 -0500 Subject: [PATCH 087/129] fix: dioxus must specify rust versionand edition --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bd15590ac..2ba3b55fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,13 +2,14 @@ name = "dioxus" version = "0.1.8" authors = ["Jonathan Kelley"] -edition = "2018" +edition = "2021" description = "Core functionality 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://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] +rust-version = "1.56.0" [dependencies] dioxus-core = { path = "./packages/core", version = "^0.1.9" } From 28716248c5de9c9ebc9c8dbc1dd32ec7be9eb99b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Feb 2022 17:34:44 -0500 Subject: [PATCH 088/129] fix: ping window after virtualdom is ready --- examples/heavy_compute.rs | 28 ++++++++++++++++++++++ packages/desktop/src/controller.rs | 3 +++ packages/desktop/src/lib.rs | 1 - packages/desktop/src/user_window_events.rs | 1 + 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 examples/heavy_compute.rs diff --git a/examples/heavy_compute.rs b/examples/heavy_compute.rs new file mode 100644 index 000000000..6e10a2f1f --- /dev/null +++ b/examples/heavy_compute.rs @@ -0,0 +1,28 @@ +//! This example shows that you can place heavy work on the main thread, and then +//! +//! You *should* be using `tokio::spawn_blocking` instead. +//! +//! Your app runs in an async runtime (Tokio), so you should avoid blocking +//! the rendering of the VirtualDom. +//! +//! + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + // This is discouraged + std::thread::sleep(std::time::Duration::from_millis(2_000)); + + // This is suggested + tokio::task::spawn_blocking(move || { + std::thread::sleep(std::time::Duration::from_millis(2_000)); + }); + + cx.render(rsx! { + div { "Hello, world!" } + }) +} diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 8497a42e9..8715c74f0 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -57,6 +57,9 @@ impl DesktopController { .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); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 197735ddc..934c82f1a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -143,7 +143,6 @@ pub fn launch_with_props( .send_event(user_window_events::UserWindowEvent::Update); } "browser_open" => { - println!("browser_open"); let data = message.params(); log::trace!("Open browser: {:?}", data); if let Some(temp) = data.as_object() { diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs index aab14561b..bde215d48 100644 --- a/packages/desktop/src/user_window_events.rs +++ b/packages/desktop/src/user_window_events.rs @@ -3,6 +3,7 @@ use wry::application::window::Fullscreen as WryFullscreen; use crate::controller::DesktopController; +#[derive(Debug)] pub(crate) enum UserWindowEvent { Update, From 45473ece8c2fb9ac076b4e881f071d0efd7b9a1e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Feb 2022 18:00:03 -0500 Subject: [PATCH 089/129] feat: add some helpers to use_ref --- examples/use_ref.rs | 21 +++++++++++++++++++++ packages/hooks/src/useref.rs | 11 +++++++++++ 2 files changed, 32 insertions(+) create mode 100644 examples/use_ref.rs diff --git a/examples/use_ref.rs b/examples/use_ref.rs new file mode 100644 index 000000000..358b9c266 --- /dev/null +++ b/examples/use_ref.rs @@ -0,0 +1,21 @@ +use std::collections::HashMap; + +use dioxus::prelude::*; +fn main() {} + +fn app(cx: Scope) -> Element { + let val = use_ref(&cx, || HashMap::::new()); + + // Pull the value out locally + let p = val.read(); + + // to get an &HashMap we have to "reborrow" through the RefCell + // Be careful: passing this into children might cause a double borrow of the RefCell and a panic + let g = &*p; + + cx.render(rsx! { + div { + "hi" + } + }) +} diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index 3ddf86e8c..a0b9189d0 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -42,6 +42,17 @@ impl UseRef { self.value.borrow_mut() } + /// Take a reference to the inner value termporarily and produce a new value + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + f(&*self.read()) + } + + /// Take a reference to the inner value termporarily and produce a new value, + /// modifying the original in place. + pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { + f(&mut *self.write()) + } + pub fn needs_update(&self) { (self.update_callback)(); } From e7022dfccf64b378ac08a93c02758690934821fc Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Feb 2022 21:51:26 -0500 Subject: [PATCH 090/129] docs: more use_ref docs --- examples/use_ref.rs | 14 +- packages/hooks/src/useref.rs | 255 ++++++++++++++++++++++++++++------- 2 files changed, 216 insertions(+), 53 deletions(-) diff --git a/examples/use_ref.rs b/examples/use_ref.rs index 358b9c266..d0b85a66f 100644 --- a/examples/use_ref.rs +++ b/examples/use_ref.rs @@ -6,12 +6,16 @@ fn main() {} fn app(cx: Scope) -> Element { let val = use_ref(&cx, || HashMap::::new()); - // Pull the value out locally - let p = val.read(); + { + // Pull the value out locally + let p = val.read(); - // to get an &HashMap we have to "reborrow" through the RefCell - // Be careful: passing this into children might cause a double borrow of the RefCell and a panic - let g = &*p; + // to get an &HashMap we have to "reborrow" through the RefCell + // Be careful: passing this into children might cause a double borrow of the RefCell and a panic + let g = &*p; + + dbg!(g); + } cx.render(rsx! { div { diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index a0b9189d0..c2537e079 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -1,68 +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 { +/// `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::::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 = 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::::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::::new()); +/// let val2 = use_ref(|| HashMap::::new()); +/// let val3 = use_ref(|| HashMap::::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 { 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 { - update_callback: Rc, + update: Rc, value: Rc>, } -impl UseRef { - 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() - } - - /// Take a reference to the inner value termporarily and produce a new value - pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { - f(&*self.read()) - } - - /// Take a reference to the inner value termporarily and produce a new value, - /// modifying the original in place. - pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { - f(&mut *self.write()) - } - - pub fn needs_update(&self) { - (self.update_callback)(); - } -} - impl Clone for UseRef { fn clone(&self) -> Self { Self { - update_callback: self.update_callback.clone(), + update: self.update.clone(), value: self.value.clone(), } } } + +impl UseRef { + /// 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::::new()); + /// + /// + /// // use reborrowing + /// let inner = &*val.read(); + /// + /// // or, be safer and use `with` + /// val.with(|i| println!("{:?}", i)); + /// ``` + pub fn with(&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::::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(&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)(); + } +} From 98076c52e88be4a05ffb854e0dfac9cae8f52c02 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Feb 2022 21:53:00 -0500 Subject: [PATCH 091/129] docs: remove use_ref example --- examples/use_ref.rs | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 examples/use_ref.rs diff --git a/examples/use_ref.rs b/examples/use_ref.rs deleted file mode 100644 index d0b85a66f..000000000 --- a/examples/use_ref.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::collections::HashMap; - -use dioxus::prelude::*; -fn main() {} - -fn app(cx: Scope) -> Element { - let val = use_ref(&cx, || HashMap::::new()); - - { - // Pull the value out locally - let p = val.read(); - - // to get an &HashMap we have to "reborrow" through the RefCell - // Be careful: passing this into children might cause a double borrow of the RefCell and a panic - let g = &*p; - - dbg!(g); - } - - cx.render(rsx! { - div { - "hi" - } - }) -} From 5954bba975691b9b080f73637b95e56e46a0d95f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Feb 2022 22:36:55 -0500 Subject: [PATCH 092/129] wip: disable reload in production --- packages/desktop/src/index.html | 24 ++++++++++++------------ packages/desktop/src/lib.rs | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index e244b6faa..292a1ae6a 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -1,15 +1,15 @@ - - Dioxus app - - - -

- - + + Dioxus app + + + +
+ + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 197735ddc..4d204a378 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -174,7 +174,25 @@ pub fn launch_with_props( } 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()); From 62df5f7097f5968455a0b25eebb0540d958d2fd3 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 20 Feb 2022 17:37:46 -0500 Subject: [PATCH 093/129] docs: document the entire core crate --- packages/core/src/diff.rs | 10 +--- packages/core/src/events.rs | 25 +++++++-- packages/core/src/lazynodes.rs | 66 ++++++++++++---------- packages/core/src/lib.rs | 4 ++ packages/core/src/mutations.rs | 94 +++++++++++++++++++++++++++---- packages/core/src/nodes.rs | 95 +++++++++++++++++++++++++++----- packages/core/src/properties.rs | 6 ++ packages/core/src/scopes.rs | 6 +- packages/core/src/util.rs | 5 ++ packages/core/src/virtual_dom.rs | 12 +++- packages/rsx/src/lib.rs | 4 +- 11 files changed, 253 insertions(+), 74 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index d7189a710..80635bbd2 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -257,11 +257,10 @@ impl<'b> DiffState<'b> { vcomponent.scope.set(Some(new_idx)); log::trace!( - "created component \"{}\", id: {:?} parent {:?} orig: {:?}", + "created component \"{}\", id: {:?} parent {:?}", vcomponent.fn_name, new_idx, parent_idx, - vcomponent.originator ); // if vcomponent.can_memoize { @@ -957,13 +956,6 @@ impl<'b> DiffState<'b> { let props = scope.props.take().unwrap(); c.props.borrow_mut().replace(props); self.scopes.try_remove(scope_id).unwrap(); - - // // we can only remove components if they are actively being diffed - // if self.scope_stack.contains(&c.originator) { - // log::trace!("Removing component {:?}", old); - - // self.scopes.try_remove(scope_id).unwrap(); - // } } self.leave_scope(); } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 8386cedd8..e92ead8cc 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -119,24 +119,39 @@ pub enum EventPriority { Low = 0, } +/// The internal Dioxus type that carries any event data to the relevant handler. +/// +/// +/// +/// +/// pub struct AnyEvent { pub(crate) bubble_state: Rc, pub(crate) data: Arc, } impl AnyEvent { + /// Convert this AnyEvent into a specific UiEvent with EventData. + /// + /// ```rust, ignore + /// let evt: FormEvent = evvt.downcast().unwrap(); + /// ``` pub fn downcast(self) -> Option> { let AnyEvent { data, bubble_state } = self; - if let Ok(data) = data.downcast::() { - Some(UiEvent { bubble_state, data }) - } else { - None - } + data.downcast::().ok().map(|data| UiEvent { bubble_state, data }) } } +/// A UiEvent is a type that wraps various EventData. +/// +/// You should prefer to use the name of the event directly, rather than +/// the UiEvent generic type. +/// +/// For the HTML crate, this would include [`MouseEvent`], [`FormEvent`] etc. pub struct UiEvent { + /// The internal data of the event + /// This is wrapped in an Arc so that it can be sent across threads pub data: Arc, #[allow(unused)] diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 1340c916c..5fac7b194 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -35,14 +35,32 @@ enum StackNodeStorage<'a, 'b> { } impl<'a, 'b> LazyNodes<'a, 'b> { - pub fn new_some(_val: F) -> Self - where - F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b, - { - Self::new(_val) + /// Create a new LazyNodes closure, optimistically placing it onto the stack. + /// + /// If the closure cannot fit into the stack allocation (16 bytes), then it + /// is placed on the heap. Most closures will fit into the stack, and is + /// the most optimal way to use the creation function. + pub fn new(val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self { + // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch + let mut slot = Some(val); + + let val = move |fac: Option>| { + let inner = slot.take().unwrap(); + fac.map(inner) + }; + + // miri does not know how to work with mucking directly into bytes + // just use a heap allocated type when miri is running + if cfg!(miri) { + Self { + inner: StackNodeStorage::Heap(Box::new(val)), + } + } else { + unsafe { LazyNodes::new_inner(val) } + } } - /// force this call onto the stack + /// Create a new LazyNodes closure, but force it onto the heap. pub fn new_boxed(_val: F) -> Self where F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b, @@ -60,25 +78,6 @@ impl<'a, 'b> LazyNodes<'a, 'b> { } } - pub fn new(_val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self { - // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch - let mut slot = Some(_val); - - let val = move |fac: Option>| { - let inner = slot.take().unwrap(); - fac.map(inner) - }; - - // miri does not know how to work with mucking directly into bytes - if cfg!(miri) { - Self { - inner: StackNodeStorage::Heap(Box::new(val)), - } - } else { - unsafe { LazyNodes::new_inner(val) } - } - } - unsafe fn new_inner(val: F) -> Self where F: FnMut(Option>) -> Option> + 'b, @@ -153,6 +152,15 @@ impl<'a, 'b> LazyNodes<'a, 'b> { } } + /// Call the closure with the given factory to produce real VNodes. + /// + /// ```rust, ignore + /// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None)); + /// + /// let fac = NodeFactory::new(&cx); + /// + /// let node = f.call(cac); + /// ``` pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> { match self.inner { StackNodeStorage::Heap(mut lazy) => lazy(Some(f)).unwrap(), @@ -245,9 +253,7 @@ mod tests { #[test] fn it_works() { fn app(cx: Scope<()>) -> Element { - cx.render(LazyNodes::new_some(|f| { - f.text(format_args!("hello world!")) - })) + cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) } let mut dom = VirtualDom::new(app); @@ -280,7 +286,7 @@ mod tests { .map(|i| { let val = cx.props.inner.clone(); - LazyNodes::new_some(move |f| { + LazyNodes::new(move |f| { log::debug!("hell closure"); let inner = DropInner { id: i }; f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) @@ -288,7 +294,7 @@ mod tests { }) .collect::>(); - LazyNodes::new_some(|f| { + LazyNodes::new(|f| { log::debug!("main closure"); f.fragment_from_iter(it) }) diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index e81ddca87..07fb5ce79 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,5 +1,6 @@ #![allow(non_snake_case)] #![doc = include_str!("../README.md")] +#![deny(missing_docs)] pub(crate) mod diff; pub(crate) mod events; @@ -76,6 +77,9 @@ pub use crate::innerlude::{ VElement, VFragment, VNode, VPlaceholder, VText, VirtualDom, }; +/// The purpose of this module is to alleviate imports of many common types +/// +/// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ fc_to_builder, Attributes, Component, DioxusElement, Element, EventHandler, Fragment, diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index bd94d0b71..324b83cb6 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -16,8 +16,13 @@ use std::{any::Any, fmt::Debug}; /// /// Mutations are the only link between the RealDOM and the VirtualDOM. pub struct Mutations<'a> { + /// The list of edits that need to be applied for the RealDOM to match the VirtualDOM. pub edits: Vec>, + + /// The list of Scopes that were diffed, created, and removed during the Diff process. pub dirty_scopes: FxHashSet, + + /// The list of nodes to connect to the RealDOM. pub refs: Vec>, } @@ -39,73 +44,142 @@ impl Debug for Mutations<'_> { serde(tag = "type") )] pub enum DomEdit<'bump> { + /// Push the given root node onto our stack. PushRoot { + /// The ID of the root node to push. root: u64, }, + /// Pop the topmost node from our stack and append them to the node + /// at the top of the stack. AppendChildren { + /// How many nodes should be popped from the stack. + /// The node remaining on the stack will be the target for the append. many: u32, }, - // // save a possibly-fragment node as a template - // SaveAsTemplate { - // many: u32, - // }, - - // "Root" refers to the item directly - // it's a waste of an instruction to push the root directly + /// Replace a given (single) node with a handful of nodes currently on the stack. ReplaceWith { + /// The ID of the node to be replaced. root: u64, + + /// How many nodes should be popped from the stack to replace the target node. m: u32, }, + + /// Insert a number of nodes after a given node. InsertAfter { + /// The ID of the node to insert after. root: u64, + + /// How many nodes should be popped from the stack to insert after the target node. n: u32, }, + + /// Insert a number of nodes before a given node. InsertBefore { + /// The ID of the node to insert before. root: u64, + + /// How many nodes should be popped from the stack to insert before the target node. n: u32, }, + + /// Remove a particular node from the DOM Remove { + /// The ID of the node to remove. root: u64, }, + + /// Create a new purely-text node CreateTextNode { + /// The ID the new node should have. + root: u64, + + /// The textcontent of the node text: &'bump str, - root: u64, }, + + /// Create a new purely-element node CreateElement { - tag: &'bump str, + /// The ID the new node should have. root: u64, + + /// The tagname of the node + tag: &'bump str, }, + + /// Create a new purely-comment node with a given namespace CreateElementNs { - tag: &'bump str, + /// The ID the new node should have. root: u64, + + /// The namespace of the node + tag: &'bump str, + + /// The namespace of the node (like `SVG`) ns: &'static str, }, + + /// Create a new placeholder node. + /// In most implementations, this will either be a hidden div or a comment node. CreatePlaceholder { + /// The ID the new node should have. root: u64, }, + + /// Create a new Event Listener. NewEventListener { + /// The name of the event to listen for. event_name: &'static str, + + /// The ID of the node to attach the listener to. scope: ScopeId, + + /// The ID of the node to attach the listener to. root: u64, }, + + /// Remove an existing Event Listener. RemoveEventListener { + /// The ID of the node to remove. root: u64, + + /// The name of the event to remove. event: &'static str, }, + + /// Set the textcontent of a node. SetText { + /// The ID of the node to set the textcontent of. root: u64, + + /// The textcontent of the node text: &'bump str, }, + + /// Set the value of a node's attribute. SetAttribute { + /// The ID of the node to set the attribute of. root: u64, + + /// The name of the attribute to set. field: &'static str, + + /// The value of the attribute. value: &'bump str, + + /// The (optional) namespace of the attribute. + /// For instance, "style" is in the "style" namespace. ns: Option<&'bump str>, }, + + /// Remove an attribute from a node. RemoveAttribute { + /// The ID of the node to remove. root: u64, + + /// The name of the attribute to remove. name: &'static str, }, } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 7a36f58ce..094af2281 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -148,7 +148,7 @@ impl<'src> VNode<'src> { } // Create an "owned" version of the vnode. - pub fn decouple(&self) -> VNode<'src> { + pub(crate) fn decouple(&self) -> VNode<'src> { match *self { VNode::Text(t) => VNode::Text(t), VNode::Element(e) => VNode::Element(e), @@ -181,7 +181,6 @@ impl Debug for VNode<'_> { .field("fnptr", &comp.user_fc) .field("key", &comp.key) .field("scope", &comp.scope) - .field("originator", &comp.originator) .finish(), } } @@ -200,6 +199,7 @@ impl std::fmt::Display for ElementId { } impl ElementId { + /// Convertt the ElementId to a `u64`. pub fn as_u64(self) -> u64 { self.0 as u64 } @@ -211,18 +211,26 @@ fn empty_cell() -> Cell> { /// A placeholder node only generated when Fragments don't have any children. pub struct VPlaceholder { + /// The [`ElementId`] of the placeholder. pub id: Cell>, } /// A bump-allocated string slice and metadata. pub struct VText<'src> { - pub text: &'src str, + /// The [`ElementId`] of the VText. pub id: Cell>, + + /// The text of the VText. + pub text: &'src str, + + /// An indiciation if this VText can be ignored during diffing + /// Is usually only when there are no strings to be formatted (so the text is &'static str) pub is_static: bool, } /// A list of VNodes with no single root. pub struct VFragment<'src> { + /// The key of the fragment to be used during keyed diffing. pub key: Option<&'src str>, /// Fragments can never have zero children. Enforced by NodeFactory. @@ -233,13 +241,34 @@ pub struct VFragment<'src> { /// An element like a "div" with children, listeners, and attributes. pub struct VElement<'a> { - pub tag: &'static str, - pub namespace: Option<&'static str>, - pub key: Option<&'a str>, + /// The [`ElementId`] of the VText. pub id: Cell>, + + /// The key of the element to be used during keyed diffing. + pub key: Option<&'a str>, + + /// The tag name of the element. + /// + /// IE "div" + pub tag: &'static str, + + /// The namespace of the VElement + /// + /// IE "svg" + pub namespace: Option<&'static str>, + + /// The parent of the Element (if any). + /// + /// Used when bubbling events pub parent: Cell>, + + /// The Listeners of the VElement. pub listeners: &'a [Listener<'a>], + + /// The attributes of the VElement. pub attributes: &'a [Attribute<'a>], + + /// The children of the VElement. pub children: &'a [VNode<'a>], } @@ -275,12 +304,19 @@ impl Debug for VElement<'_> { /// }; /// ``` pub trait DioxusElement { + /// The tag name of the element. const TAG_NAME: &'static str; + + /// The namespace of the element. const NAME_SPACE: Option<&'static str>; + + /// The tag name of the element. #[inline] fn tag_name(&self) -> &'static str { Self::TAG_NAME } + + /// The namespace of the element. #[inline] fn namespace(&self) -> Option<&'static str> { Self::NAME_SPACE @@ -291,16 +327,25 @@ pub trait DioxusElement { /// `href="https://example.com"`. #[derive(Clone, Debug)] pub struct Attribute<'a> { + /// The name of the attribute. pub name: &'static str, + /// The value of the attribute. pub value: &'a str, + /// An indication if this attribute can be ignored during diffing + /// + /// Usually only when there are no strings to be formatted (so the value is &'static str) pub is_static: bool, + /// An indication of we should always try and set the attribute. + /// Used in controlled components to ensure changes are propagated. pub is_volatile: bool, - // Doesn't exist in the html spec. - // Used in Dioxus to denote "style" tags. + /// The namespace of the attribute. + /// + /// Doesn't exist in the html spec. + /// Used in Dioxus to denote "style" tags and other attribute groups. pub namespace: Option<&'static str>, } @@ -353,6 +398,8 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// /// ``` pub struct EventHandler<'bump, T = ()> { + /// The (optional) callback that the user specified + /// Uses a `RefCell` to allow for interior mutability, and FnMut closures. pub callback: RefCell>>, } @@ -381,12 +428,23 @@ impl EventHandler<'_, T> { /// Virtual Components for custom user-defined components /// Only supports the functional syntax pub struct VComponent<'src> { + /// The key of the component to be used during keyed diffing. pub key: Option<&'src str>, - pub originator: ScopeId, + + /// The ID of the component. + /// Will not be assigned until after the component has been initialized. pub scope: Cell>, + + /// An indication if the component is static (can be memozied) pub can_memoize: bool, + + /// The function pointer to the component's render function. pub user_fc: ComponentPtr, + + /// The actual name of the component. pub fn_name: &'static str, + + /// The props of the component. pub props: RefCell>>, } @@ -434,6 +492,7 @@ pub struct NodeFactory<'a> { } impl<'a> NodeFactory<'a> { + /// Create a new [`NodeFactory`] from a [`Scope`] or [`ScopeState`] pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> { NodeFactory { scope, @@ -441,6 +500,7 @@ impl<'a> NodeFactory<'a> { } } + /// Get the custom alloactor for this component #[inline] pub fn bump(&self) -> &'a bumpalo::Bump { self.bump @@ -482,6 +542,7 @@ impl<'a> NodeFactory<'a> { })) } + /// Create a new [`VNode::VElement`] pub fn element( &self, el: impl DioxusElement, @@ -500,6 +561,9 @@ impl<'a> NodeFactory<'a> { ) } + /// Create a new [`VNode::VElement`] without the trait bound + /// + /// IE pass in "div" instead of `div` pub fn raw_element( &self, tag_name: &'static str, @@ -529,6 +593,7 @@ impl<'a> NodeFactory<'a> { })) } + /// Create a new [`Attribute`] pub fn attr( &self, name: &'static str, @@ -546,6 +611,7 @@ impl<'a> NodeFactory<'a> { } } + /// Create a new [`VNode::VComponent`] pub fn component

( &self, component: fn(Scope<'a, P>) -> Element, @@ -561,11 +627,8 @@ impl<'a> NodeFactory<'a> { scope: Default::default(), can_memoize: P::IS_STATIC, user_fc: component as ComponentPtr, - originator: self.scope.scope_id(), fn_name, props: RefCell::new(Some(Box::new(VComponentProps { - // local_props: RefCell::new(Some(props)), - // heap_props: RefCell::new(None), props, memo: P::memoize, // smuggle the memoization function across borders @@ -586,6 +649,7 @@ impl<'a> NodeFactory<'a> { VNode::Component(vcomp) } + /// Create a new [`Listener`] pub fn listener(self, event: &'static str, callback: InternalHandler<'a>) -> Listener<'a> { Listener { event, @@ -594,6 +658,7 @@ impl<'a> NodeFactory<'a> { } } + /// Create a new [`VNode::VFragment`] from a root of the rsx! call pub fn fragment_root<'b, 'c>( self, node_iter: impl IntoIterator + 'c> + 'b, @@ -614,6 +679,7 @@ impl<'a> NodeFactory<'a> { } } + /// Create a new [`VNode::VFragment`] from any iterator pub fn fragment_from_iter<'b, 'c>( self, node_iter: impl IntoIterator + 'c> + 'b, @@ -653,8 +719,7 @@ impl<'a> NodeFactory<'a> { } } - // this isn't quite feasible yet - // I think we need some form of interior mutability or state on nodefactory that stores which subtree was created + /// Create a new [`VNode`] from any iterator of children pub fn create_children( self, node_iter: impl IntoIterator>, @@ -679,6 +744,7 @@ impl<'a> NodeFactory<'a> { } } + /// Create a new [`EventHandler`] from an [`FnMut`] pub fn event_handler(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> { let handler: &mut dyn FnMut(T) = self.bump.alloc(f); let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; @@ -709,6 +775,7 @@ impl Debug for NodeFactory<'_> { /// As such, all node creation must go through the factory, which is only available in the component context. /// These strict requirements make it possible to manage lifetimes and state. pub trait IntoVNode<'a> { + /// Convert this into a [`VNode`], using the [`NodeFactory`] as a source of allocation fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a>; } diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index f42bc36ba..e81fc52a2 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -128,8 +128,14 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { /// } /// ``` pub trait Properties: Sized { + /// The type of the builder for this component. + /// Used to create "in-progress" versions of the props. type Builder; + + /// An indication if these props are can be memoized automatically. const IS_STATIC: bool; + + /// Create a builder for this component. fn builder() -> Self::Builder; /// Memoization can only happen if the props are valid for the 'static lifetime diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 00c3024bf..bc592fc4a 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -396,7 +396,10 @@ impl ScopeArena { /// } /// ``` pub struct Scope<'a, P = ()> { + /// The internal ScopeState for this component pub scope: &'a ScopeState, + + /// The props for this component pub props: &'a P, } @@ -747,7 +750,8 @@ impl ScopeState { self.push_future(fut); } - // todo: attach some state to the future to know if we should poll it + /// Informs the scheduler that this task is no longer needed and should be removed + /// on next poll. pub fn remove_future(&self, id: TaskId) { self.tasks.remove_fut(id); } diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index b9640792d..a6a524e27 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -1,5 +1,7 @@ use crate::innerlude::*; +/// An iterator that only yields "real" [`Element`]s. IE only Elements that are +/// not [`VNode::VComponent`] or [`VNode::VFragment`], . pub struct ElementIdIterator<'a> { vdom: &'a VirtualDom, @@ -9,6 +11,9 @@ pub struct ElementIdIterator<'a> { } impl<'a> ElementIdIterator<'a> { + /// Create a new iterator from the given [`VirtualDom`] and [`VNode`] + /// + /// This will allow you to iterate through all the real childrne of the [`VNode`]. pub fn new(vdom: &'a VirtualDom, node: &'a VNode<'a>) -> Self { Self { vdom, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index a7092104c..6d1678773 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -114,15 +114,18 @@ pub struct VirtualDom { ), } +/// The type of message that can be sent to the scheduler. +/// +/// These messages control how the scheduler will process updates to the UI. #[derive(Debug)] pub enum SchedulerMsg { - // events from the host + /// Events from the Renderer Event(UserEvent), - // setstate + /// Immediate updates from Components that mark them as dirty Immediate(ScopeId), - // an async task pushed from an event handler (or just spawned) + /// New tasks from components that should be polled when the next poll is ready NewTask(ScopeId), } @@ -388,6 +391,9 @@ impl VirtualDom { } } + /// Handle an individual message for the scheduler. + /// + /// This will either call an event listener or mark a component as dirty. pub fn process_message(&mut self, msg: SchedulerMsg) { match msg { SchedulerMsg::NewTask(_id) => { diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index e20d80dec..858f07c5c 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -79,7 +79,7 @@ impl ToTokens for CallBody { match &self.custom_context { // The `in cx` pattern allows directly rendering Some(ident) => out_tokens.append_all(quote! { - #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + #ident.render(LazyNodes::new(move |__cx: NodeFactory| -> VNode { use dioxus_elements::{GlobalAttributes, SvgAttributes}; #inner })) @@ -87,7 +87,7 @@ impl ToTokens for CallBody { // Otherwise we just build the LazyNode wrapper None => out_tokens.append_all(quote! { - LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + LazyNodes::new(move |__cx: NodeFactory| -> VNode { use dioxus_elements::{GlobalAttributes, SvgAttributes}; #inner }) From d700850b70f0aa6db346b0eea382f36cd52dda42 Mon Sep 17 00:00:00 2001 From: Oliver Forral Date: Sun, 20 Feb 2022 17:27:07 -0700 Subject: [PATCH 094/129] fixing build warnings in hello world example Fixing this warning when I try to compile warning: function `App` should have a snake case name --> src/main.rs:7:4 | 7 | fn App(cx: Scope) -> Element { | ^^^ help: convert the identifier to snake case: `app` | = note: `#[warn(non_snake_case)]` on by default Also changing edition from 2018 to 2021 --- docs/guide/src/hello_world.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/src/hello_world.md b/docs/guide/src/hello_world.md index e1d4777d9..32cdab4a9 100644 --- a/docs/guide/src/hello_world.md +++ b/docs/guide/src/hello_world.md @@ -63,7 +63,7 @@ $ cat Cargo.toml [package] name = "hello-dioxus" version = "0.1.0" -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -92,10 +92,10 @@ use dioxus::prelude::*; fn main() { - dioxus::desktop::launch(App); + dioxus::desktop::launch(app); } -fn App(cx: Scope) -> Element { +fn app(cx: Scope) -> Element { cx.render(rsx! ( div { "Hello, world!" } )) @@ -118,14 +118,14 @@ This initialization code launches a Tokio runtime on a helper thread where your ```rust fn main() { - dioxus::desktop::launch(App); + dioxus::desktop::launch(app); } ``` Finally, our app. Every component in Dioxus is a function that takes in `Context` and `Props` and returns an `Element`. ```rust -fn App(cx: Scope) -> Element { +fn app(cx: Scope) -> Element { cx.render(rsx! { div { "Hello, world!" } }) From b2a4d387c7e031485126efb4919ef31532597b1e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 20 Feb 2022 23:52:36 -0500 Subject: [PATCH 095/129] docs: document all of desktop crate --- packages/desktop/src/controller.rs | 3 +- packages/desktop/src/desktop_context.rs | 98 +++++++++++++++++++--- packages/desktop/src/lib.rs | 26 +++--- packages/desktop/src/user_window_events.rs | 72 ---------------- packages/hooks/src/lib.rs | 3 + packages/hooks/src/usefuture.rs | 42 ++++++++-- packages/web/src/lib.rs | 2 + 7 files changed, 138 insertions(+), 108 deletions(-) delete mode 100644 packages/desktop/src/user_window_events.rs diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 8497a42e9..06dfd3e9c 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -1,5 +1,4 @@ -use crate::desktop_context::DesktopContext; -use crate::user_window_events::UserWindowEvent; +use crate::desktop_context::{DesktopContext, UserWindowEvent}; use dioxus_core::*; use std::{ collections::{HashMap, VecDeque}, diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index e1b726d73..1376110f1 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,14 +3,23 @@ use std::rc::Rc; use dioxus_core::ScopeState; use wry::application::event_loop::EventLoopProxy; -use crate::user_window_events::UserWindowEvent; use UserWindowEvent::*; -type ProxyType = EventLoopProxy; +pub type ProxyType = EventLoopProxy; -/// Desktop-Window handle api context +/// Get an imperative handle to the current window +pub fn use_window(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| cx.consume_context::()) + .as_ref() + .unwrap() +} + +/// An imperative interface to the current window. /// -/// you can use this context control some window event +/// To get a handle to the current window, use the [`use_window`] hook. +/// +/// +/// # Example /// /// you can use `cx.consume_context::` to get this context /// @@ -19,7 +28,8 @@ type ProxyType = EventLoopProxy; /// ``` #[derive(Clone)] pub struct DesktopContext { - proxy: ProxyType, + /// The wry/tao proxy to the current window + pub proxy: ProxyType, } impl DesktopContext { @@ -84,12 +94,12 @@ impl DesktopContext { let _ = self.proxy.send_event(AlwaysOnTop(top)); } - // set cursor visible or not + /// set cursor visible or not pub fn set_cursor_visible(&self, visible: bool) { let _ = self.proxy.send_event(CursorVisible(visible)); } - // set cursor grab + /// set cursor grab pub fn set_cursor_grab(&self, grab: bool) { let _ = self.proxy.send_event(CursorGrab(grab)); } @@ -110,9 +120,73 @@ impl DesktopContext { } } -/// use this function can get the `DesktopContext` context. -pub fn use_window(cx: &ScopeState) -> &Rc { - cx.use_hook(|_| cx.consume_context::()) - .as_ref() - .unwrap() +use wry::application::event_loop::ControlFlow; +use wry::application::window::Fullscreen as WryFullscreen; + +use crate::controller::DesktopController; + +pub enum UserWindowEvent { + Update, + + CloseWindow, + DragWindow, + FocusWindow, + + Visible(bool), + Minimize(bool), + Maximize(bool), + MaximizeToggle, + Resizable(bool), + AlwaysOnTop(bool), + Fullscreen(bool), + + CursorVisible(bool), + CursorGrab(bool), + + SetTitle(String), + SetDecorations(bool), + + DevTool, +} + +pub(super) fn handler( + user_event: UserWindowEvent, + desktop: &mut DesktopController, + control_flow: &mut ControlFlow, +) { + // currently dioxus-desktop supports a single window only, + // so we can grab the only webview from the map; + let webview = desktop.webviews.values().next().unwrap(); + let window = webview.window(); + + match user_event { + Update => desktop.try_load_ready_webviews(), + CloseWindow => *control_flow = ControlFlow::Exit, + DragWindow => { + // if the drag_window has any errors, we don't do anything + window.fullscreen().is_none().then(|| window.drag_window()); + } + Visible(state) => window.set_visible(state), + Minimize(state) => window.set_minimized(state), + Maximize(state) => window.set_maximized(state), + MaximizeToggle => window.set_maximized(!window.is_maximized()), + Fullscreen(state) => { + if let Some(handle) = window.current_monitor() { + window.set_fullscreen(state.then(|| WryFullscreen::Borderless(Some(handle)))); + } + } + FocusWindow => window.set_focus(), + Resizable(state) => window.set_resizable(state), + AlwaysOnTop(state) => window.set_always_on_top(state), + + CursorVisible(state) => window.set_cursor_visible(state), + CursorGrab(state) => { + let _ = window.set_cursor_grab(state); + } + + SetTitle(content) => window.set_title(&content), + SetDecorations(state) => window.set_decorations(state), + + DevTool => webview.devtool(), + } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 4d204a378..cd8099e3c 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -1,18 +1,23 @@ #![doc = include_str!("readme.md")] #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] +#![deny(missing_docs)] -pub mod cfg; +mod cfg; mod controller; -pub mod desktop_context; -pub mod escape; -pub mod events; +mod desktop_context; +mod escape; +mod events; mod protocol; -mod user_window_events; +use desktop_context::UserWindowEvent; +pub use desktop_context::{use_window, DesktopContext}; +pub use wry; +pub use wry::application as tao; + +use crate::events::trigger_from_serialized; use cfg::DesktopConfig; use controller::DesktopController; -pub use desktop_context::use_window; use dioxus_core::*; use events::parse_ipc_message; use tao::{ @@ -20,12 +25,8 @@ use tao::{ event_loop::{ControlFlow, EventLoop}, window::Window, }; -pub use wry; -pub use wry::application as tao; use wry::webview::WebViewBuilder; -use crate::events::trigger_from_serialized; - /// Launch the WebView and run the event loop. /// /// This function will start a multithreaded Tokio runtime as well the WebView event loop. @@ -139,8 +140,7 @@ pub fn launch_with_props( } "initialize" => { is_ready.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = proxy - .send_event(user_window_events::UserWindowEvent::Update); + let _ = proxy.send_event(UserWindowEvent::Update); } "browser_open" => { println!("browser_open"); @@ -214,7 +214,7 @@ pub fn launch_with_props( }, Event::UserEvent(user_event) => { - user_window_events::handler(user_event, &mut desktop, control_flow) + desktop_context::handler(user_event, &mut desktop, control_flow) } Event::MainEventsCleared => {} Event::Resumed => {} diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs deleted file mode 100644 index aab14561b..000000000 --- a/packages/desktop/src/user_window_events.rs +++ /dev/null @@ -1,72 +0,0 @@ -use wry::application::event_loop::ControlFlow; -use wry::application::window::Fullscreen as WryFullscreen; - -use crate::controller::DesktopController; - -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(), - } -} diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index cb4afd297..2960832c6 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -1,3 +1,6 @@ +#![deny(missing_docs)] +//! Useful foundational hooks for Dioxus + mod usestate; pub use usestate::{use_state, UseState}; diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 58f350124..32b359204 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -1,19 +1,30 @@ +#![allow(missing_docs)] use dioxus_core::{ScopeState, TaskId}; use std::{cell::Cell, future::Future, rc::Rc}; +/// A future that resolves to a value. +/// +/// This runs the future only once - though the future may be regenerated +/// through the [`UseFuture::restart`] method. +/// +/// This is commonly used for components that cannot be rendered until some +/// asynchronous operation has completed. +/// +/// +/// +/// +/// pub fn use_future<'a, T: 'static, F: Future + 'static>( cx: &'a ScopeState, new_fut: impl FnOnce() -> F, ) -> &'a UseFuture { - let state = cx.use_hook(move |_| { - // - UseFuture { - update: cx.schedule_update(), - needs_regen: Cell::new(true), - slot: Rc::new(Cell::new(None)), - value: None, - task: None, - } + let state = cx.use_hook(move |_| UseFuture { + update: cx.schedule_update(), + needs_regen: Cell::new(true), + slot: Rc::new(Cell::new(None)), + value: None, + task: None, + pending: true, }); if let Some(value) = state.slot.take() { @@ -24,6 +35,7 @@ pub fn use_future<'a, T: 'static, F: Future + 'static>( if state.needs_regen.get() { // We don't need regen anymore state.needs_regen.set(false); + state.pending = false; // Create the new future let fut = new_fut(); @@ -42,10 +54,17 @@ pub fn use_future<'a, T: 'static, F: Future + 'static>( state } +pub enum FutureState<'a, T> { + Pending, + Complete(&'a T), + Regenerating(&'a T), // the old value +} + pub struct UseFuture { update: Rc, needs_regen: Cell, value: Option, + pending: bool, slot: Rc>>, task: Option, } @@ -72,4 +91,9 @@ impl UseFuture { pub fn value(&self) -> Option<&T> { self.value.as_ref() } + + pub fn state(&self) -> FutureState { + // self.value.as_ref() + FutureState::Pending + } } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 90fe84d04..c82dc4975 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -1,3 +1,5 @@ +#![deny(missing_docs)] + //! Dioxus WebSys //! //! ## Overview From fb661e5519f04d9efe29968ad359d7a4e54c99c6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 20 Feb 2022 23:52:54 -0500 Subject: [PATCH 096/129] wip: disable hook documentation --- packages/hooks/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 2960832c6..cb0c43fb9 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(missing_docs)] +// #![deny(missing_docs)] //! Useful foundational hooks for Dioxus mod usestate; From 86729d929d2e39fed46b338d8d9286246e302ba6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 21 Feb 2022 00:37:46 -0500 Subject: [PATCH 097/129] wip: coroutine works basically --- packages/hooks/Cargo.toml | 2 + packages/hooks/src/usecoroutine.rs | 376 +++++++++++++++++++++++------ 2 files changed, 304 insertions(+), 74 deletions(-) diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 12b671d14..e97885743 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -13,6 +13,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../../packages/core", version = "^0.1.9" } +futures-channel = "0.3.21" +log = { version = "0.4", features = ["release_max_level_off"] } [dev-dependencies] diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 5636b4031..7be9377fe 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -1,13 +1,15 @@ #![warn(clippy::pedantic)] -use bumpalo::boxed::Box as BumpBox; use dioxus_core::exports::bumpalo; use dioxus_core::{LazyNodes, ScopeState, TaskId}; +use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use std::any::Any; use std::future::Future; use std::{cell::Cell, rc::Rc}; /// Maintain a handle over a future that can be paused, resumed, and canceled. /// +/// This is an upgraded form of use_future with lots of bells-and-whistles. /// /// /// @@ -23,82 +25,95 @@ use std::{cell::Cell, rc::Rc}; /// /// /// -pub fn use_coroutine<'a, F>( +/// +pub fn use_coroutine<'a, O: 'static, M: 'static>( cx: &'a ScopeState, - create_future: impl FnOnce() -> F + 'a, -) -> UseCoroutine<'a, F> -where - F: Future + 'static, -{ - let state = cx.use_hook(move |_| CoroutineInner { - task_id: Cell::new(None), - running: std::rc::Rc::default(), - run_count: Cell::new(0), +) -> UseCoroutineBuilder<'a, O, M> { + let inner = cx.use_hook(|_| { + // + UseCoroutine { + val: Cell::new(None), + rx: Cell::new(None), + tx: None, + } }); - // as an optimization, we use the bump arena to allocate the callback instead of boxes - // that way we don't always call the constructor, but it's still efficient - // safety: bumpalo is limited in constructing unsized box types, so we have to do it through dynamic dispatch - let boxed: BumpBox<'a, dyn FnMut() -> F + 'a> = unsafe { - let mut bump = None; - cx.render(LazyNodes::new(move |f| { - bump.replace(f.bump()); - f.static_text("") - })); - let mut slot = Some(create_future); - let bump = bump.expect("bump is assigned during render"); - BumpBox::from_raw(bump.alloc(move || { - let inner = slot.take().expect("closure to not be called twice"); - inner() - })) - }; - - state.run_count.set(state.run_count.get() + 1); - - UseCoroutine { - inner: state, - create_fut: Cell::new(Some(boxed)), - cx, - } + UseCoroutineBuilder { cx, inner } } -struct CoroutineInner { - running: Rc>, - task_id: Cell>, - run_count: Cell, -} - -pub struct UseCoroutine<'a, F: Future + 'static> { - create_fut: Cell F + 'a>>>, - inner: &'a CoroutineInner, +pub struct UseCoroutineBuilder<'a, O, M = ()> { cx: &'a ScopeState, + inner: &'a mut UseCoroutine, } -impl<'a, F: Future + 'static> UseCoroutine<'a, F> { - pub fn auto_start(&self, start: bool) -> &Self { - if start && self.inner.run_count.get() == 1 { - self.start(); - } +impl<'a, O: 'static, M> UseCoroutineBuilder<'a, O, M> { + // fn with_channel(self) -> UseCoroutineBuilder<'a, O, I> { + // UseCoroutineBuilder { + // cx: self.cx, + // inner: self.inner, + // } + // } + + fn build>(mut self, f: impl FnOnce() -> F) -> &'a UseCoroutine { + todo!() + } + fn build_channel>( + mut self, + f: impl FnOnce(UnboundedReceiver) -> F, + ) -> &'a UseCoroutine { + todo!() + } + + pub fn use_dep(mut self) -> Self { self } + /// Provide the channel to downstream consumers + pub fn provide_context(mut self) -> Self { + self + } + + pub fn auto_start(mut self, start: bool) -> Self { + // if start && self.inner.run_count.get() == 1 { + // self.start(); + // } + self + } +} + +pub struct UseCoroutine { + val: Cell>, + rx: Cell>>, + tx: Option>, +} + +impl UseCoroutine { pub fn is_running(&self) -> bool { - self.inner.running.get() + false + // self.inner.running.get() } pub fn start(&self) { - if !self.is_running() { - if let Some(mut fut) = self.create_fut.take() { - let fut = fut(); - let ready_handle = self.inner.running.clone(); + // if !self.is_running() { + // if let Some(mut fut) = self.create_fut.take() { + // let fut = fut(); + // let ready_handle = self.inner.running.clone(); - let task = self.cx.push_future(async move { - ready_handle.set(true); - fut.await; - ready_handle.set(false); - }); + // let task = self.cx.push_future(async move { + // ready_handle.set(true); + // fut.await; + // ready_handle.set(false); + // }); - self.inner.task_id.set(Some(task)); + // self.inner.task_id.set(Some(task)); + // } + // } + } + + pub fn send(&self, msg: M) { + if let Some(tx) = self.tx.clone() { + if tx.unbounded_send(msg).is_err() { + log::error!("Failed to send message"); } } } @@ -121,32 +136,245 @@ mod tests { use futures_util::StreamExt; fn app(cx: Scope) -> Element { - let poll_tasks = use_coroutine(&cx, || async { + let poll_tasks = use_coroutine(&cx).auto_start(false).build(|| async { loop { println!("polling tasks"); } }); - poll_tasks.auto_start(true); - todo!() } fn app_with_channel(cx: Scope) -> Element { - let (tx, mut rx) = unbounded(); + // let poll_tasks = use_coroutine(&cx).build_channel(|mut rx| async move { + // while let Some(msg) = rx.next().await { + // println!("polling tasks: {}", msg); + // } + // }); - let tx = cx.use_hook(|_| tx); + let poll_tasks = + use_coroutine(&cx).build_channel(|mut rx: UnboundedReceiver<()>| async move { + while let Some(msg) = rx.next().await { + println!("polling tasks: {:?}", msg); + } + }); - let poll_tasks = use_coroutine(&cx, move || async move { - while let Some(msg) = rx.next().await { - println!("polling tasks: {}", msg); - } - }); - - poll_tasks.auto_start(true); - - tx.unbounded_send("asd").unwrap(); + // poll_tasks.send(10); todo!() } } + +mod use2 { + #![allow(missing_docs)] + + use dioxus_core::{ScopeState, TaskId}; + use std::{ + any::Any, + cell::{Cell, RefCell}, + future::Future, + rc::Rc, + }; + + /// A future that resolves to a value. + /// + /// This runs the future only once - though the future may be regenerated + /// through the [`UseFuture::restart`] method. + /// + /// This is commonly used for components that cannot be rendered until some + /// asynchronous operation has completed. + /// + /// + /// + /// + /// + pub fn use_future<'a>( + // pub fn use_future<'a, T: 'static, F: Future + 'static>( + cx: &'a ScopeState, + ) -> &'a UseFuture<()> { + // let state = cx.use_hook(move |_| UseFuture { + // update: cx.schedule_update(), + // needs_regen: Cell::new(true), + // slot: Rc::new(Cell::new(None)), + // value: None, + // task: None, + // pending: true, + // dep_cont: Cell::new(0), + // deps: RefCell::new(Vec::new()), + // first_time: true, + // }); + + // if let Some(value) = state.slot.take() { + // state.value = Some(value); + // state.task = None; + // } + + // if state.needs_regen.get() { + // // We don't need regen anymore + // state.needs_regen.set(false); + // state.pending = false; + + // // Create the new future + // let fut = new_fut(); + + // // Clone in our cells + // let slot = state.slot.clone(); + // let updater = state.update.clone(); + + // state.task = Some(cx.push_future(async move { + // let res = fut.await; + // slot.set(Some(res)); + // updater(); + // })); + // } + + // state.first_time = false; + + // state + + // new_fut: impl FnOnce() -> F, + + todo!() + } + + pub enum FutureState<'a, T> { + Pending, + Complete(&'a T), + Regenerating(&'a T), // the old value + } + + pub struct UseFuture { + update: Rc, + needs_regen: Cell, + value: Option, + pending: bool, + slot: Rc>>, + task: Option, + deps: RefCell>>, + dep_cont: Cell, + first_time: bool, + } + + impl UseFuture { + pub fn restart(&self) { + self.needs_regen.set(true); + (self.update)(); + } + + // clears the value in the future slot without starting the future over + pub fn clear(&self) -> Option { + (self.update)(); + self.slot.replace(None) + } + + // Manually set the value in the future slot without starting the future over + pub fn set(&self, new_value: T) { + self.slot.set(Some(new_value)); + self.needs_regen.set(true); + (self.update)(); + } + + pub fn value(&self) -> Option<&T> { + self.value.as_ref() + } + + pub fn state(&self) -> FutureState { + // self.value.as_ref() + FutureState::Pending + } + + /// Add this value to the dependency list + /// + /// This is a hook and should be called during the initial hook process. + /// It should •not• be called in a conditional. + pub fn use_dep(&self, dependency: &F) -> &Self { + let count = self.dep_cont.get(); + let mut deps = self.deps.borrow_mut(); + + match deps.get_mut(count) { + Some(dep) => match dep.downcast_mut::() { + Some(saved_dep) => { + if dependency != saved_dep { + *saved_dep = dependency.clone(); + self.needs_regen.set(true); + } + } + None => { + if cfg!(debug_assertions) { + panic!("Tried to use a dependency for use_future outside of the use_future hook."); + } + } + }, + None => deps.push(Box::new(dependency.to_owned())), + } + + self + } + + pub fn restart_if(&self, f: impl FnOnce() -> bool) -> &Self { + self + } + + pub fn build(&self, new_fut: impl FnOnce() -> F) {} + } + + #[test] + fn test_use_future_deps() { + use dioxus_core::prelude::*; + + struct MyProps { + val: String, + name: i32, + } + + fn app(cx: Scope) -> Element { + let MyProps { val, name } = cx.props; + + // let val = use_c(&cx) + // .use_dep(val) + // .restart_if(|| false) + // .use_dep(name) + // .build(|(val, name)| async move {}); + + // async fn fetch_thing(name: String, num: i32) -> String { + // format!("{} {}", name, num) + // } + + // let val = use_future(&cx, || fetch_thing(val.clone(), *name)) + // .with_dep(val) + // .with_dep(name) + // .restart_if(|| false); + + None + } + } + + // pub struct CoroutineBuilder<'a, const PARAM: usize> { + // deps: Vec>, + // cx: &'a ScopeState, + // } + + // macro_rules! dep_impl { + // ($id1:literal to $id2:literal) => { + // impl<'a> CoroutineBuilder<'a, $id1> { + // pub fn use_dep( + // mut self, + // dep: &F, + // ) -> CoroutineBuilder<'a, $id2> { + // self.deps.push(Box::new(dep.clone())); + // unsafe { std::mem::transmute(self) } + // } + // } + // }; + // } + + // dep_impl!(0 to 1); + // dep_impl!(1 to 2); + // dep_impl!(2 to 3); + // dep_impl!(3 to 4); + // dep_impl!(4 to 5); + // dep_impl!(5 to 6); + // dep_impl!(6 to 7); + // dep_impl!(7 to 8); + // dep_impl!(8 to 9); +} From 9211e1fc7862ddb46ebbb8a6ddb26d5476f6d4b3 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 21 Feb 2022 15:39:47 -0500 Subject: [PATCH 098/129] feat: use future fully figured out --- examples/dog_app.rs | 38 +-- examples/suspense.rs | 6 +- examples/tasks.rs | 4 +- packages/core/src/scopes.rs | 2 +- packages/hooks/docs/old.md | 182 +++++++++++++ packages/hooks/src/usecoroutine.rs | 395 +++++++---------------------- packages/hooks/src/usefuture.rs | 202 +++++++++++++-- 7 files changed, 481 insertions(+), 348 deletions(-) create mode 100644 packages/hooks/docs/old.md diff --git a/examples/dog_app.rs b/examples/dog_app.rs index a30f3e5e8..ad983035e 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -16,7 +16,9 @@ struct ListBreeds { } fn app(cx: Scope) -> Element { - let breeds = use_future(&cx, || async move { + let (breed, set_breed) = use_state(&cx, || None); + + let breeds = use_future(&cx, (), |_| async move { reqwest::get("https://dog.ceo/api/breeds/list/all") .await .unwrap() @@ -24,13 +26,10 @@ fn app(cx: Scope) -> Element { .await }); - let (breed, set_breed) = use_state(&cx, || None); - match breeds.value() { Some(Ok(breeds)) => cx.render(rsx! { div { - h1 {"Select a dog breed!"} - + h1 { "Select a dog breed!" } div { display: "flex", ul { flex: "50%", breeds.message.keys().map(|breed| rsx!( @@ -51,34 +50,23 @@ fn app(cx: Scope) -> Element { } } }), - Some(Err(_e)) => cx.render(rsx! { - div { "Error fetching breeds" } - }), - None => cx.render(rsx! { - div { "Loading dogs..." } - }), + Some(Err(_e)) => cx.render(rsx! { div { "Error fetching breeds" } }), + None => cx.render(rsx! { div { "Loading dogs..." } }), } } +#[derive(serde::Deserialize, Debug)] +struct DogApi { + message: String, +} + #[inline_props] fn Breed(cx: Scope, breed: String) -> Element { - #[derive(serde::Deserialize, Debug)] - struct DogApi { - message: String, - } - - let endpoint = format!("https://dog.ceo/api/breed/{}/images/random", breed); - - let fut = use_future(&cx, || async move { + let fut = use_future(&cx, (breed,), |(breed,)| async move { + let endpoint = format!("https://dog.ceo/api/breed/{}/images/random", breed); reqwest::get(endpoint).await.unwrap().json::().await }); - let (name, set_name) = use_state(&cx, || breed.clone()); - if name != breed { - set_name(breed.clone()); - fut.restart(); - } - cx.render(match fut.value() { Some(Ok(resp)) => rsx! { button { diff --git a/examples/suspense.rs b/examples/suspense.rs index 5a9bf4fd8..ace5954f2 100644 --- a/examples/suspense.rs +++ b/examples/suspense.rs @@ -35,8 +35,8 @@ fn app(cx: Scope) -> Element { div { h1 {"Dogs are very important"} p { - "The dog or domestic dog (Canis familiaris[4][5] or Canis lupus familiaris[5])" - "is a domesticated descendant of the wolf which is characterized by an upturning tail." + "The dog or domestic dog (Canis familiaris[4][5] or Canis lupus familiaris[5])" + "is a domesticated descendant of the wolf which is characterized by an upturning tail." "The dog derived from an ancient, extinct wolf,[6][7] and the modern grey wolf is the" "dog's nearest living relative.[8] The dog was the first species to be domesticated,[9][8]" "by hunter–gatherers over 15,000 years ago,[7] before the development of agriculture.[1]" @@ -52,7 +52,7 @@ fn app(cx: Scope) -> Element { /// Suspense is achieved my moving the future into only the component that /// actually renders the data. fn Doggo(cx: Scope) -> Element { - let fut = use_future(&cx, || async move { + let fut = use_future(&cx, (), |_| async move { reqwest::get("https://dog.ceo/api/breeds/image/random/") .await .unwrap() diff --git a/examples/tasks.rs b/examples/tasks.rs index 3c4d4cba3..22b61eab8 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -12,8 +12,8 @@ fn main() { fn app(cx: Scope) -> Element { let (count, set_count) = use_state(&cx, || 0); - use_future(&cx, move || { - let set_count = set_count.to_owned(); + use_future(&cx, (), move |_| { + let set_count = set_count.clone(); async move { loop { tokio::time::sleep(Duration::from_millis(1000)).await; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 00c3024bf..7508a93d4 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -748,7 +748,7 @@ impl ScopeState { } // todo: attach some state to the future to know if we should poll it - pub fn remove_future(&self, id: TaskId) { + pub fn cancel_future(&self, id: TaskId) { self.tasks.remove_fut(id); } diff --git a/packages/hooks/docs/old.md b/packages/hooks/docs/old.md new file mode 100644 index 000000000..269cd90ed --- /dev/null +++ b/packages/hooks/docs/old.md @@ -0,0 +1,182 @@ + +mod use2 { + #![allow(missing_docs)] + + use dioxus_core::{ScopeState, TaskId}; + use std::{ + any::Any, + cell::{Cell, RefCell}, + future::Future, + rc::Rc, + }; + + /// A future that resolves to a value. + /// + /// This runs the future only once - though the future may be regenerated + /// through the [`UseFuture::restart`] method. + /// + /// This is commonly used for components that cannot be rendered until some + /// asynchronous operation has completed. + /// + /// + /// + /// + /// + pub fn use_future<'a>( + // pub fn use_future<'a, T: 'static, F: Future + 'static>( + cx: &'a ScopeState, + ) -> &'a UseFuture<()> { + // let state = cx.use_hook(move |_| UseFuture { + // update: cx.schedule_update(), + // needs_regen: Cell::new(true), + // slot: Rc::new(Cell::new(None)), + // value: None, + // task: None, + // pending: true, + // dep_cont: Cell::new(0), + // deps: RefCell::new(Vec::new()), + // first_time: true, + // }); + + // if let Some(value) = state.slot.take() { + // state.value = Some(value); + // state.task = None; + // } + + // if state.needs_regen.get() { + // // We don't need regen anymore + // state.needs_regen.set(false); + // state.pending = false; + + // // Create the new future + // let fut = new_fut(); + + // // Clone in our cells + // let slot = state.slot.clone(); + // let updater = state.update.clone(); + + // state.task = Some(cx.push_future(async move { + // let res = fut.await; + // slot.set(Some(res)); + // updater(); + // })); + // } + + // state.first_time = false; + + // state + + // new_fut: impl FnOnce() -> F, + + todo!() + } + + pub enum FutureState<'a, T> { + Pending, + Complete(&'a T), + Regenerating(&'a T), // the old value + } + + pub struct UseFuture { + update: Rc, + needs_regen: Cell, + value: Option, + slot: Rc>>, + task: Option, + pending: bool, + deps: RefCell>>, + dep_cont: Cell, + first_time: bool, + } + + impl UseFuture { + pub fn restart(&self) { + self.needs_regen.set(true); + (self.update)(); + } + + // clears the value in the future slot without starting the future over + pub fn clear(&self) -> Option { + (self.update)(); + self.slot.replace(None) + } + + // Manually set the value in the future slot without starting the future over + pub fn set(&self, new_value: T) { + self.slot.set(Some(new_value)); + self.needs_regen.set(true); + (self.update)(); + } + + pub fn value(&self) -> Option<&T> { + self.value.as_ref() + } + + pub fn state(&self) -> FutureState { + // self.value.as_ref() + FutureState::Pending + } + + pub fn build(&self, new_fut: impl FnOnce() -> F) {} + } + + #[test] + fn test_use_future_deps() { + use dioxus_core::prelude::*; + + struct MyProps { + val: String, + name: i32, + } + + fn app(cx: Scope) -> Element { + let MyProps { val, name } = cx.props; + + // let val = use_c(&cx) + // .use_dep(val) + // .restart_if(|| false) + // .use_dep(name) + // .build(|(val, name)| async move {}); + + // async fn fetch_thing(name: String, num: i32) -> String { + // format!("{} {}", name, num) + // } + + // let val = use_future(&cx, || fetch_thing(val.clone(), *name)) + // .with_dep(val) + // .with_dep(name) + // .restart_if(|| false); + + None + } + } + + // pub struct CoroutineBuilder<'a, const PARAM: usize> { + // deps: Vec>, + // cx: &'a ScopeState, + // } + + // macro_rules! dep_impl { + // ($id1:literal to $id2:literal) => { + // impl<'a> CoroutineBuilder<'a, $id1> { + // pub fn use_dep( + // mut self, + // dep: &F, + // ) -> CoroutineBuilder<'a, $id2> { + // self.deps.push(Box::new(dep.clone())); + // unsafe { std::mem::transmute(self) } + // } + // } + // }; + // } + + // dep_impl!(0 to 1); + // dep_impl!(1 to 2); + // dep_impl!(2 to 3); + // dep_impl!(3 to 4); + // dep_impl!(4 to 5); + // dep_impl!(5 to 6); + // dep_impl!(6 to 7); + // dep_impl!(7 to 8); + // dep_impl!(8 to 9); +} diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 7be9377fe..40a12443d 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -9,95 +9,82 @@ use std::{cell::Cell, rc::Rc}; /// Maintain a handle over a future that can be paused, resumed, and canceled. /// -/// This is an upgraded form of use_future with lots of bells-and-whistles. +/// This is an upgraded form of [`use_future`] with lots of bells-and-whistles. +/// +/// [`use_coroutine`] is well suited for long-running tasks and is very customizable. +/// +/// +/// ## Long running tasks /// /// /// +/// ## One-off tasks /// /// +/// ## Cancellation /// /// -/// -/// -/// -/// -/// -/// -/// -/// -/// -pub fn use_coroutine<'a, O: 'static, M: 'static>( - cx: &'a ScopeState, -) -> UseCoroutineBuilder<'a, O, M> { - let inner = cx.use_hook(|_| { +/// ## Global State +#[allow(clippy::mut_from_ref)] +pub fn use_coroutine(cx: &ScopeState) -> &mut UseCoroutine { + cx.use_hook(|_| { // UseCoroutine { val: Cell::new(None), rx: Cell::new(None), tx: None, + first_run: true, + deps: vec![], + dep_cnt: 0, + needs_regen: false, + auto_start: true, } - }); - - UseCoroutineBuilder { cx, inner } -} - -pub struct UseCoroutineBuilder<'a, O, M = ()> { - cx: &'a ScopeState, - inner: &'a mut UseCoroutine, -} - -impl<'a, O: 'static, M> UseCoroutineBuilder<'a, O, M> { - // fn with_channel(self) -> UseCoroutineBuilder<'a, O, I> { - // UseCoroutineBuilder { - // cx: self.cx, - // inner: self.inner, - // } - // } - - fn build>(mut self, f: impl FnOnce() -> F) -> &'a UseCoroutine { - todo!() - } - fn build_channel>( - mut self, - f: impl FnOnce(UnboundedReceiver) -> F, - ) -> &'a UseCoroutine { - todo!() - } - - pub fn use_dep(mut self) -> Self { - self - } - - /// Provide the channel to downstream consumers - pub fn provide_context(mut self) -> Self { - self - } - - pub fn auto_start(mut self, start: bool) -> Self { - // if start && self.inner.run_count.get() == 1 { - // self.start(); - // } - self - } + }) } pub struct UseCoroutine { val: Cell>, rx: Cell>>, tx: Option>, + first_run: bool, + deps: Vec>, + dep_cnt: usize, + needs_regen: bool, + auto_start: bool, +} + +pub enum FutureState<'a, T> { + Pending, + Complete(&'a T), + Regenerating(&'a T), // the old value +} + +impl UseCoroutine { + /// explicitly set the type of the channel used by the coroutine + fn with_channel(&mut self) -> &mut UseCoroutine { + if self.first_run { + // self.provide_context() + } + todo!() + } + + /// explicitly set the type of the channel used by the coroutine + fn with_channel_isolate(&mut self) -> &mut UseCoroutine { + todo!() + } } impl UseCoroutine { pub fn is_running(&self) -> bool { false - // self.inner.running.get() + // self.running.get() } pub fn start(&self) { // if !self.is_running() { // if let Some(mut fut) = self.create_fut.take() { // let fut = fut(); - // let ready_handle = self.inner.running.clone(); + // let ready_handle = self.running.clone(); // let task = self.cx.push_future(async move { // ready_handle.set(true); @@ -105,7 +92,7 @@ impl UseCoroutine { // ready_handle.set(false); // }); - // self.inner.task_id.set(Some(task)); + // self.task_id.set(Some(task)); // } // } } @@ -121,11 +108,55 @@ impl UseCoroutine { // todo: wire these up, either into the task system or into the coroutine system itself // we would have change how we poll the coroutine and how its awaken + fn build>(&mut self, f: impl FnOnce(UnboundedReceiver) -> F) -> &Self { + self.first_run = false; + if self.auto_start || self.needs_regen { + // + } + + self + } + + pub fn auto_start(mut self, start: bool) -> Self { + // if start && self.run_count.get() == 1 { + // self.start(); + // } + self + } + + /// Add this value to the dependency list + /// + /// This is a hook and should be called during the initial hook process. + /// It should •not• be called in a conditional. + pub fn with_dep(&mut self, dependency: &F) -> &mut Self { + if let Some(dep) = self.deps.get_mut(self.dep_cnt) { + if let Some(saved_dep) = dep.downcast_mut::() { + if dependency != saved_dep { + *saved_dep = dependency.clone(); + self.needs_regen = true; + } + }; + } else { + self.deps.push(Box::new(dependency.to_owned())); + self.needs_regen = true; + } + + self + } + + pub fn restart_if(&self, f: impl FnOnce() -> bool) -> &Self { + self + } + // pub fn resume(&self) {} // pub fn stop(&self) {} // pub fn restart(&self) {} } +pub struct CoroutineContext { + tx: UnboundedSender, +} + #[cfg(test)] mod tests { #![allow(unused)] @@ -135,246 +166,16 @@ mod tests { use dioxus_core::prelude::*; use futures_util::StreamExt; - fn app(cx: Scope) -> Element { - let poll_tasks = use_coroutine(&cx).auto_start(false).build(|| async { - loop { - println!("polling tasks"); - } - }); - - todo!() - } - - fn app_with_channel(cx: Scope) -> Element { - // let poll_tasks = use_coroutine(&cx).build_channel(|mut rx| async move { - // while let Some(msg) = rx.next().await { - // println!("polling tasks: {}", msg); - // } - // }); - - let poll_tasks = - use_coroutine(&cx).build_channel(|mut rx: UnboundedReceiver<()>| async move { + fn app(cx: Scope, name: String) -> Element { + let task = use_coroutine(&cx) + .with_dep(&name) + .with_channel::() + .build(|mut rx| async move { while let Some(msg) = rx.next().await { - println!("polling tasks: {:?}", msg); + println!("got message: {}", msg); } }); - // poll_tasks.send(10); - - todo!() + None } } - -mod use2 { - #![allow(missing_docs)] - - use dioxus_core::{ScopeState, TaskId}; - use std::{ - any::Any, - cell::{Cell, RefCell}, - future::Future, - rc::Rc, - }; - - /// A future that resolves to a value. - /// - /// This runs the future only once - though the future may be regenerated - /// through the [`UseFuture::restart`] method. - /// - /// This is commonly used for components that cannot be rendered until some - /// asynchronous operation has completed. - /// - /// - /// - /// - /// - pub fn use_future<'a>( - // pub fn use_future<'a, T: 'static, F: Future + 'static>( - cx: &'a ScopeState, - ) -> &'a UseFuture<()> { - // let state = cx.use_hook(move |_| UseFuture { - // update: cx.schedule_update(), - // needs_regen: Cell::new(true), - // slot: Rc::new(Cell::new(None)), - // value: None, - // task: None, - // pending: true, - // dep_cont: Cell::new(0), - // deps: RefCell::new(Vec::new()), - // first_time: true, - // }); - - // if let Some(value) = state.slot.take() { - // state.value = Some(value); - // state.task = None; - // } - - // if state.needs_regen.get() { - // // We don't need regen anymore - // state.needs_regen.set(false); - // state.pending = false; - - // // Create the new future - // let fut = new_fut(); - - // // Clone in our cells - // let slot = state.slot.clone(); - // let updater = state.update.clone(); - - // state.task = Some(cx.push_future(async move { - // let res = fut.await; - // slot.set(Some(res)); - // updater(); - // })); - // } - - // state.first_time = false; - - // state - - // new_fut: impl FnOnce() -> F, - - todo!() - } - - pub enum FutureState<'a, T> { - Pending, - Complete(&'a T), - Regenerating(&'a T), // the old value - } - - pub struct UseFuture { - update: Rc, - needs_regen: Cell, - value: Option, - pending: bool, - slot: Rc>>, - task: Option, - deps: RefCell>>, - dep_cont: Cell, - first_time: bool, - } - - impl UseFuture { - pub fn restart(&self) { - self.needs_regen.set(true); - (self.update)(); - } - - // clears the value in the future slot without starting the future over - pub fn clear(&self) -> Option { - (self.update)(); - self.slot.replace(None) - } - - // Manually set the value in the future slot without starting the future over - pub fn set(&self, new_value: T) { - self.slot.set(Some(new_value)); - self.needs_regen.set(true); - (self.update)(); - } - - pub fn value(&self) -> Option<&T> { - self.value.as_ref() - } - - pub fn state(&self) -> FutureState { - // self.value.as_ref() - FutureState::Pending - } - - /// Add this value to the dependency list - /// - /// This is a hook and should be called during the initial hook process. - /// It should •not• be called in a conditional. - pub fn use_dep(&self, dependency: &F) -> &Self { - let count = self.dep_cont.get(); - let mut deps = self.deps.borrow_mut(); - - match deps.get_mut(count) { - Some(dep) => match dep.downcast_mut::() { - Some(saved_dep) => { - if dependency != saved_dep { - *saved_dep = dependency.clone(); - self.needs_regen.set(true); - } - } - None => { - if cfg!(debug_assertions) { - panic!("Tried to use a dependency for use_future outside of the use_future hook."); - } - } - }, - None => deps.push(Box::new(dependency.to_owned())), - } - - self - } - - pub fn restart_if(&self, f: impl FnOnce() -> bool) -> &Self { - self - } - - pub fn build(&self, new_fut: impl FnOnce() -> F) {} - } - - #[test] - fn test_use_future_deps() { - use dioxus_core::prelude::*; - - struct MyProps { - val: String, - name: i32, - } - - fn app(cx: Scope) -> Element { - let MyProps { val, name } = cx.props; - - // let val = use_c(&cx) - // .use_dep(val) - // .restart_if(|| false) - // .use_dep(name) - // .build(|(val, name)| async move {}); - - // async fn fetch_thing(name: String, num: i32) -> String { - // format!("{} {}", name, num) - // } - - // let val = use_future(&cx, || fetch_thing(val.clone(), *name)) - // .with_dep(val) - // .with_dep(name) - // .restart_if(|| false); - - None - } - } - - // pub struct CoroutineBuilder<'a, const PARAM: usize> { - // deps: Vec>, - // cx: &'a ScopeState, - // } - - // macro_rules! dep_impl { - // ($id1:literal to $id2:literal) => { - // impl<'a> CoroutineBuilder<'a, $id1> { - // pub fn use_dep( - // mut self, - // dep: &F, - // ) -> CoroutineBuilder<'a, $id2> { - // self.deps.push(Box::new(dep.clone())); - // unsafe { std::mem::transmute(self) } - // } - // } - // }; - // } - - // dep_impl!(0 to 1); - // dep_impl!(1 to 2); - // dep_impl!(2 to 3); - // dep_impl!(3 to 4); - // dep_impl!(4 to 5); - // dep_impl!(5 to 6); - // dep_impl!(6 to 7); - // dep_impl!(7 to 8); - // dep_impl!(8 to 9); -} diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 58f350124..9db67aef2 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -1,42 +1,54 @@ use dioxus_core::{ScopeState, TaskId}; -use std::{cell::Cell, future::Future, rc::Rc}; +use std::{any::Any, cell::Cell, future::Future, rc::Rc}; -pub fn use_future<'a, T: 'static, F: Future + 'static>( +/// A hook that provides a future that will resolve to a value. +/// +/// Whenever the hooks dependencies change, the future will be re-evaluated. +/// If a future is pending when the dependencies change, the previous future +/// will be allowed to continue +/// +/// +/// - dependencies: a tuple of references to values that are PartialEq + Clone +pub fn use_future<'a, T: 'static, F: Future + 'static, D: UseFutureDep>( cx: &'a ScopeState, - new_fut: impl FnOnce() -> F, + dependencies: D, + future: impl FnOnce(D::Out) -> F, ) -> &'a UseFuture { - let state = cx.use_hook(move |_| { - // - UseFuture { - update: cx.schedule_update(), - needs_regen: Cell::new(true), - slot: Rc::new(Cell::new(None)), - value: None, - task: None, - } + let state = cx.use_hook(move |_| UseFuture { + update: cx.schedule_update(), + needs_regen: Cell::new(true), + slot: Rc::new(Cell::new(None)), + value: None, + task: Cell::new(None), + dependencies: Vec::new(), }); if let Some(value) = state.slot.take() { state.value = Some(value); - state.task = None; + state.task.set(None); } - if state.needs_regen.get() { + if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { // We don't need regen anymore state.needs_regen.set(false); // Create the new future - let fut = new_fut(); + let fut = future(dependencies.out()); // Clone in our cells let slot = state.slot.clone(); - let updater = state.update.clone(); + let schedule_update = state.update.clone(); - state.task = Some(cx.push_future(async move { + // Cancel the current future + if let Some(current) = state.task.take() { + cx.cancel_future(current); + } + + state.task.set(Some(cx.push_future(async move { let res = fut.await; slot.set(Some(res)); - updater(); - })); + schedule_update(); + }))); } state @@ -47,15 +59,33 @@ pub struct UseFuture { needs_regen: Cell, value: Option, slot: Rc>>, - task: Option, + task: Cell>, + dependencies: Vec>, +} + +pub enum UseFutureState<'a, T> { + Pending, + Complete(&'a T), + Reloading(&'a T), } impl UseFuture { + /// Restart the future with new dependencies. + /// + /// Will not cancel the previous future, but will ignore any values that it + /// generates. pub fn restart(&self) { self.needs_regen.set(true); (self.update)(); } + /// Forcefully cancel a future + pub fn cancel(&self, cx: &ScopeState) { + if let Some(task) = self.task.take() { + cx.cancel_future(task); + } + } + // clears the value in the future slot without starting the future over pub fn clear(&self) -> Option { (self.update)(); @@ -69,7 +99,139 @@ impl UseFuture { (self.update)(); } + /// Return any value, even old values if the future has not yet resolved. + /// + /// If the future has never completed, the returned value will be `None`. pub fn value(&self) -> Option<&T> { self.value.as_ref() } + + /// Get the ID of the future in Dioxus' internal scheduler + pub fn task(&self) -> Option { + self.task.get() + } + + /// Get the current stateof the future. + pub fn state(&self) -> UseFutureState { + match (&self.task.get(), &self.value) { + // If we have a task and an existing value, we're reloading + (Some(_), Some(val)) => UseFutureState::Reloading(val), + + // no task, but value - we're done + (None, Some(val)) => UseFutureState::Complete(val), + + // no task, no value - something's wrong? return pending + (None, None) => UseFutureState::Pending, + + // Task, no value - we're still pending + (Some(_), None) => UseFutureState::Pending, + } + } +} + +pub trait UseFutureDep: Sized + Clone { + type Out; + fn out(&self) -> Self::Out; + fn apply(self, state: &mut Vec>) -> bool; +} + +impl UseFutureDep for () { + type Out = (); + fn out(&self) -> Self::Out {} + fn apply(self, _state: &mut Vec>) -> bool { + false + } +} + +pub trait Dep: 'static + PartialEq + Clone {} +impl Dep for T where T: 'static + PartialEq + Clone {} + +macro_rules! impl_dep { + ( + $($el:ident=$name:ident,)* + ) => { + impl< $($el),* > UseFutureDep for ($(&$el,)*) + where + $( + $el: Dep + ),* + { + type Out = ($($el,)*); + + fn out(&self) -> Self::Out { + let ($($name,)*) = self; + ($((*$name).clone(),)*) + } + + #[allow(unused)] + fn apply(self, state: &mut Vec>) -> bool { + let ($($name,)*) = self; + let mut idx = 0; + let mut needs_regen = false; + + $( + match state.get_mut(idx).map(|f| f.downcast_mut::<$el>()).flatten() { + Some(val) => { + if *val != *$name { + *val = $name.clone(); + needs_regen = true; + } + } + None => { + state.push(Box::new($name.clone())); + needs_regen = true; + } + } + idx += 1; + )* + + needs_regen + } + } + }; +} + +impl_dep!(A = a,); +impl_dep!(A = a, B = b,); +impl_dep!(A = a, B = b, C = c,); +impl_dep!(A = a, B = b, C = c, D = d,); +impl_dep!(A = a, B = b, C = c, D = d, E = e,); +impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f,); + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(unused)] + #[test] + fn test_use_future() { + use dioxus_core::prelude::*; + + struct MyProps { + a: String, + b: i32, + c: i32, + d: i32, + e: i32, + } + + fn app(cx: Scope) -> Element { + // should only ever run once + let fut = use_future(&cx, (), |_| async move { + // + }); + + // runs when a is changed + let fut = use_future(&cx, (&cx.props.a,), |(a,)| async move { + // + }); + + // runs when a or b is changed + let fut = use_future(&cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { + // + }); + + None + } + } } From fbaf25dc6b381f0797fc4d38e6d14700739143de Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 21 Feb 2022 23:34:18 -0500 Subject: [PATCH 099/129] feat: pair down usecoroutine and make it much happier --- packages/hooks/src/usecoroutine.rs | 263 +++++++++++++---------------- 1 file changed, 113 insertions(+), 150 deletions(-) diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 40a12443d..b4b2907ff 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -1,160 +1,102 @@ -#![warn(clippy::pedantic)] - -use dioxus_core::exports::bumpalo; -use dioxus_core::{LazyNodes, ScopeState, TaskId}; -use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use std::any::Any; +use dioxus_core::{ScopeState, TaskId}; +pub use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use std::future::Future; -use std::{cell::Cell, rc::Rc}; +use std::rc::Rc; /// Maintain a handle over a future that can be paused, resumed, and canceled. /// -/// This is an upgraded form of [`use_future`] with lots of bells-and-whistles. +/// This is an upgraded form of [`use_future`] with an integrated channel system. +/// Specifically, the coroutine generated here comes with an [`UnboundedChannel`] +/// built into it - saving you the hassle of building your own. /// -/// [`use_coroutine`] is well suited for long-running tasks and is very customizable. -/// -/// -/// ## Long running tasks -/// -/// -/// -/// ## One-off tasks -/// -/// -/// ## Cancellation +/// Addititionally, coroutines are automatically injected as shared contexts, so +/// downstream components can tap into a coroutine's channel and send messages +/// into a singular async event loop. /// +/// This makes it effective for apps that need to interact with an event loop or +/// some asynchronous code without thinking too hard about state. /// /// ## Global State -#[allow(clippy::mut_from_ref)] -pub fn use_coroutine(cx: &ScopeState) -> &mut UseCoroutine { +/// +/// Typically, writing apps that handle concurrency properly can be difficult, +/// so the intention of this hook is to make it easy to join and poll async tasks +/// concurrently in a centralized place. You'll find that you can have much better +/// control over your app's state if you centralize your async actions, even under +/// the same concurrent context. This makes it easier to prevent undeseriable +/// states in your UI while various async tasks are already running. +/// +/// This hook is especially powerful when combined with Fermi. We can store important +/// global data in a coroutine, and then access display-level values from the rest +/// of our app through atoms. +/// +/// ## UseCallback instead +/// +/// However, you must plan out your own concurrency and synchronization. If you +/// don't care about actions in your app being synchronized, you can use [`use_callback`] +/// hook to spawn multiple tasks and run them concurrently. +/// +/// ## Example +/// +/// ```rust, ignore +/// enum Action { +/// Start, +/// Stop, +/// } +/// +/// let chat_client = use_coroutine(&cx, |rx: UnboundedReceiver| async move { +/// while let Some(action) = rx.next().await { +/// match action { +/// Action::Start => {} +/// Action::Stop => {}, +/// } +/// } +/// }); +/// +/// +/// cx.render(rsx!{ +/// button { +/// onclick: move |_| chat_client.send(Action::Start), +/// "Start Chat Service" +/// } +/// }) +/// ``` +pub fn use_coroutine(cx: &ScopeState, init: G) -> &CoroutineHandle +where + M: 'static, + G: FnOnce(UnboundedReceiver) -> F, + F: Future + 'static, +{ cx.use_hook(|_| { - // - UseCoroutine { - val: Cell::new(None), - rx: Cell::new(None), - tx: None, - first_run: true, - deps: vec![], - dep_cnt: 0, - needs_regen: false, - auto_start: true, - } + let (tx, rx) = futures_channel::mpsc::unbounded(); + let task = cx.push_future(init(rx)); + cx.provide_context(CoroutineHandle { tx, task }) }) } -pub struct UseCoroutine { - val: Cell>, - rx: Cell>>, - tx: Option>, - first_run: bool, - deps: Vec>, - dep_cnt: usize, - needs_regen: bool, - auto_start: bool, +/// Get a handle to a coroutine higher in the tree +/// +/// See the docs for [`use_coroutine`] for more details. +pub fn use_coroutine_handle(cx: &ScopeState) -> Option<&Rc>> { + cx.use_hook(|_| cx.consume_context::>()) + .as_ref() } -pub enum FutureState<'a, T> { - Pending, - Complete(&'a T), - Regenerating(&'a T), // the old value -} - -impl UseCoroutine { - /// explicitly set the type of the channel used by the coroutine - fn with_channel(&mut self) -> &mut UseCoroutine { - if self.first_run { - // self.provide_context() - } - todo!() - } - - /// explicitly set the type of the channel used by the coroutine - fn with_channel_isolate(&mut self) -> &mut UseCoroutine { - todo!() - } -} - -impl UseCoroutine { - pub fn is_running(&self) -> bool { - false - // self.running.get() - } - - pub fn start(&self) { - // if !self.is_running() { - // if let Some(mut fut) = self.create_fut.take() { - // let fut = fut(); - // let ready_handle = self.running.clone(); - - // let task = self.cx.push_future(async move { - // ready_handle.set(true); - // fut.await; - // ready_handle.set(false); - // }); - - // self.task_id.set(Some(task)); - // } - // } - } - - pub fn send(&self, msg: M) { - if let Some(tx) = self.tx.clone() { - if tx.unbounded_send(msg).is_err() { - log::error!("Failed to send message"); - } - } - } - - // todo: wire these up, either into the task system or into the coroutine system itself - // we would have change how we poll the coroutine and how its awaken - - fn build>(&mut self, f: impl FnOnce(UnboundedReceiver) -> F) -> &Self { - self.first_run = false; - if self.auto_start || self.needs_regen { - // - } - - self - } - - pub fn auto_start(mut self, start: bool) -> Self { - // if start && self.run_count.get() == 1 { - // self.start(); - // } - self - } - - /// Add this value to the dependency list - /// - /// This is a hook and should be called during the initial hook process. - /// It should •not• be called in a conditional. - pub fn with_dep(&mut self, dependency: &F) -> &mut Self { - if let Some(dep) = self.deps.get_mut(self.dep_cnt) { - if let Some(saved_dep) = dep.downcast_mut::() { - if dependency != saved_dep { - *saved_dep = dependency.clone(); - self.needs_regen = true; - } - }; - } else { - self.deps.push(Box::new(dependency.to_owned())); - self.needs_regen = true; - } - - self - } - - pub fn restart_if(&self, f: impl FnOnce() -> bool) -> &Self { - self - } - - // pub fn resume(&self) {} - // pub fn stop(&self) {} - // pub fn restart(&self) {} -} - -pub struct CoroutineContext { +pub struct CoroutineHandle { tx: UnboundedSender, + task: TaskId, +} + +impl CoroutineHandle { + /// Get the ID of this coroutine + #[must_use] + pub fn task_id(&self) -> TaskId { + self.task + } + + /// Send a message to the coroutine + pub fn send(&self, msg: T) { + let _ = self.tx.unbounded_send(msg); + } } #[cfg(test)] @@ -167,15 +109,36 @@ mod tests { use futures_util::StreamExt; fn app(cx: Scope, name: String) -> Element { - let task = use_coroutine(&cx) - .with_dep(&name) - .with_channel::() - .build(|mut rx| async move { - while let Some(msg) = rx.next().await { - println!("got message: {}", msg); - } - }); + let task = use_coroutine(&cx, |mut rx: UnboundedReceiver| async move { + while let Some(msg) = rx.next().await { + println!("got message: {}", msg); + } + }); + + let task2 = use_coroutine(&cx, view_task); + + let task3 = use_coroutine(&cx, |rx| complex_task(rx, 10)); None } + + async fn view_task(mut rx: UnboundedReceiver) { + while let Some(msg) = rx.next().await { + println!("got message: {}", msg); + } + } + + enum Actions { + CloseAll, + OpenAll, + } + + async fn complex_task(mut rx: UnboundedReceiver, name: i32) { + while let Some(msg) = rx.next().await { + match msg { + Actions::CloseAll => todo!(), + Actions::OpenAll => todo!(), + } + } + } } From 3837899a7e7ee9704a32a35f3e5e3a4ffcef9aab Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 21 Feb 2022 23:35:12 -0500 Subject: [PATCH 100/129] fix: remove clutter --- packages/hooks/docs/old.md | 182 ------------------------------------- 1 file changed, 182 deletions(-) delete mode 100644 packages/hooks/docs/old.md diff --git a/packages/hooks/docs/old.md b/packages/hooks/docs/old.md deleted file mode 100644 index 269cd90ed..000000000 --- a/packages/hooks/docs/old.md +++ /dev/null @@ -1,182 +0,0 @@ - -mod use2 { - #![allow(missing_docs)] - - use dioxus_core::{ScopeState, TaskId}; - use std::{ - any::Any, - cell::{Cell, RefCell}, - future::Future, - rc::Rc, - }; - - /// A future that resolves to a value. - /// - /// This runs the future only once - though the future may be regenerated - /// through the [`UseFuture::restart`] method. - /// - /// This is commonly used for components that cannot be rendered until some - /// asynchronous operation has completed. - /// - /// - /// - /// - /// - pub fn use_future<'a>( - // pub fn use_future<'a, T: 'static, F: Future + 'static>( - cx: &'a ScopeState, - ) -> &'a UseFuture<()> { - // let state = cx.use_hook(move |_| UseFuture { - // update: cx.schedule_update(), - // needs_regen: Cell::new(true), - // slot: Rc::new(Cell::new(None)), - // value: None, - // task: None, - // pending: true, - // dep_cont: Cell::new(0), - // deps: RefCell::new(Vec::new()), - // first_time: true, - // }); - - // if let Some(value) = state.slot.take() { - // state.value = Some(value); - // state.task = None; - // } - - // if state.needs_regen.get() { - // // We don't need regen anymore - // state.needs_regen.set(false); - // state.pending = false; - - // // Create the new future - // let fut = new_fut(); - - // // Clone in our cells - // let slot = state.slot.clone(); - // let updater = state.update.clone(); - - // state.task = Some(cx.push_future(async move { - // let res = fut.await; - // slot.set(Some(res)); - // updater(); - // })); - // } - - // state.first_time = false; - - // state - - // new_fut: impl FnOnce() -> F, - - todo!() - } - - pub enum FutureState<'a, T> { - Pending, - Complete(&'a T), - Regenerating(&'a T), // the old value - } - - pub struct UseFuture { - update: Rc, - needs_regen: Cell, - value: Option, - slot: Rc>>, - task: Option, - pending: bool, - deps: RefCell>>, - dep_cont: Cell, - first_time: bool, - } - - impl UseFuture { - pub fn restart(&self) { - self.needs_regen.set(true); - (self.update)(); - } - - // clears the value in the future slot without starting the future over - pub fn clear(&self) -> Option { - (self.update)(); - self.slot.replace(None) - } - - // Manually set the value in the future slot without starting the future over - pub fn set(&self, new_value: T) { - self.slot.set(Some(new_value)); - self.needs_regen.set(true); - (self.update)(); - } - - pub fn value(&self) -> Option<&T> { - self.value.as_ref() - } - - pub fn state(&self) -> FutureState { - // self.value.as_ref() - FutureState::Pending - } - - pub fn build(&self, new_fut: impl FnOnce() -> F) {} - } - - #[test] - fn test_use_future_deps() { - use dioxus_core::prelude::*; - - struct MyProps { - val: String, - name: i32, - } - - fn app(cx: Scope) -> Element { - let MyProps { val, name } = cx.props; - - // let val = use_c(&cx) - // .use_dep(val) - // .restart_if(|| false) - // .use_dep(name) - // .build(|(val, name)| async move {}); - - // async fn fetch_thing(name: String, num: i32) -> String { - // format!("{} {}", name, num) - // } - - // let val = use_future(&cx, || fetch_thing(val.clone(), *name)) - // .with_dep(val) - // .with_dep(name) - // .restart_if(|| false); - - None - } - } - - // pub struct CoroutineBuilder<'a, const PARAM: usize> { - // deps: Vec>, - // cx: &'a ScopeState, - // } - - // macro_rules! dep_impl { - // ($id1:literal to $id2:literal) => { - // impl<'a> CoroutineBuilder<'a, $id1> { - // pub fn use_dep( - // mut self, - // dep: &F, - // ) -> CoroutineBuilder<'a, $id2> { - // self.deps.push(Box::new(dep.clone())); - // unsafe { std::mem::transmute(self) } - // } - // } - // }; - // } - - // dep_impl!(0 to 1); - // dep_impl!(1 to 2); - // dep_impl!(2 to 3); - // dep_impl!(3 to 4); - // dep_impl!(4 to 5); - // dep_impl!(5 to 6); - // dep_impl!(6 to 7); - // dep_impl!(7 to 8); - // dep_impl!(8 to 9); -} From d19ae3ff56eb7e74be282cdbc864bf93bcd762c4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 21 Feb 2022 23:45:01 -0500 Subject: [PATCH 101/129] feat: tweak debug tools to be customizable --- packages/desktop/src/cfg.rs | 8 ++++++++ packages/desktop/src/lib.rs | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index c5d6eef50..161a574b1 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -17,6 +17,7 @@ pub struct DesktopConfig { pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) event_handler: Option>, + pub(crate) disable_context_menu: bool, } pub(crate) type WryProtocol = ( @@ -29,15 +30,22 @@ impl DesktopConfig { #[inline] pub fn new() -> Self { let window = WindowBuilder::new().with_title("Dioxus app"); + Self { event_handler: None, window, protocols: Vec::new(), file_drop_handler: None, pre_rendered: None, + disable_context_menu: !cfg!(debug_assertions), } } + pub fn with_disable_context_menu(&mut self, disable: bool) -> &mut Self { + self.disable_context_menu = disable; + self + } + pub fn with_prerendered(&mut self, content: String) -> &mut Self { self.pre_rendered = Some(content); self diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index e84baa034..268639597 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -172,10 +172,7 @@ pub fn launch_with_props( 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 { + if cfg.disable_context_menu { // in release mode, we don't want to show the dev tool or reload menus webview = webview.with_initialization_script( r#" @@ -192,6 +189,9 @@ pub fn launch_with_props( } "#, ) + } else { + // in debug, we are okay with the reload menu showing and dev tool + webview = webview.with_dev_tool(true); } desktop.webviews.insert(window_id, webview.build().unwrap()); From 4eeb3da8c9eb22cf87d4ff3bc9155e86817d40c8 Mon Sep 17 00:00:00 2001 From: Shuanghui Yan Date: Tue, 22 Feb 2022 23:14:11 +0800 Subject: [PATCH 102/129] fix: add ns param for removeAttribute --- packages/core/src/mutations.rs | 14 ++++++++++++-- packages/interpreter/src/bindings.rs | 2 +- packages/interpreter/src/interpreter.js | 14 ++++++++------ packages/web/src/dom.rs | 4 ++-- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index bd94d0b71..e6c83f92d 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -107,6 +107,7 @@ pub enum DomEdit<'bump> { RemoveAttribute { root: u64, name: &'static str, + ns: Option<&'bump str>, }, } @@ -218,8 +219,17 @@ impl<'a> Mutations<'a> { } pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) { - let name = attribute.name; - self.edits.push(RemoveAttribute { name, root }); + let Attribute { + name, + namespace, + .. + } = attribute; + + self.edits.push(RemoveAttribute { + name, + ns: *namespace, + root + }); } pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) { diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index ecd74870d..aa937728b 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -63,5 +63,5 @@ extern "C" { ); #[wasm_bindgen(method)] - pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str); + pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str, ns: Option<&str>); } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 76da73752..6a4ed0904 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -105,7 +105,7 @@ export class Interpreter { if (ns === "style") { // @ts-ignore node.style[name] = value; - } else if (ns != null || ns != undefined) { + } else if (ns !== null || ns !== undefined) { node.setAttributeNS(ns, name, value); } else { switch (name) { @@ -133,10 +133,12 @@ export class Interpreter { } } } - RemoveAttribute(root, name) { + RemoveAttribute(root, field, ns) { + const name = field; const node = this.nodes[root]; - - if (name === "value") { + if (ns !== null || ns !== undefined) { + node.removeAttributeNS(ns, name); + } else if (name === "value") { node.value = ""; } else if (name === "checked") { node.checked = false; @@ -260,7 +262,7 @@ export class Interpreter { } } - if (realId == null) { + if (realId === null) { return; } window.ipc.postMessage( @@ -281,7 +283,7 @@ export class Interpreter { this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); break; case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); + this.RemoveAttribute(edit.root, edit.name, edit.ns); break; } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index b2eb65d08..3ecfe9913 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -127,8 +127,8 @@ impl WebsysDom { self.interpreter.RemoveEventListener(root, event) } - DomEdit::RemoveAttribute { root, name } => { - self.interpreter.RemoveAttribute(root, name) + DomEdit::RemoveAttribute { root, name, ns } => { + self.interpreter.RemoveAttribute(root, name, ns) } DomEdit::CreateTextNode { text, root } => { From 45231651dd4125b94b6e52ba86f79e68aad6bf99 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 22 Feb 2022 16:34:06 -0500 Subject: [PATCH 103/129] fix: quick patch --- Cargo.toml | 5 +- packages/core-macro/Cargo.toml | 4 +- packages/core-macro/src/lib.rs | 3 +- packages/core-macro/src/rsx/component.rs | 234 +++++++++++++++++++ packages/core-macro/src/rsx/element.rs | 282 +++++++++++++++++++++++ packages/core-macro/src/rsx/mod.rs | 97 ++++++++ packages/core-macro/src/rsx/node.rs | 86 +++++++ packages/core-macro/src/rsx/pretty.rs | 1 + packages/rsx/Cargo.toml | 5 +- 9 files changed, 709 insertions(+), 8 deletions(-) create mode 100644 packages/core-macro/src/rsx/component.rs create mode 100644 packages/core-macro/src/rsx/element.rs create mode 100644 packages/core-macro/src/rsx/mod.rs create mode 100644 packages/core-macro/src/rsx/node.rs create mode 100644 packages/core-macro/src/rsx/pretty.rs diff --git a/Cargo.toml b/Cargo.toml index d4e41af61..d4d3f874e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,8 @@ dioxus-core = { path = "./packages/core", version = "^0.1.9" } dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } -dioxus-rsx = { path = "./packages/rsx", optional = true } fermi = { path = "./packages/fermi", version = "^0.1.0", optional = true } +# dioxus-rsx = { path = "./packages/rsx", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } dioxus-desktop = { path = "./packages/desktop", version = "^0.1.6", optional = true } @@ -31,7 +31,8 @@ dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.0.0", o [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro", "dioxus-rsx"] +macro = ["dioxus-core-macro"] +# macro = ["dioxus-core-macro", "dioxus-rsx"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index ed5d34e53..e6ff29a22 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,8 +15,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] -dioxus-rsx = { path = "../rsx" } -proc-macro-error = "1.0.4" +# dioxus-rsx = { path = "../rsx" } +proc-macro-error = "1" proc-macro2 = { version = "1.0.6" } quote = "1.0" syn = { version = "1.0.11", features = ["full", "extra-traits"] } diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 96ea0ce87..931933386 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -5,6 +5,7 @@ use syn::parse_macro_input; mod ifmt; mod inlineprops; mod props; +mod rsx; #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { @@ -178,7 +179,7 @@ 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::(s) { + match syn::parse::(s) { Err(err) => err.to_compile_error().into(), Ok(stream) => stream.to_token_stream().into(), } diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs new file mode 100644 index 000000000..17006e6ac --- /dev/null +++ b/packages/core-macro/src/rsx/component.rs @@ -0,0 +1,234 @@ +//! Parse components into the VComponent VNode +//! ========================================== +//! +//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the vcomponent format. +//! We can be reasonably sure that whatever enters this parsing path is in the right format. +//! This feature must support +//! - [x] Namespaced components +//! - [x] Fields +//! - [x] Componentbuilder synax +//! - [x] Optional commas +//! - [ ] Children +//! - [ ] Keys +//! - [ ] Properties spreading with with `..` syntax + +use super::*; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + ext::IdentExt, + parse::{Parse, ParseBuffer, ParseStream}, + token, Expr, Ident, LitStr, Result, Token, +}; + +pub struct Component { + pub name: syn::Path, + pub body: Vec, + pub children: Vec, + pub manual_props: Option, +} + +impl Parse for Component { + fn parse(stream: ParseStream) -> Result { + let name = syn::Path::parse_mod_style(stream)?; + + let content: ParseBuffer; + + // if we see a `{` then we have a block + // else parse as a function-like call + if stream.peek(token::Brace) { + syn::braced!(content in stream); + } else { + syn::parenthesized!(content in stream); + } + + let mut body = Vec::new(); + let mut children = Vec::new(); + let mut manual_props = None; + + while !content.is_empty() { + // if we splat into a component then we're merging properties + if content.peek(Token![..]) { + content.parse::()?; + manual_props = Some(content.parse::()?); + } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + body.push(content.parse::()?); + } else { + children.push(content.parse::()?); + } + + if content.peek(Token![,]) { + let _ = content.parse::(); + } + } + + Ok(Self { + name, + body, + children, + manual_props, + }) + } +} + +impl ToTokens for Component { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let name = &self.name; + + let mut has_key = None; + + let builder = match &self.manual_props { + Some(manual_props) => { + let mut toks = quote! { + let mut __manual_props = #manual_props; + }; + for field in &self.body { + if field.name == "key" { + has_key = Some(field); + } else { + let name = &field.name; + let val = &field.content; + toks.append_all(quote! { + __manual_props.#name = #val; + }); + } + } + toks.append_all(quote! { + __manual_props + }); + quote! {{ + #toks + }} + } + None => { + let mut toks = quote! { fc_to_builder(#name) }; + for field in &self.body { + match field.name.to_string().as_str() { + "key" => { + // + has_key = Some(field); + } + _ => toks.append_all(quote! {#field}), + } + } + + if !self.children.is_empty() { + let childs = &self.children; + toks.append_all(quote! { + .children(__cx.create_children([ #( #childs ),* ])) + }); + } + + toks.append_all(quote! { + .build() + }); + toks + } + }; + + let key_token = match has_key { + Some(field) => { + let inners = &field.content; + quote! { Some(format_args_f!(#inners)) } + } + None => quote! { None }, + }; + + let fn_name = self.name.segments.last().unwrap().ident.to_string(); + + tokens.append_all(quote! { + __cx.component( + #name, + #builder, + #key_token, + #fn_name + ) + }) + } +} + +// the struct's fields info +pub struct ComponentField { + name: Ident, + content: ContentField, +} + +enum ContentField { + ManExpr(Expr), + Formatted(LitStr), + OnHandlerRaw(Expr), +} + +impl ToTokens for ContentField { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + ContentField::ManExpr(e) => e.to_tokens(tokens), + ContentField::Formatted(s) => tokens.append_all(quote! { + __cx.raw_text(format_args_f!(#s)).0 + }), + ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { + __cx.event_handler(#e) + }), + } + } +} + +impl Parse for ComponentField { + fn parse(input: ParseStream) -> Result { + let name = Ident::parse_any(input)?; + input.parse::()?; + + if name.to_string().starts_with("on") { + let content = ContentField::OnHandlerRaw(input.parse()?); + return Ok(Self { name, content }); + } + + if name == "key" { + let content = ContentField::ManExpr(input.parse()?); + return Ok(Self { name, content }); + } + + if input.peek(LitStr) && input.peek2(Token![,]) { + let t: LitStr = input.fork().parse()?; + + if is_literal_foramtted(&t) { + let content = ContentField::Formatted(input.parse()?); + return Ok(Self { name, content }); + } + } + + if input.peek(LitStr) && input.peek2(LitStr) { + let item = input.parse::().unwrap(); + proc_macro_error::emit_error!(item, "This attribute is missing a trailing comma") + } + + let content = ContentField::ManExpr(input.parse()?); + Ok(Self { name, content }) + } +} + +impl ToTokens for ComponentField { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ComponentField { name, content, .. } = self; + tokens.append_all(quote! { + .#name(#content) + }) + } +} + +fn is_literal_foramtted(lit: &LitStr) -> bool { + let s = lit.value(); + let mut chars = s.chars(); + + while let Some(next) = chars.next() { + if next == '{' { + let nen = chars.next(); + if nen != Some('{') { + return true; + } + } + } + + false +} diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs new file mode 100644 index 000000000..fa8fc9f51 --- /dev/null +++ b/packages/core-macro/src/rsx/element.rs @@ -0,0 +1,282 @@ +use super::*; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseBuffer, ParseStream}, + Expr, Ident, LitStr, Result, Token, +}; + +// ======================================= +// Parse the VNode::Element type +// ======================================= +pub struct Element { + pub name: Ident, + pub key: Option, + pub attributes: Vec, + pub children: Vec, + pub _is_static: bool, +} + +impl Parse for Element { + fn parse(stream: ParseStream) -> Result { + let el_name = Ident::parse(stream)?; + + // parse the guts + let content: ParseBuffer; + syn::braced!(content in stream); + + let mut attributes: Vec = vec![]; + let mut children: Vec = vec![]; + let mut key = None; + let mut _el_ref = None; + + // parse fields with commas + // break when we don't get this pattern anymore + // start parsing bodynodes + // "def": 456, + // abc: 123, + loop { + // Parse the raw literal fields + if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + let name = content.parse::()?; + let ident = name.clone(); + + content.parse::()?; + + if content.peek(LitStr) && content.peek2(Token![,]) { + let value = content.parse::()?; + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::CustomAttrText { name, value }, + }); + } else { + let value = content.parse::()?; + + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::CustomAttrExpression { name, value }, + }); + } + + if content.is_empty() { + break; + } + + // todo: add a message saying you need to include commas between fields + if content.parse::().is_err() { + proc_macro_error::emit_error!( + ident, + "This attribute is missing a trailing comma" + ) + } + continue; + } + + if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + let name = content.parse::()?; + let ident = name.clone(); + + let name_str = name.to_string(); + content.parse::()?; + + if name_str.starts_with("on") { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventTokens { + name, + tokens: content.parse()?, + }, + }); + } else { + match name_str.as_str() { + "key" => { + key = Some(content.parse()?); + } + "classes" => todo!("custom class list not supported yet"), + // "namespace" => todo!("custom namespace not supported yet"), + "node_ref" => { + _el_ref = Some(content.parse::()?); + } + _ => { + if content.peek(LitStr) { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::AttrText { + name, + value: content.parse()?, + }, + }); + } else { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::AttrExpression { + name, + value: content.parse()?, + }, + }); + } + } + } + } + + if content.is_empty() { + break; + } + + // todo: add a message saying you need to include commas between fields + if content.parse::().is_err() { + proc_macro_error::emit_error!( + ident, + "This attribute is missing a trailing comma" + ) + } + continue; + } + + break; + } + + while !content.is_empty() { + if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { + let ident = content.parse::().unwrap(); + let name = ident.value(); + proc_macro_error::emit_error!( + ident, "This attribute `{}` is in the wrong place.", name; + help = +"All attribute fields must be placed above children elements. + + div { + attr: \"...\", <---- attribute is above children + div { } <---- children are below attributes + }"; + ) + } + + if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) { + let ident = content.parse::().unwrap(); + let name = ident.to_string(); + proc_macro_error::emit_error!( + ident, "This attribute `{}` is in the wrong place.", name; + help = +"All attribute fields must be placed above children elements. + + div { + attr: \"...\", <---- attribute is above children + div { } <---- children are below attributes + }"; + ) + } + + children.push(content.parse::()?); + // consume comma if it exists + // we don't actually care if there *are* commas after elements/text + if content.peek(Token![,]) { + let _ = content.parse::(); + } + } + + Ok(Self { + key, + name: el_name, + attributes, + children, + _is_static: false, + }) + } +} + +impl ToTokens for Element { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let name = &self.name; + let children = &self.children; + + let key = match &self.key { + Some(ty) => quote! { Some(format_args_f!(#ty)) }, + None => quote! { None }, + }; + + let listeners = self + .attributes + .iter() + .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); + + let attr = self + .attributes + .iter() + .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); + + tokens.append_all(quote! { + __cx.element( + dioxus_elements::#name, + __cx.bump().alloc([ #(#listeners),* ]), + __cx.bump().alloc([ #(#attr),* ]), + __cx.bump().alloc([ #(#children),* ]), + #key, + ) + }); + } +} + +pub enum ElementAttr { + /// attribute: "valuee {}" + AttrText { name: Ident, value: LitStr }, + + /// attribute: true, + AttrExpression { name: Ident, value: Expr }, + + /// "attribute": "value {}" + CustomAttrText { name: LitStr, value: LitStr }, + + /// "attribute": true, + CustomAttrExpression { name: LitStr, value: Expr }, + + // /// onclick: move |_| {} + // EventClosure { name: Ident, closure: ExprClosure }, + /// onclick: {} + EventTokens { name: Ident, tokens: Expr }, +} + +pub struct ElementAttrNamed { + pub el_name: Ident, + pub attr: ElementAttr, +} + +impl ToTokens for ElementAttrNamed { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ElementAttrNamed { el_name, attr } = self; + + tokens.append_all(match attr { + ElementAttr::AttrText { name, value } => { + quote! { + dioxus_elements::#el_name.#name(__cx, format_args_f!(#value)) + } + } + ElementAttr::AttrExpression { name, value } => { + quote! { + dioxus_elements::#el_name.#name(__cx, #value) + } + } + ElementAttr::CustomAttrText { name, value } => { + quote! { + __cx.attr( #name, format_args_f!(#value), None, false ) + } + } + ElementAttr::CustomAttrExpression { name, value } => { + quote! { + __cx.attr( #name, format_args_f!(#value), None, false ) + } + } + // ElementAttr::EventClosure { name, closure } => { + // quote! { + // dioxus_elements::on::#name(__cx, #closure) + // } + // } + ElementAttr::EventTokens { name, tokens } => { + quote! { + dioxus_elements::on::#name(__cx, #tokens) + } + } + }); + } +} diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/core-macro/src/rsx/mod.rs new file mode 100644 index 000000000..e20d80dec --- /dev/null +++ b/packages/core-macro/src/rsx/mod.rs @@ -0,0 +1,97 @@ +//! Parse the root tokens in the rsx!{} macro +//! ========================================= +//! +//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing. +//! This feature must support: +//! - [x] Optionally rendering if the `in XYZ` pattern is present +//! - [x] Fragments as top-level element (through ambiguous) +//! - [x] Components as top-level element (through ambiguous) +//! - [x] Tags as top-level elements (through ambiguous) +//! - [x] Good errors if parsing fails +//! +//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. + +mod component; +mod element; +mod node; + +pub mod pretty; + +// Re-export the namespaces into each other +pub use component::*; +pub use element::*; +pub use node::*; + +// imports +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + Ident, Result, Token, +}; + +pub struct CallBody { + pub custom_context: Option, + pub roots: Vec, +} + +impl Parse for CallBody { + fn parse(input: ParseStream) -> Result { + let custom_context = if input.peek(Ident) && input.peek2(Token![,]) { + let name = input.parse::()?; + input.parse::()?; + + Some(name) + } else { + None + }; + + let mut roots = Vec::new(); + + while !input.is_empty() { + let node = input.parse::()?; + + if input.peek(Token![,]) { + let _ = input.parse::(); + } + + roots.push(node); + } + + Ok(Self { + custom_context, + roots, + }) + } +} + +/// Serialize the same way, regardless of flavor +impl ToTokens for CallBody { + fn to_tokens(&self, out_tokens: &mut TokenStream2) { + let inner = if self.roots.len() == 1 { + let inner = &self.roots[0]; + quote! { #inner } + } else { + let childs = &self.roots; + quote! { __cx.fragment_root([ #(#childs),* ]) } + }; + + match &self.custom_context { + // The `in cx` pattern allows directly rendering + Some(ident) => out_tokens.append_all(quote! { + #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + #inner + })) + }), + + // Otherwise we just build the LazyNode wrapper + None => out_tokens.append_all(quote! { + LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + #inner + }) + }), + }; + } +} diff --git a/packages/core-macro/src/rsx/node.rs b/packages/core-macro/src/rsx/node.rs new file mode 100644 index 000000000..eeee421b0 --- /dev/null +++ b/packages/core-macro/src/rsx/node.rs @@ -0,0 +1,86 @@ +use super::*; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + token, Attribute, Expr, LitStr, Result, Token, +}; + +/* +Parse +-> div {} +-> Component {} +-> component() +-> "text {with_args}" +-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr +*/ +pub enum BodyNode { + Element(Element), + Component(Component), + Text(LitStr), + RawExpr(Expr), + Meta(Attribute), +} + +impl Parse for BodyNode { + fn parse(stream: ParseStream) -> Result { + if stream.peek(LitStr) { + return Ok(BodyNode::Text(stream.parse()?)); + } + + // div {} -> el + // Div {} -> comp + if stream.peek(syn::Ident) && stream.peek2(token::Brace) { + if stream + .fork() + .parse::()? + .to_string() + .chars() + .next() + .unwrap() + .is_ascii_uppercase() + { + return Ok(BodyNode::Component(stream.parse()?)); + } else { + return Ok(BodyNode::Element(stream.parse::()?)); + } + } + + // component() -> comp + // ::component {} -> comp + // ::component () -> comp + if (stream.peek(syn::Ident) && stream.peek2(token::Paren)) + || (stream.peek(Token![::])) + || (stream.peek(Token![:]) && stream.peek2(Token![:])) + { + return Ok(BodyNode::Component(stream.parse::()?)); + } + + // crate::component{} -> comp + // crate::component() -> comp + if let Ok(pat) = stream.fork().parse::() { + if pat.segments.len() > 1 { + return Ok(BodyNode::Component(stream.parse::()?)); + } + } + + Ok(BodyNode::RawExpr(stream.parse::()?)) + } +} + +impl ToTokens for BodyNode { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match &self { + BodyNode::Element(el) => el.to_tokens(tokens), + BodyNode::Component(comp) => comp.to_tokens(tokens), + BodyNode::Text(txt) => tokens.append_all(quote! { + __cx.text(format_args_f!(#txt)) + }), + BodyNode::RawExpr(exp) => tokens.append_all(quote! { + __cx.fragment_from_iter(#exp) + }), + BodyNode::Meta(_) => {} + } + } +} diff --git a/packages/core-macro/src/rsx/pretty.rs b/packages/core-macro/src/rsx/pretty.rs new file mode 100644 index 000000000..26ad54006 --- /dev/null +++ b/packages/core-macro/src/rsx/pretty.rs @@ -0,0 +1 @@ +//! pretty printer for rsx! diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml index 4cad9064c..0d84ba250 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -1,13 +1,12 @@ [package] name = "dioxus-rsx" -version = "0.0.0" +version = "0.1.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-macro-error = "1" proc-macro2 = { version = "1.0.6" } quote = "1.0" syn = { version = "1.0.11", features = ["full", "extra-traits"] } From 6b175225077f72c86b32c33048b5c7a7223d1b68 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 22 Feb 2022 21:36:59 -0500 Subject: [PATCH 104/129] feat: textarea example --- examples/textarea.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 examples/textarea.rs diff --git a/examples/textarea.rs b/examples/textarea.rs new file mode 100644 index 000000000..5988149ba --- /dev/null +++ b/examples/textarea.rs @@ -0,0 +1,23 @@ +// How to use textareas + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let (model, set_model) = use_state(&cx, || String::from("asd")); + + println!("{}", model); + + cx.render(rsx! { + textarea { + class: "border", + rows: "10", + cols: "80", + value: "{model}", + oninput: move |e| set_model(e.value.clone()), + } + }) +} From 92573d67a1c9f2cd553b49796a7150cfcc1b90af Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 08:09:16 -0500 Subject: [PATCH 105/129] fix: keyed diffing accidentally pushing every node --- packages/core/src/diff.rs | 23 +++++++---------------- packages/interpreter/src/interpreter.js | 2 -- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index d7189a710..50d5d8874 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -853,7 +853,7 @@ impl<'b> DiffState<'b> { nodes_created += self.create_node(new_node); } else { self.diff_node(&old[old_index], new_node); - nodes_created += self.push_all_nodes(new_node); + nodes_created += self.push_all_real_nodes(new_node); } } @@ -876,7 +876,7 @@ impl<'b> DiffState<'b> { nodes_created += self.create_node(new_node); } else { self.diff_node(&old[old_index], new_node); - nodes_created += self.push_all_nodes(new_node); + nodes_created += self.push_all_real_nodes(new_node); } } @@ -899,7 +899,7 @@ impl<'b> DiffState<'b> { nodes_created += self.create_node(new_node); } else { self.diff_node(&old[old_index], new_node); - nodes_created += self.push_all_nodes(new_node); + nodes_created += self.push_all_real_nodes(new_node); } } @@ -1100,9 +1100,9 @@ impl<'b> DiffState<'b> { } // recursively push all the nodes of a tree onto the stack and return how many are there - fn push_all_nodes(&mut self, node: &'b VNode<'b>) -> usize { + fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize { match node { - VNode::Text(_) | VNode::Placeholder(_) => { + VNode::Text(_) | VNode::Placeholder(_) | VNode::Element(_) => { self.mutations.push_root(node.mounted_id()); 1 } @@ -1110,7 +1110,7 @@ impl<'b> DiffState<'b> { VNode::Fragment(frag) => { let mut added = 0; for child in frag.children { - added += self.push_all_nodes(child); + added += self.push_all_real_nodes(child); } added } @@ -1118,16 +1118,7 @@ impl<'b> DiffState<'b> { VNode::Component(c) => { let scope_id = c.scope.get().unwrap(); let root = self.scopes.root_node(scope_id); - self.push_all_nodes(root) - } - - VNode::Element(el) => { - let mut num_on_stack = 0; - for child in el.children.iter() { - num_on_stack += self.push_all_nodes(child); - } - self.mutations.push_root(el.id.get().unwrap()); - num_on_stack + 1 + self.push_all_real_nodes(root) } } } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 76da73752..10829f55b 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -53,14 +53,12 @@ export class Interpreter { } } CreateTextNode(text, root) { - // todo: make it so the types are okay const node = document.createTextNode(text); this.nodes[root] = node; this.stack.push(node); } CreateElement(tag, root) { const el = document.createElement(tag); - // el.setAttribute("data-dioxus-id", `${root}`); this.nodes[root] = el; this.stack.push(el); } From 07bea88fcc51d0c1285683e21453800c4428fd59 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 08:44:16 -0500 Subject: [PATCH 106/129] fix: login example --- examples/login_form.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index 7fd91262d..0a0c77e87 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -12,7 +12,7 @@ fn app(cx: Scope) -> Element { let onsubmit = move |evt: FormEvent| { cx.spawn(async move { let resp = reqwest::Client::new() - .post("http://localhost/login") + .post("http://localhost:8080/login") .form(&[ ("username", &evt.values["username"]), ("password", &evt.values["password"]), @@ -22,10 +22,12 @@ fn app(cx: Scope) -> Element { match resp { // Parse data from here, such as storing a response token - Ok(_data) => println!("Login successful"), + Ok(_data) => println!("Login successful!"), //Handle any errors from the fetch here - Err(_err) => println!("Login failed"), + Err(_err) => { + println!("Login failed - you need a login server running on localhost:8080.") + } } }); }; @@ -36,10 +38,10 @@ fn app(cx: Scope) -> Element { onsubmit: onsubmit, prevent_default: "onsubmit", // Prevent the default behavior of to post - input { "type": "text" } + input { "type": "text", id: "username", name: "username" } label { "Username" } br {} - input { "type": "password" } + input { "type": "password", id: "password", name: "password" } label { "Password" } br {} button { "Login" } From 7a5c403e381ab7353e44257bc790dc68ed01a8ed Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 09:47:53 -0500 Subject: [PATCH 107/129] wip: add use_callback --- packages/hooks/src/lib.rs | 4 +- packages/hooks/src/usecallback.rs | 68 +++++++++++++++++++++++++++++++ packages/hooks/src/usefuture.rs | 22 ++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 packages/hooks/src/usecallback.rs diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index cb4afd297..598442e19 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -13,8 +13,8 @@ pub use usecoroutine::*; mod usefuture; pub use usefuture::*; -mod usesuspense; -pub use usesuspense::*; +mod usecallback; +pub use usecallback::*; #[macro_export] /// A helper macro for using hooks in async environements. diff --git a/packages/hooks/src/usecallback.rs b/packages/hooks/src/usecallback.rs new file mode 100644 index 000000000..1affd323f --- /dev/null +++ b/packages/hooks/src/usecallback.rs @@ -0,0 +1,68 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::use_state; +use crate::UseFutureDep; +use dioxus_core::{ScopeState, UiEvent}; +use std::future::Future; + +pub fn use_callback + 'static>( + cx: &ScopeState, + // + g: G, + f: impl FnMut(I, G::Out) -> F, +) -> &UseCallback +where + G::Out: 'static, + I: 'static, +{ + cx.use_hook(|_| { + // + UseCallback { + f: todo!(), + f2: Box::new(|f| {}), + } + }) +} + +pub struct UseCallback { + f: Rc>>>, + f2: Box, // f: Rc>>>, +} + +impl std::ops::Deref for UseCallback { + type Target = dyn Fn(I); + + fn deref(&self) -> &Self::Target { + &self.f2 + } +} + +trait MyThing {} +impl MyThing for Box {} +impl MyThing for Box {} + +#[test] +fn demo() { + use dioxus_core::prelude::*; + fn example(cx: Scope) -> Element { + let (name, _) = use_state(&cx, || 0); + let (age, _) = use_state(&cx, || 0); + + let onsubmit = use_callback(&cx, (name,), |event: (), (name,)| async move { + // + }); + + let onsubmit = use_callback(&cx, (name,), my_callback); + async fn my_callback(event: UiEvent<()>, name: (i32,)) { + // + } + + let onsubmit = use_callback(&cx, name, my_callback2); + + async fn my_callback2(event: UiEvent<()>, name: i32) { + // + } + + None + } +} diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 9db67aef2..0c066363e 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -146,6 +146,28 @@ impl UseFutureDep for () { pub trait Dep: 'static + PartialEq + Clone {} impl Dep for T where T: 'static + PartialEq + Clone {} +impl UseFutureDep for &A { + type Out = A; + fn out(&self) -> Self::Out { + (*self).clone() + } + fn apply(self, state: &mut Vec>) -> bool { + match state.get_mut(0).map(|f| f.downcast_mut::()).flatten() { + Some(val) => { + if *val != *self { + *val = self.clone(); + return true; + } + } + None => { + state.push(Box::new(self.clone())); + return true; + } + } + false + } +} + macro_rules! impl_dep { ( $($el:ident=$name:ident,)* From bf3370f0ceb2075b306a9a9708d835da9077ce31 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 09:48:58 -0500 Subject: [PATCH 108/129] readme: clarify comparison --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 23cd9c516..bac0cb240 100644 --- a/README.md +++ b/README.md @@ -160,9 +160,9 @@ You shouldn't use Dioxus if: ## Comparison with other Rust UI frameworks Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**. -- [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead of React-hooks, no borrowed props, supports SSR (no hydration). +- [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead, no borrowed props, supports SSR (no hydration), no direct desktop/mobile support. - [Percy](https://github.com/chinedufn/percy): Supports SSR but with less emphasis on state management and event handling. -- [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but lacking in ergonomics. +- [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but no direct support for desktop/mobile. - [Dominator](https://github.com/Pauan/rust-dominator): Signal-based zero-cost alternative, less emphasis on community and docs. - [Azul](https://azul.rs): Fully native HTML/CSS renderer for desktop applications, no support for web/ssr From 1f4d2cd6e4957e9955c0c9f2126a0a0ab3dce7b5 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 11:26:31 -0500 Subject: [PATCH 109/129] ci: fix ci on master pushes --- .github/workflows/macos.yml | 11 +++++++++++ .github/workflows/main.yml | 11 +++++++++++ .github/workflows/windows.yml | 11 +++++++++++ 3 files changed, 33 insertions(+) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 718922a57..6ff2b1e9c 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,6 +1,17 @@ name: macOS tests on: + push: + branches: + - master + paths: + - packages/** + - examples/** + - src/** + - .github/** + - lib.rs + - Cargo.toml + pull_request: types: [opened, synchronize, reopened, ready_for_review] branches: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 49ed00901..27116f17d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,17 @@ name: Rust CI on: + push: + branches: + - master + paths: + - packages/** + - examples/** + - src/** + - .github/** + - lib.rs + - Cargo.toml + pull_request: types: [opened, synchronize, reopened, ready_for_review] branches: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 75953807a..961adee97 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,6 +1,17 @@ name: windows on: + push: + branches: + - master + paths: + - packages/** + - examples/** + - src/** + - .github/** + - lib.rs + - Cargo.toml + pull_request: types: [opened, synchronize, reopened, ready_for_review] branches: From 03feaa446af94894761b4bddf632a312f83c357a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 11:37:28 -0500 Subject: [PATCH 110/129] fix: make clippy happy --- packages/core-macro/src/rsx/node.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core-macro/src/rsx/node.rs b/packages/core-macro/src/rsx/node.rs index eeee421b0..d5d701767 100644 --- a/packages/core-macro/src/rsx/node.rs +++ b/packages/core-macro/src/rsx/node.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseStream}, - token, Attribute, Expr, LitStr, Result, Token, + token, Expr, LitStr, Result, Token, }; /* @@ -20,7 +20,6 @@ pub enum BodyNode { Component(Component), Text(LitStr), RawExpr(Expr), - Meta(Attribute), } impl Parse for BodyNode { @@ -80,7 +79,6 @@ impl ToTokens for BodyNode { BodyNode::RawExpr(exp) => tokens.append_all(quote! { __cx.fragment_from_iter(#exp) }), - BodyNode::Meta(_) => {} } } } From bda4a71a7277fb86612e1b6fd260ec619c91b81b Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Wed, 23 Feb 2022 19:47:17 +0100 Subject: [PATCH 111/129] feat: change schedule_update's Rc to an Arc BREAKING CHANGE --- packages/core/src/scopes.rs | 10 +++++----- packages/fermi/src/root.rs | 6 +++--- packages/hooks/src/use_shared_state.rs | 4 ++-- packages/hooks/src/usefuture.rs | 4 ++-- packages/hooks/src/useref.rs | 4 ++-- packages/hooks/src/usestate.rs | 4 ++-- packages/router/src/service.rs | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 00c3024bf..c6ba5b66a 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -10,7 +10,7 @@ use std::{ collections::HashMap, future::Future, pin::Pin, - rc::Rc, + rc::Rc, sync::Arc, }; /// for traceability, we use the raw fn pointer to identify the function @@ -582,9 +582,9 @@ impl ScopeState { /// Create a subscription that schedules a future render for the reference component /// /// ## Notice: you should prefer using prepare_update and get_scope_id - pub fn schedule_update(&self) -> Rc { + pub fn schedule_update(&self) -> Arc { let (chan, id) = (self.tasks.sender.clone(), self.scope_id()); - Rc::new(move || { + Arc::new(move || { let _ = chan.unbounded_send(SchedulerMsg::Immediate(id)); }) } @@ -594,9 +594,9 @@ impl ScopeState { /// A component's ScopeId can be obtained from `use_hook` or the [`ScopeState::scope_id`] method. /// /// This method should be used when you want to schedule an update for a component - pub fn schedule_update_any(&self) -> Rc { + pub fn schedule_update_any(&self) -> Arc { let chan = self.tasks.sender.clone(); - Rc::new(move |id| { + Arc::new(move |id| { let _ = chan.unbounded_send(SchedulerMsg::Immediate(id)); }) } diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs index 5c07083be..d80b176dc 100644 --- a/packages/fermi/src/root.rs +++ b/packages/fermi/src/root.rs @@ -1,4 +1,4 @@ -use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc}; +use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; use dioxus_core::ScopeId; use im_rc::HashSet; @@ -9,7 +9,7 @@ pub type AtomId = *const (); pub struct AtomRoot { pub atoms: RefCell>, - pub update_any: Rc, + pub update_any: Arc, } pub struct Slot { @@ -18,7 +18,7 @@ pub struct Slot { } impl AtomRoot { - pub fn new(update_any: Rc) -> Self { + pub fn new(update_any: Arc) -> Self { Self { update_any, atoms: RefCell::new(HashMap::new()), diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index a05e99d7d..2f3f201a2 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -2,7 +2,7 @@ use dioxus_core::{ScopeId, ScopeState}; use std::{ cell::{Cell, Ref, RefCell, RefMut}, collections::HashSet, - rc::Rc, + rc::Rc, sync::Arc, }; type ProvidedState = RefCell>; @@ -10,7 +10,7 @@ type ProvidedState = RefCell>; // Tracks all the subscribers to a shared State pub struct ProvidedStateInner { value: Rc>, - notify_any: Rc, + notify_any: Arc, consumers: HashSet, } diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 58f350124..e1d57eec5 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -1,5 +1,5 @@ use dioxus_core::{ScopeState, TaskId}; -use std::{cell::Cell, future::Future, rc::Rc}; +use std::{cell::Cell, future::Future, rc::Rc, sync::Arc}; pub fn use_future<'a, T: 'static, F: Future + 'static>( cx: &'a ScopeState, @@ -43,7 +43,7 @@ pub fn use_future<'a, T: 'static, F: Future + 'static>( } pub struct UseFuture { - update: Rc, + update: Arc, needs_regen: Cell, value: Option, slot: Rc>>, diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index c2537e079..7441f85b0 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -1,7 +1,7 @@ use dioxus_core::ScopeState; use std::{ cell::{Ref, RefCell, RefMut}, - rc::Rc, + rc::Rc, sync::Arc, }; /// `use_ref` is a key foundational hook for storing state in Dioxus. @@ -121,7 +121,7 @@ pub fn use_ref<'a, T: 'static>( /// A type created by the [`use_ref`] hook. See its documentation for more details. pub struct UseRef { - update: Rc, + update: Arc, value: Rc>, } diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 7e8892be9..e0d40b993 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -4,7 +4,7 @@ use dioxus_core::prelude::*; use std::{ cell::{RefCell, RefMut}, fmt::{Debug, Display}, - rc::Rc, + rc::Rc, sync::Arc, }; /// Store state between component renders. @@ -69,7 +69,7 @@ pub fn use_state<'a, T: 'static>( pub struct UseState { pub(crate) current_val: Rc, - pub(crate) update_callback: Rc, + pub(crate) update_callback: Arc, pub(crate) setter: Rc, pub(crate) slot: Rc>>, } diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index a4ab4dc0d..5487bcc24 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -2,7 +2,7 @@ use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ cell::{Cell, Ref, RefCell}, collections::{HashMap, HashSet}, - rc::Rc, + rc::Rc, sync::Arc, }; use dioxus_core::ScopeId; @@ -10,7 +10,7 @@ use dioxus_core::ScopeId; use crate::platform::RouterProvider; pub struct RouterService { - pub(crate) regen_route: Rc, + pub(crate) regen_route: Arc, pub(crate) pending_events: Rc>>, slots: Rc>>, onchange_listeners: Rc>>, @@ -42,7 +42,7 @@ enum RouteSlot { } impl RouterService { - pub fn new(regen_route: Rc, root_scope: ScopeId) -> Self { + pub fn new(regen_route: Arc, root_scope: ScopeId) -> Self { let history = BrowserHistory::default(); let location = history.location(); let path = location.path(); From ff3d772756777eac59c8c628043e25fcb82ce6e2 Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Wed, 23 Feb 2022 19:47:38 +0100 Subject: [PATCH 112/129] feat: add a method to access the scheduler --- packages/core/src/scopes.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index c6ba5b66a..92bee79a6 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -579,6 +579,11 @@ impl ScopeState { self.our_arena_idx } + /// Get a handle to the raw update scheduler channel + pub fn scheduler_channel(&self) -> UnboundedSender { + self.tasks.sender.clone() + } + /// Create a subscription that schedules a future render for the reference component /// /// ## Notice: you should prefer using prepare_update and get_scope_id From 4ab54274ac4923a03e54823a0b5964e5cbffaab1 Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Wed, 23 Feb 2022 19:48:14 +0100 Subject: [PATCH 113/129] style: reformat... --- packages/core/src/scopes.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 92bee79a6..f4cdbdbd7 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -10,7 +10,8 @@ use std::{ collections::HashMap, future::Future, pin::Pin, - rc::Rc, sync::Arc, + rc::Rc, + sync::Arc, }; /// for traceability, we use the raw fn pointer to identify the function From 24743e44e92def5384b2a8e1f9b5bcac7bfc00ca Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 13:53:45 -0500 Subject: [PATCH 114/129] feat: canoncialize assets for macOS --- packages/desktop/Cargo.toml | 5 ++++ packages/desktop/src/protocol.rs | 41 ++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 6fd277ba4..f90d00000 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -29,6 +29,11 @@ dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } +dunce = "1.0.2" + + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "0.9.3" [features] diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index c8f007c1d..6cac600f2 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use wry::{ http::{status::StatusCode, Request, Response, ResponseBuilder}, Result, @@ -21,8 +21,13 @@ pub(super) fn desktop_handler(request: &Request) -> Result { .mimetype("text/javascript") .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec()) } else { + // the path of the asset specified without any relative paths let path_buf = Path::new(trimmed).canonicalize()?; - let cur_path = Path::new(".").canonicalize()?; + + // the current path of the bundle + let cur_path = get_asset_root() + .unwrap_or_else(|| Path::new(".").to_path_buf()) + .canonicalize()?; if !path_buf.starts_with(cur_path) { return ResponseBuilder::new() @@ -45,3 +50,35 @@ pub(super) fn desktop_handler(request: &Request) -> Result { ResponseBuilder::new().mimetype(&meta).body(data) } } + +#[allow(unreachable_code)] +fn get_asset_root() -> Option { + /* + We're matching exactly how cargo-bundle works. + + - [x] macOS + - [ ] Windows + - [ ] Linux (rpm) + - [ ] Linux (deb) + - [ ] iOS + - [ ] Android + + */ + + if std::env::var_os("CARGO").is_some() { + return None; + } + + // TODO: support for other platforms + #[cfg(target_os = "macos")] + { + let bundle = core_foundation::bundle::CFBundle::main_bundle(); + let bundle_path = bundle.path()?; + let resources_path = bundle.resources_path()?; + let absolute_resources_root = bundle_path.join(resources_path); + let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?; + return Some(canonical_resources_root); + } + + None +} From 916dc6440265b10db159adf7e0f5a989dd789817 Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Wed, 23 Feb 2022 20:00:01 +0100 Subject: [PATCH 115/129] style: format againkjdfakbjfbkasjd --- packages/hooks/src/use_shared_state.rs | 3 ++- packages/hooks/src/useref.rs | 3 ++- packages/hooks/src/usestate.rs | 3 ++- packages/router/src/service.rs | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 2f3f201a2..3672302e9 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -2,7 +2,8 @@ use dioxus_core::{ScopeId, ScopeState}; use std::{ cell::{Cell, Ref, RefCell, RefMut}, collections::HashSet, - rc::Rc, sync::Arc, + rc::Rc, + sync::Arc, }; type ProvidedState = RefCell>; diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index 7441f85b0..6ab25cbd4 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -1,7 +1,8 @@ use dioxus_core::ScopeState; use std::{ cell::{Ref, RefCell, RefMut}, - rc::Rc, sync::Arc, + rc::Rc, + sync::Arc, }; /// `use_ref` is a key foundational hook for storing state in Dioxus. diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index e0d40b993..7dd6bfea4 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -4,7 +4,8 @@ use dioxus_core::prelude::*; use std::{ cell::{RefCell, RefMut}, fmt::{Debug, Display}, - rc::Rc, sync::Arc, + rc::Rc, + sync::Arc, }; /// Store state between component renders. diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 5487bcc24..ad45083ed 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -2,7 +2,8 @@ use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ cell::{Cell, Ref, RefCell}, collections::{HashMap, HashSet}, - rc::Rc, sync::Arc, + rc::Rc, + sync::Arc, }; use dioxus_core::ScopeId; From 144e0bd91fd35dc1a5b34524878282a51edb8a3e Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Wed, 23 Feb 2022 20:01:09 +0100 Subject: [PATCH 116/129] feat: add send + sync for safety --- packages/core/src/scopes.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index f4cdbdbd7..652371bfc 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -588,7 +588,7 @@ impl ScopeState { /// Create a subscription that schedules a future render for the reference component /// /// ## Notice: you should prefer using prepare_update and get_scope_id - pub fn schedule_update(&self) -> Arc { + pub fn schedule_update(&self) -> Arc { let (chan, id) = (self.tasks.sender.clone(), self.scope_id()); Arc::new(move || { let _ = chan.unbounded_send(SchedulerMsg::Immediate(id)); @@ -600,7 +600,7 @@ impl ScopeState { /// A component's ScopeId can be obtained from `use_hook` or the [`ScopeState::scope_id`] method. /// /// This method should be used when you want to schedule an update for a component - pub fn schedule_update_any(&self) -> Arc { + pub fn schedule_update_any(&self) -> Arc { let chan = self.tasks.sender.clone(); Arc::new(move |id| { let _ = chan.unbounded_send(SchedulerMsg::Immediate(id)); From c12bcd8150371ff7655ffa8b1b1ec80eecd9a777 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 14:19:00 -0500 Subject: [PATCH 117/129] fix: protocol works on in both cargo and bundle This commit enables assets to be served locally through cargo run or through a bundle strategy. We use cargo-bundle's bundle strategy. This PR checks only for macOS targets. --- packages/desktop/src/protocol.rs | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 6cac600f2..b7daba550 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -21,30 +21,25 @@ pub(super) fn desktop_handler(request: &Request) -> Result { .mimetype("text/javascript") .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec()) } else { - // the path of the asset specified without any relative paths - let path_buf = Path::new(trimmed).canonicalize()?; + let asset_root = get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf()); + let asset = asset_root.join(trimmed).canonicalize()?; - // the current path of the bundle - let cur_path = get_asset_root() - .unwrap_or_else(|| Path::new(".").to_path_buf()) - .canonicalize()?; - - if !path_buf.starts_with(cur_path) { + if !asset.starts_with(asset_root) { return ResponseBuilder::new() .status(StatusCode::FORBIDDEN) .body(String::from("Forbidden").into_bytes()); } - if !path_buf.exists() { + if !asset.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(); + let mime = mime_guess::from_path(&asset).first_or_octet_stream(); // do not let path searching to go two layers beyond the caller level - let data = std::fs::read(path_buf)?; + let data = std::fs::read(asset)?; let meta = format!("{}", mime); ResponseBuilder::new().mimetype(&meta).body(data) @@ -73,10 +68,11 @@ fn get_asset_root() -> Option { #[cfg(target_os = "macos")] { let bundle = core_foundation::bundle::CFBundle::main_bundle(); - let bundle_path = bundle.path()?; - let resources_path = bundle.resources_path()?; - let absolute_resources_root = bundle_path.join(resources_path); - let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?; + let bundle_path = dbg!(bundle.path()?); + let resources_path = dbg!(bundle.resources_path()?); + let absolute_resources_root = dbg!(bundle_path.join(resources_path)); + let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?); + return Some(canonical_resources_root); } From ccb3aa7977c016546bb310ca3f86d26a521ec223 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 15:11:57 -0500 Subject: [PATCH 118/129] feat: allow custom root directories --- packages/desktop/Cargo.toml | 3 --- packages/desktop/src/cfg.rs | 12 ++++++++++++ packages/desktop/src/lib.rs | 7 ++++++- packages/desktop/src/protocol.rs | 6 ++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index f90d00000..017d399c1 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -30,12 +30,9 @@ webbrowser = "0.5.5" mime_guess = "2.0.3" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } dunce = "1.0.2" - - [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9.3" - [features] default = ["tokio_runtime"] tokio_runtime = ["tokio"] diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 161a574b1..6028706bc 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use wry::application::window::Icon; use wry::{ application::{ @@ -18,6 +20,7 @@ pub struct DesktopConfig { pub(crate) pre_rendered: Option, pub(crate) event_handler: Option>, pub(crate) disable_context_menu: bool, + pub(crate) resource_dir: Option, } pub(crate) type WryProtocol = ( @@ -38,14 +41,23 @@ impl DesktopConfig { file_drop_handler: None, pre_rendered: None, disable_context_menu: !cfg!(debug_assertions), + resource_dir: None, } } + /// set the directory from which assets will be searched in release mode + pub fn with_resource_directory(mut self, path: impl Into) -> Self { + self.resource_dir = Some(path.into()); + self + } + + /// Set whether or not the right-click context menu should be disabled. pub fn with_disable_context_menu(&mut self, disable: bool) -> &mut Self { self.disable_context_menu = disable; self } + /// With pre-rendered HTML content pub fn with_prerendered(&mut self, content: String) -> &mut Self { self.pre_rendered = Some(content); self diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 268639597..5ddb0583a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -122,8 +122,11 @@ pub fn launch_with_props( let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone()); let proxy = proxy.clone(); + let file_handler = cfg.file_drop_handler.take(); + let resource_dir = cfg.resource_dir.clone(); + let mut webview = WebViewBuilder::new(window) .unwrap() .with_transparent(cfg.window.window.transparent) @@ -160,7 +163,9 @@ pub fn launch_with_props( log::warn!("invalid IPC message received"); }); }) - .with_custom_protocol(String::from("dioxus"), protocol::desktop_handler) + .with_custom_protocol(String::from("dioxus"), move |r| { + protocol::desktop_handler(r, resource_dir.clone()) + }) .with_file_drop_handler(move |window, evet| { file_handler .as_ref() diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index b7daba550..9422ec157 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -4,7 +4,7 @@ use wry::{ Result, }; -pub(super) fn desktop_handler(request: &Request) -> Result { +pub(super) fn desktop_handler(request: &Request, asset_root: Option) -> Result { // 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://", ""); @@ -21,7 +21,9 @@ pub(super) fn desktop_handler(request: &Request) -> Result { .mimetype("text/javascript") .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec()) } else { - let asset_root = get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf()); + let asset_root = asset_root + .unwrap_or_else(|| get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf())); + let asset = asset_root.join(trimmed).canonicalize()?; if !asset.starts_with(asset_root) { From 8d565529085007478f74e8bdd895708083d68ecd Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 18:17:28 -0500 Subject: [PATCH 119/129] fix: focusout issue on todomvc --- examples/todomvc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 937d6992c..f0109eb4b 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -129,7 +129,6 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { label { r#for: "cbg-{todo.id}", onclick: move |_| set_is_editing(true), - onfocusout: move |_| set_is_editing(false), "{todo.contents}" } } @@ -139,6 +138,7 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { value: "{todo.contents}", oninput: move |evt| cx.props.set_todos.make_mut()[&cx.props.id].contents = evt.value.clone(), autofocus: "true", + onfocusout: move |_| set_is_editing(false), onkeydown: move |evt| { match evt.key.as_str() { "Enter" | "Escape" | "Tab" => set_is_editing(false), From f005ce20ac2dbdf45b63d698078a6cdace58fb1a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 19:32:08 -0500 Subject: [PATCH 120/129] docs: remove tagline --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index bac0cb240..4ff7c0bed 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@

From aa2d6b8d1148a5eaabdfa9f5044c8e8bc493e923 Mon Sep 17 00:00:00 2001 From: Shuanghui Yan Date: Thu, 24 Feb 2022 18:13:00 +0800 Subject: [PATCH 121/129] fix: format styles --- packages/core/src/mutations.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index e6c83f92d..de9656ffd 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -220,15 +220,13 @@ impl<'a> Mutations<'a> { pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) { let Attribute { - name, - namespace, - .. + name, namespace, .. } = attribute; - self.edits.push(RemoveAttribute { + self.edits.push(RemoveAttribute { name, ns: *namespace, - root + root, }); } From cc0e2c487789587149e766be6e8eabefdd7fc385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Kurlbaum?= Date: Thu, 24 Feb 2022 14:32:43 +0100 Subject: [PATCH 122/129] Add options for compilation on systems that lack libappindicator3 --- Cargo.toml | 1 + docs/guide/src/hello_world.md | 9 +++++++-- docs/guide/src/setup.md | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d4d3f874e..26ced24cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ html = ["dioxus-html"] ssr = ["dioxus-ssr"] web = ["dioxus-web"] desktop = ["dioxus-desktop"] +ayatana = ["dioxus-desktop/ayatana"] router = ["dioxus-router"] [workspace] diff --git a/docs/guide/src/hello_world.md b/docs/guide/src/hello_world.md index 32cdab4a9..6bcc77e30 100644 --- a/docs/guide/src/hello_world.md +++ b/docs/guide/src/hello_world.md @@ -81,6 +81,13 @@ $ cargo add dioxus --features desktop It's very important to add `dioxus` with the `desktop` feature for this example. The `dioxus` crate is a batteries-include crate that combines a bunch of utility crates together, ensuring compatibility of the most important parts of the ecosystem. Under the hood, the `dioxus` crate configures various renderers, hooks, debug tooling, and more. The `desktop` feature ensures the we only depend on the smallest set of required crates to compile and render. +If you system does not provide the `libappindicator3` library, like Debian/bullseye, you can enable the replacement `ayatana` with an additional flag: + +```shell +$ # On Debian/bullseye use: +$ cargo add dioxus --features desktop --features ayatana +``` + If you plan to develop extensions for the `Dioxus` ecosystem, please use the `dioxus` crate with the `core` feature to limit the amount of dependencies your project brings in. ### Our first app @@ -143,5 +150,3 @@ For now, just know that `Scope` lets you store state with hooks and render eleme ## Moving on Congrats! You've built your first desktop application with Dioxus. Next, we're going to learn about the basics of building interactive user interfaces. - - diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 75cfc6606..1bbcec213 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -52,6 +52,12 @@ Webview Linux apps require WebkitGtk. When distributing, this can be part of you sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev ``` +When using Debian/bullseye `libappindicator3-dev` is no longer available but replaced by `libayatana-appindicator3-dev`. + +``` +# on Debian/bullseye use: +sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatanta-appindicator3-dev +``` If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/en/docs/get-started/setup-linux). From 3e474382c6cce63bcabf308d5ff196bad088271f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 25 Feb 2022 00:35:25 -0500 Subject: [PATCH 123/129] fix: clippy new lint --- packages/core/src/scopes.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 652371bfc..b1dd41bca 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -97,11 +97,10 @@ impl ScopeArena { // Get the height of the scope let height = parent_scope - .map(|id| self.get_scope(id).map(|scope| scope.height + 1)) - .flatten() + .and_then(|id| self.get_scope(id).map(|scope| scope.height + 1)) .unwrap_or_default(); - let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten(); + let parent_scope = parent_scope.and_then(|f| self.get_scope_raw(f)); /* This scopearena aggressively reuses old scopes when possible. @@ -659,8 +658,7 @@ impl ScopeState { self.shared_contexts .borrow_mut() .insert(TypeId::of::(), value.clone()) - .map(|f| f.downcast::().ok()) - .flatten(); + .and_then(|f| f.downcast::().ok()); value } @@ -690,8 +688,7 @@ impl ScopeState { self.shared_contexts .borrow_mut() .insert(TypeId::of::(), value.clone()) - .map(|f| f.downcast::().ok()) - .flatten(); + .and_then(|f| f.downcast::().ok()); return value; } From 64edc6e1b117ec87e3bad120a50332b151f11f4e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 26 Feb 2022 03:12:22 -0500 Subject: [PATCH 124/129] feat: useeffect implemented --- packages/hooks/src/lib.rs | 3 ++ packages/hooks/src/usecallback.rs | 8 +-- packages/hooks/src/useeffect.rs | 81 +++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 packages/hooks/src/useeffect.rs diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 598442e19..e63353a4e 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -16,6 +16,9 @@ pub use usefuture::*; mod usecallback; pub use usecallback::*; +mod useeffect; +pub use useeffect::*; + #[macro_export] /// A helper macro for using hooks in async environements. /// diff --git a/packages/hooks/src/usecallback.rs b/packages/hooks/src/usecallback.rs index 1affd323f..94e74cf1c 100644 --- a/packages/hooks/src/usecallback.rs +++ b/packages/hooks/src/usecallback.rs @@ -7,7 +7,6 @@ use std::future::Future; pub fn use_callback + 'static>( cx: &ScopeState, - // g: G, f: impl FnMut(I, G::Out) -> F, ) -> &UseCallback @@ -26,7 +25,7 @@ where pub struct UseCallback { f: Rc>>>, - f2: Box, // f: Rc>>>, + f2: Box, } impl std::ops::Deref for UseCallback { @@ -37,10 +36,6 @@ impl std::ops::Deref for UseCallback { } } -trait MyThing {} -impl MyThing for Box {} -impl MyThing for Box {} - #[test] fn demo() { use dioxus_core::prelude::*; @@ -53,6 +48,7 @@ fn demo() { }); let onsubmit = use_callback(&cx, (name,), my_callback); + async fn my_callback(event: UiEvent<()>, name: (i32,)) { // } diff --git a/packages/hooks/src/useeffect.rs b/packages/hooks/src/useeffect.rs new file mode 100644 index 000000000..b6f6a01eb --- /dev/null +++ b/packages/hooks/src/useeffect.rs @@ -0,0 +1,81 @@ +use dioxus_core::{ScopeState, TaskId}; +use std::{any::Any, cell::Cell, future::Future}; + +use crate::UseFutureDep; + +/// A hook that provides a future that executes after the hooks have been applied +/// +/// Whenever the hooks dependencies change, the future will be re-evaluated. +/// If a future is pending when the dependencies change, the previous future +/// will be allowed to continue +/// +/// - dependencies: a tuple of references to values that are PartialEq + Clone +pub fn use_effect<'a, T: 'static, F: Future + 'static, D: UseFutureDep>( + cx: &'a ScopeState, + dependencies: D, + future: impl FnOnce(D::Out) -> F, +) -> &'a UseEffect { + let state = cx.use_hook(move |_| UseEffect { + needs_regen: Cell::new(true), + task: Cell::new(None), + dependencies: Vec::new(), + }); + + if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { + // We don't need regen anymore + state.needs_regen.set(false); + + // Create the new future + let fut = future(dependencies.out()); + + state.task.set(Some(cx.push_future(async move { + fut.await; + }))); + } + + state +} + +pub struct UseEffect { + needs_regen: Cell, + task: Cell>, + dependencies: Vec>, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(unused)] + #[test] + fn test_use_future() { + use dioxus_core::prelude::*; + + struct MyProps { + a: String, + b: i32, + c: i32, + d: i32, + e: i32, + } + + fn app(cx: Scope) -> Element { + // should only ever run once + use_effect(&cx, (), |_| async move { + // + }); + + // runs when a is changed + use_effect(&cx, (&cx.props.a,), |(a,)| async move { + // + }); + + // runs when a or b is changed + use_effect(&cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { + // + }); + + None + } + } +} From 4ad4b18e4bcfc57aa487f11b45c38d516c93b2d7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 26 Feb 2022 17:31:35 -0500 Subject: [PATCH 125/129] feat: useeffect --- packages/hooks/src/useeffect.rs | 43 +++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/hooks/src/useeffect.rs b/packages/hooks/src/useeffect.rs index b6f6a01eb..09366fc35 100644 --- a/packages/hooks/src/useeffect.rs +++ b/packages/hooks/src/useeffect.rs @@ -10,20 +10,39 @@ use crate::UseFutureDep; /// will be allowed to continue /// /// - dependencies: a tuple of references to values that are PartialEq + Clone -pub fn use_effect<'a, T: 'static, F: Future + 'static, D: UseFutureDep>( - cx: &'a ScopeState, - dependencies: D, - future: impl FnOnce(D::Out) -> F, -) -> &'a UseEffect { +/// +/// ## Examples +/// +/// ```rust, ignore +/// +/// #[inline_props] +/// fn app(cx: Scope, name: &str) -> Element { +/// use_effect(&cx, (name,), |(name,)| async move { +/// set_title(name); +/// })) +/// } +/// ``` +pub fn use_effect(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F) +where + T: 'static, + F: Future + 'static, + D: UseFutureDep, +{ + struct UseEffect { + needs_regen: bool, + task: Cell>, + dependencies: Vec>, + } + let state = cx.use_hook(move |_| UseEffect { - needs_regen: Cell::new(true), + needs_regen: true, task: Cell::new(None), dependencies: Vec::new(), }); - if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { + if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen { // We don't need regen anymore - state.needs_regen.set(false); + state.needs_regen = false; // Create the new future let fut = future(dependencies.out()); @@ -32,14 +51,6 @@ pub fn use_effect<'a, T: 'static, F: Future + 'static, D: UseFutureD fut.await; }))); } - - state -} - -pub struct UseEffect { - needs_regen: Cell, - task: Cell>, - dependencies: Vec>, } #[cfg(test)] From 56517ef5de3ce7998f4938d17958912db9e3009c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 26 Feb 2022 17:35:23 -0500 Subject: [PATCH 126/129] fix: merge conflicts --- packages/core-macro/src/rsx/mod.rs | 4 ++-- packages/core/src/mutations.rs | 2 ++ packages/desktop/src/desktop_context.rs | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/core-macro/src/rsx/mod.rs index e20d80dec..858f07c5c 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/core-macro/src/rsx/mod.rs @@ -79,7 +79,7 @@ impl ToTokens for CallBody { match &self.custom_context { // The `in cx` pattern allows directly rendering Some(ident) => out_tokens.append_all(quote! { - #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + #ident.render(LazyNodes::new(move |__cx: NodeFactory| -> VNode { use dioxus_elements::{GlobalAttributes, SvgAttributes}; #inner })) @@ -87,7 +87,7 @@ impl ToTokens for CallBody { // Otherwise we just build the LazyNode wrapper None => out_tokens.append_all(quote! { - LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + LazyNodes::new(move |__cx: NodeFactory| -> VNode { use dioxus_elements::{GlobalAttributes, SvgAttributes}; #inner }) diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 1e84fe059..2d3a4505f 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -181,6 +181,8 @@ pub enum DomEdit<'bump> { /// The name of the attribute to remove. name: &'static str, + + /// The namespace of the attribute. ns: Option<&'bump str>, }, } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 1376110f1..eaff13750 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -125,6 +125,7 @@ use wry::application::window::Fullscreen as WryFullscreen; use crate::controller::DesktopController; +#[derive(Debug)] pub enum UserWindowEvent { Update, From 74f40da997af0c2c5902d7000f56465fd036e62c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 26 Feb 2022 17:36:33 -0500 Subject: [PATCH 127/129] clippy: remove unused module --- packages/hooks/src/lib.rs | 3 -- packages/hooks/src/usecallback.rs | 64 ------------------------------- 2 files changed, 67 deletions(-) delete mode 100644 packages/hooks/src/usecallback.rs diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index e63353a4e..823a4fc81 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -13,9 +13,6 @@ pub use usecoroutine::*; mod usefuture; pub use usefuture::*; -mod usecallback; -pub use usecallback::*; - mod useeffect; pub use useeffect::*; diff --git a/packages/hooks/src/usecallback.rs b/packages/hooks/src/usecallback.rs deleted file mode 100644 index 94e74cf1c..000000000 --- a/packages/hooks/src/usecallback.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use crate::use_state; -use crate::UseFutureDep; -use dioxus_core::{ScopeState, UiEvent}; -use std::future::Future; - -pub fn use_callback + 'static>( - cx: &ScopeState, - g: G, - f: impl FnMut(I, G::Out) -> F, -) -> &UseCallback -where - G::Out: 'static, - I: 'static, -{ - cx.use_hook(|_| { - // - UseCallback { - f: todo!(), - f2: Box::new(|f| {}), - } - }) -} - -pub struct UseCallback { - f: Rc>>>, - f2: Box, -} - -impl std::ops::Deref for UseCallback { - type Target = dyn Fn(I); - - fn deref(&self) -> &Self::Target { - &self.f2 - } -} - -#[test] -fn demo() { - use dioxus_core::prelude::*; - fn example(cx: Scope) -> Element { - let (name, _) = use_state(&cx, || 0); - let (age, _) = use_state(&cx, || 0); - - let onsubmit = use_callback(&cx, (name,), |event: (), (name,)| async move { - // - }); - - let onsubmit = use_callback(&cx, (name,), my_callback); - - async fn my_callback(event: UiEvent<()>, name: (i32,)) { - // - } - - let onsubmit = use_callback(&cx, name, my_callback2); - - async fn my_callback2(event: UiEvent<()>, name: i32) { - // - } - - None - } -} From f77f8c85000239460c8c67e67be3acf08ac8a400 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 26 Feb 2022 17:38:05 -0500 Subject: [PATCH 128/129] fmt --- packages/core/src/events.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index e92ead8cc..1a9899e4a 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -139,7 +139,9 @@ impl AnyEvent { pub fn downcast(self) -> Option> { let AnyEvent { data, bubble_state } = self; - data.downcast::().ok().map(|data| UiEvent { bubble_state, data }) + data.downcast::() + .ok() + .map(|data| UiEvent { bubble_state, data }) } } From d07e145df66201c588b64f9c224b63bc007bde59 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 26 Feb 2022 17:39:25 -0500 Subject: [PATCH 129/129] clippy: update for recent update --- packages/hooks/src/usefuture.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 1af254843..c7bdd819b 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -152,7 +152,7 @@ impl UseFutureDep for &A { (*self).clone() } fn apply(self, state: &mut Vec>) -> bool { - match state.get_mut(0).map(|f| f.downcast_mut::()).flatten() { + match state.get_mut(0).and_then(|f| f.downcast_mut::()) { Some(val) => { if *val != *self { *val = self.clone();