From d700850b70f0aa6db346b0eea382f36cd52dda42 Mon Sep 17 00:00:00 2001 From: Oliver Forral Date: Sun, 20 Feb 2022 17:27:07 -0700 Subject: [PATCH 01/20] 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 d19ae3ff56eb7e74be282cdbc864bf93bcd762c4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 21 Feb 2022 23:45:01 -0500 Subject: [PATCH 02/20] 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 03/20] 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 04/20] 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 05/20] 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 06/20] 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 07/20] 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 bf3370f0ceb2075b306a9a9708d835da9077ce31 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 09:48:58 -0500 Subject: [PATCH 08/20] 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 09/20] 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 10/20] 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 11/20] 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 12/20] 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 13/20] 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 916dc6440265b10db159adf7e0f5a989dd789817 Mon Sep 17 00:00:00 2001 From: Ilya Maximov Date: Wed, 23 Feb 2022 20:00:01 +0100 Subject: [PATCH 14/20] 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 15/20] 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 8d565529085007478f74e8bdd895708083d68ecd Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Feb 2022 18:17:28 -0500 Subject: [PATCH 16/20] 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 17/20] 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 @@

Dioxus

-

- Frontend that scales. -

From aa2d6b8d1148a5eaabdfa9f5044c8e8bc493e923 Mon Sep 17 00:00:00 2001 From: Shuanghui Yan Date: Thu, 24 Feb 2022 18:13:00 +0800 Subject: [PATCH 18/20] 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 19/20] 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 20/20] 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; }