From 14bc007c15cc6406010a940fa2aab2cfbc9d3964 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 28 Dec 2022 11:51:41 -0500 Subject: [PATCH 01/29] wip: refactor out translation into its own crate --- Cargo.toml | 1 + packages/autofmt/src/lib.rs | 28 ++++++++++++++++++++++-- packages/html-to-rsx/Cargo.toml | 20 +++++++++++++++++ packages/html-to-rsx/README.md | 19 ++++++++++++++++ packages/html-to-rsx/examples/html.rs | 13 +++++++++++ packages/html-to-rsx/src/lib.rs | 31 +++++++++++++++++++++++++++ packages/rsx/src/comments.rs | 24 +++++++++++++++++++++ packages/rsx/src/lib.rs | 3 +++ packages/rsx/src/node.rs | 16 ++++++++++++++ 9 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 packages/html-to-rsx/Cargo.toml create mode 100644 packages/html-to-rsx/README.md create mode 100644 packages/html-to-rsx/examples/html.rs create mode 100644 packages/html-to-rsx/src/lib.rs create mode 100644 packages/rsx/src/comments.rs diff --git a/Cargo.toml b/Cargo.toml index 5955451f8..044421f24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "packages/native-core", "packages/native-core-macro", "docs/guide", + "packages/html-to-rsx" ] # This is a "virtual package" diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index aca9f64b0..a19fe54fe 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -1,3 +1,5 @@ +use dioxus_rsx::CallBody; + use crate::buffer::*; use crate::util::*; @@ -31,6 +33,11 @@ pub struct FormattedBlock { /// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting. /// /// This function expects a complete file, not just a block of code. To format individual rsx! blocks, use fmt_block instead. +/// +/// The point here is to provide precise modifications of a source file so an accompanying IDE tool can map these changes +/// back to the file precisely. +/// +/// Nested blocks of RSX will be handled automatically pub fn fmt_file(contents: &str) -> Vec { let mut formatted_blocks = Vec::new(); let mut last_bracket_end = 0; @@ -93,15 +100,32 @@ pub fn fmt_file(contents: &str) -> Vec { formatted_blocks } +pub fn write_block_out(body: CallBody) -> Option { + let mut buf = Buffer { + src: vec![], + indent: 0, + ..Buffer::default() + }; + + // Oneliner optimization + if buf.is_short_children(&body.roots).is_some() { + buf.write_ident(&body.roots[0]).unwrap(); + } else { + buf.write_body_indented(&body.roots).unwrap(); + } + + buf.consume() +} + pub fn fmt_block(block: &str, indent_level: usize) -> Option { + let body = syn::parse_str::(block).unwrap(); + let mut buf = Buffer { src: block.lines().map(|f| f.to_string()).collect(), indent: indent_level, ..Buffer::default() }; - let body = syn::parse_str::(block).unwrap(); - // Oneliner optimization if buf.is_short_children(&body.roots).is_some() { buf.write_ident(&body.roots[0]).unwrap(); diff --git a/packages/html-to-rsx/Cargo.toml b/packages/html-to-rsx/Cargo.toml new file mode 100644 index 000000000..3ff43c3b3 --- /dev/null +++ b/packages/html-to-rsx/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rsx-rosetta" +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-autofmt = { path = "../autofmt" } +dioxus-rsx = { path = "../rsx" } +html_parser = "0.6.3" +proc-macro2 = "1.0.49" +quote = "1.0.23" +syn = { version = "1.0.107", features = ["full"] } +thiserror = "1.0.38" + +# [features] +# default = ["html"] + +# eventually more output options diff --git a/packages/html-to-rsx/README.md b/packages/html-to-rsx/README.md new file mode 100644 index 000000000..35f86d00a --- /dev/null +++ b/packages/html-to-rsx/README.md @@ -0,0 +1,19 @@ +# Rosetta for RSX +--- + +Dioxus sports its own templating language inspired by C#/Kotlin/RTMP, etc. It's pretty straightforward. + +However, it's NOT HTML. This is done since HTML is verbose and you'd need a dedicated LSP or IDE integration to get a good DX in .rs files. + +RSX is simple... It's similar enough to regular Rust code to trick most IDEs into automatically providing support for things like block selections, folding, highlighting, etc. + +To accomodate the transition from HTML to RSX, you might need to translate some existing code. + +This library provids a central AST that can accept a number of inputs: + +- HTML +- Syn (todo) +- Akama (todo) +- Jinja (todo) + +From there, you can convert directly to a string or into some other AST. diff --git a/packages/html-to-rsx/examples/html.rs b/packages/html-to-rsx/examples/html.rs new file mode 100644 index 000000000..7f5031857 --- /dev/null +++ b/packages/html-to-rsx/examples/html.rs @@ -0,0 +1,13 @@ +use html_parser::Dom; + +fn main() { + let html = "hello world!"; + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::convert_from_html(dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + dbg!(out); +} diff --git a/packages/html-to-rsx/src/lib.rs b/packages/html-to-rsx/src/lib.rs new file mode 100644 index 000000000..46d4017fb --- /dev/null +++ b/packages/html-to-rsx/src/lib.rs @@ -0,0 +1,31 @@ +use dioxus_rsx::{BodyNode, CallBody, IfmtInput}; +use html_parser::{Dom, Node}; +use proc_macro2::Span; +use syn::LitStr; + +#[derive(thiserror::Error, Debug)] +pub enum ConvertError {} + +pub fn convert_from_html(html: Dom) -> CallBody { + let roots = html + .children + .into_iter() + .map(|f| create_body_node_from_node(f)) + .filter_map(|f| f) + .collect(); + + CallBody { roots } +} + +fn create_body_node_from_node(node: Node) -> Option { + let res = match node { + Node::Text(text) => BodyNode::Text(IfmtInput { + source: Some(LitStr::new(text.as_str(), Span::call_site())), + segments: vec![], + }), + Node::Element(_) => todo!(), + Node::Comment(_) => return None, + }; + + Some(res) +} diff --git a/packages/rsx/src/comments.rs b/packages/rsx/src/comments.rs new file mode 100644 index 000000000..3c999eed9 --- /dev/null +++ b/packages/rsx/src/comments.rs @@ -0,0 +1,24 @@ +use std::hash::Hash; + +use proc_macro2::Span; + +// A form of whitespace +#[derive(Debug, Clone)] +pub struct UserComment { + pub span: Span, + pub comment: String, +} + +impl PartialEq for UserComment { + fn eq(&self, other: &Self) -> bool { + self.comment == other.comment + } +} + +impl Eq for UserComment {} + +impl Hash for UserComment { + fn hash(&self, state: &mut H) { + self.comment.hash(state); + } +} diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index d22b122e9..4b33a71c8 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -13,6 +13,7 @@ #[macro_use] mod errors; +mod comments; mod component; mod element; pub mod hot_reload; @@ -296,6 +297,8 @@ impl DynamicMapping { BodyNode::Text(text) if text.is_static() => {} + BodyNode::Comment(_) => {} + BodyNode::RawExpr(_) | BodyNode::Text(_) | BodyNode::ForLoop(_) diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index ef76d14ff..eb2f95a42 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -1,3 +1,5 @@ +use crate::comments::UserComment; + use super::*; use proc_macro2::{Span, TokenStream as TokenStream2}; @@ -16,6 +18,17 @@ Parse -> component() -> "text {with_args}" -> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr +-> // some comment here (no support for slash asterisk comments - those get deleted completely) + + + +div { + // Comment + div { // a comment here because it shares the line + + } +} + */ #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub enum BodyNode { @@ -25,6 +38,7 @@ pub enum BodyNode { IfChain(ExprIf), Text(IfmtInput), RawExpr(Expr), + Comment(UserComment), } impl BodyNode { @@ -40,6 +54,7 @@ impl BodyNode { BodyNode::RawExpr(exp) => exp.span(), BodyNode::ForLoop(fl) => fl.for_token.span(), BodyNode::IfChain(f) => f.if_token.span(), + BodyNode::Comment(c) => c.span, } } } @@ -128,6 +143,7 @@ impl Parse for BodyNode { impl ToTokens for BodyNode { fn to_tokens(&self, tokens: &mut TokenStream2) { match &self { + BodyNode::Comment(_) => {} BodyNode::Element(el) => el.to_tokens(tokens), BodyNode::Component(comp) => comp.to_tokens(tokens), BodyNode::Text(txt) => tokens.append_all(quote! { From d22577082ceff5f579a3d8ca30e7562db7ad3a73 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 28 Dec 2022 12:05:13 -0500 Subject: [PATCH 02/29] chore: remove comment from syn tree --- packages/rsx/src/lib.rs | 2 -- packages/rsx/src/node.rs | 3 --- 2 files changed, 5 deletions(-) diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 4b33a71c8..7946a7132 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -297,8 +297,6 @@ impl DynamicMapping { BodyNode::Text(text) if text.is_static() => {} - BodyNode::Comment(_) => {} - BodyNode::RawExpr(_) | BodyNode::Text(_) | BodyNode::ForLoop(_) diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index eb2f95a42..c83655cb0 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -38,7 +38,6 @@ pub enum BodyNode { IfChain(ExprIf), Text(IfmtInput), RawExpr(Expr), - Comment(UserComment), } impl BodyNode { @@ -54,7 +53,6 @@ impl BodyNode { BodyNode::RawExpr(exp) => exp.span(), BodyNode::ForLoop(fl) => fl.for_token.span(), BodyNode::IfChain(f) => f.if_token.span(), - BodyNode::Comment(c) => c.span, } } } @@ -143,7 +141,6 @@ impl Parse for BodyNode { impl ToTokens for BodyNode { fn to_tokens(&self, tokens: &mut TokenStream2) { match &self { - BodyNode::Comment(_) => {} BodyNode::Element(el) => el.to_tokens(tokens), BodyNode::Component(comp) => comp.to_tokens(tokens), BodyNode::Text(txt) => tokens.append_all(quote! { From bc3cf6501c5c916d053549727a6df7389015adbe Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 28 Dec 2022 16:22:20 -0500 Subject: [PATCH 03/29] feat: complete html to rsx rosetta --- Cargo.toml | 82 ++++++++--------- packages/autofmt/src/lib.rs | 2 +- packages/html-to-rsx/examples/html.rs | 13 --- packages/html-to-rsx/src/lib.rs | 31 ------- .../{html-to-rsx => rsx-rosetta}/Cargo.toml | 2 +- .../{html-to-rsx => rsx-rosetta}/README.md | 0 packages/rsx-rosetta/examples/html.rs | 24 +++++ packages/rsx-rosetta/src/lib.rs | 92 +++++++++++++++++++ 8 files changed, 159 insertions(+), 87 deletions(-) delete mode 100644 packages/html-to-rsx/examples/html.rs delete mode 100644 packages/html-to-rsx/src/lib.rs rename packages/{html-to-rsx => rsx-rosetta}/Cargo.toml (94%) rename packages/{html-to-rsx => rsx-rosetta}/README.md (100%) create mode 100644 packages/rsx-rosetta/examples/html.rs create mode 100644 packages/rsx-rosetta/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 044421f24..51b4ac829 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,49 +18,49 @@ members = [ "packages/tui", "packages/native-core", "packages/native-core-macro", + "packages/rsx-rosetta", "docs/guide", - "packages/html-to-rsx" ] -# This is a "virtual package" -# It is not meant to be published, but is used so "cargo run --example XYZ" works properly -[package] -name = "dioxus-examples" -version = "0.0.0" -authors = ["Jonathan Kelley"] -edition = "2021" -description = "Top level crate for the Dioxus repository" -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.60.0" -publish = false +# # This is a "virtual package" +# # It is not meant to be published, but is used so "cargo run --example XYZ" works properly +# [package] +# name = "dioxus-examples" +# version = "0.0.0" +# authors = ["Jonathan Kelley"] +# edition = "2021" +# description = "Top level crate for the Dioxus repository" +# 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.60.0" +# publish = false -[dev-dependencies] -dioxus = { path = "./packages/dioxus" } -dioxus-desktop = { path = "./packages/desktop" } -dioxus-ssr = { path = "./packages/ssr" } -dioxus-router = { path = "./packages/router" } -fermi = { path = "./packages/fermi" } -futures-util = "0.3.21" -log = "0.4.14" -num-format = "0.4.0" -separator = "0.4.1" -serde = { version = "1.0.136", features = ["derive"] } -im-rc = "15.0.0" -anyhow = "1.0.53" -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"] } -fern = { version = "0.6.0", features = ["colored"] } -thiserror = "1.0.30" -env_logger = "0.9.0" -simple_logger = "4.0.0" +# [dev-dependencies] +# dioxus = { path = "./packages/dioxus" } +# dioxus-desktop = { path = "./packages/desktop" } +# dioxus-ssr = { path = "./packages/ssr" } +# dioxus-router = { path = "./packages/router" } +# fermi = { path = "./packages/fermi" } +# futures-util = "0.3.21" +# log = "0.4.14" +# num-format = "0.4.0" +# separator = "0.4.1" +# serde = { version = "1.0.136", features = ["derive"] } +# im-rc = "15.0.0" +# anyhow = "1.0.53" +# 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"] } +# fern = { version = "0.6.0", features = ["colored"] } +# thiserror = "1.0.30" +# env_logger = "0.9.0" +# simple_logger = "4.0.0" -[profile.release] -opt-level = 3 -lto = true -debug = true +# [profile.release] +# opt-level = 3 +# lto = true +# debug = true diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index a19fe54fe..37e5e1b33 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -102,7 +102,7 @@ pub fn fmt_file(contents: &str) -> Vec { pub fn write_block_out(body: CallBody) -> Option { let mut buf = Buffer { - src: vec![], + src: vec!["".to_string()], indent: 0, ..Buffer::default() }; diff --git a/packages/html-to-rsx/examples/html.rs b/packages/html-to-rsx/examples/html.rs deleted file mode 100644 index 7f5031857..000000000 --- a/packages/html-to-rsx/examples/html.rs +++ /dev/null @@ -1,13 +0,0 @@ -use html_parser::Dom; - -fn main() { - let html = "hello world!"; - - let dom = Dom::parse(html).unwrap(); - - let body = rsx_rosetta::convert_from_html(dom); - - let out = dioxus_autofmt::write_block_out(body).unwrap(); - - dbg!(out); -} diff --git a/packages/html-to-rsx/src/lib.rs b/packages/html-to-rsx/src/lib.rs deleted file mode 100644 index 46d4017fb..000000000 --- a/packages/html-to-rsx/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -use dioxus_rsx::{BodyNode, CallBody, IfmtInput}; -use html_parser::{Dom, Node}; -use proc_macro2::Span; -use syn::LitStr; - -#[derive(thiserror::Error, Debug)] -pub enum ConvertError {} - -pub fn convert_from_html(html: Dom) -> CallBody { - let roots = html - .children - .into_iter() - .map(|f| create_body_node_from_node(f)) - .filter_map(|f| f) - .collect(); - - CallBody { roots } -} - -fn create_body_node_from_node(node: Node) -> Option { - let res = match node { - Node::Text(text) => BodyNode::Text(IfmtInput { - source: Some(LitStr::new(text.as_str(), Span::call_site())), - segments: vec![], - }), - Node::Element(_) => todo!(), - Node::Comment(_) => return None, - }; - - Some(res) -} diff --git a/packages/html-to-rsx/Cargo.toml b/packages/rsx-rosetta/Cargo.toml similarity index 94% rename from packages/html-to-rsx/Cargo.toml rename to packages/rsx-rosetta/Cargo.toml index 3ff43c3b3..2def4e7c8 100644 --- a/packages/html-to-rsx/Cargo.toml +++ b/packages/rsx-rosetta/Cargo.toml @@ -12,7 +12,7 @@ html_parser = "0.6.3" proc-macro2 = "1.0.49" quote = "1.0.23" syn = { version = "1.0.107", features = ["full"] } -thiserror = "1.0.38" +convert_case = "0.5.0" # [features] # default = ["html"] diff --git a/packages/html-to-rsx/README.md b/packages/rsx-rosetta/README.md similarity index 100% rename from packages/html-to-rsx/README.md rename to packages/rsx-rosetta/README.md diff --git a/packages/rsx-rosetta/examples/html.rs b/packages/rsx-rosetta/examples/html.rs new file mode 100644 index 000000000..4a64b6c57 --- /dev/null +++ b/packages/rsx-rosetta/examples/html.rs @@ -0,0 +1,24 @@ +use html_parser::Dom; + +fn main() { + let html = r#" +
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+ hello world! +
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::convert_from_html(dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + println!("{}", out); +} diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs new file mode 100644 index 000000000..1d1248852 --- /dev/null +++ b/packages/rsx-rosetta/src/lib.rs @@ -0,0 +1,92 @@ +use dioxus_rsx::{BodyNode, CallBody, Element, ElementAttr, ElementAttrNamed, IfmtInput}; +pub use html_parser::{Dom, Node}; +use proc_macro2::{Ident, Span}; +use syn::LitStr; + +pub fn convert_from_html(dom: Dom) -> CallBody { + CallBody { + roots: dom + .children + .into_iter() + .map(|f| create_body_node_from_node(f)) + .filter_map(|f| f) + .collect(), + } +} + +fn create_body_node_from_node(node: Node) -> Option { + match node { + Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))), + Node::Element(el) => { + use convert_case::{Case, Casing}; + + let el_name = el.name.to_case(Case::Snake); + let el_name = Ident::new(el_name.as_str(), Span::call_site()); + + let mut attributes: Vec<_> = el + .attributes + .into_iter() + .map(|(name, value)| { + let ident = if matches!(name.as_str(), "for" | "async" | "type" | "as") { + Ident::new_raw(name.as_str(), Span::call_site()) + } else { + let new_name = name.to_case(Case::Snake); + Ident::new(new_name.as_str(), Span::call_site()) + }; + + ElementAttrNamed { + attr: ElementAttr::AttrText { + name: ident, + value: ifmt_from_text(value.unwrap_or("false".to_string())), + }, + el_name: el_name.clone(), + } + }) + .collect(); + + let class = el.classes.join(" "); + if !class.is_empty() { + attributes.push(ElementAttrNamed { + attr: ElementAttr::AttrText { + name: Ident::new("class", Span::call_site()), + value: ifmt_from_text(class), + }, + el_name: el_name.clone(), + }); + } + + if let Some(id) = el.id { + attributes.push(ElementAttrNamed { + attr: ElementAttr::AttrText { + name: Ident::new("id", Span::call_site()), + value: ifmt_from_text(id), + }, + el_name: el_name.clone(), + }); + } + + let children = el + .children + .into_iter() + .map(|f| create_body_node_from_node(f)) + .filter_map(|f| f) + .collect(); + + Some(BodyNode::Element(Element { + name: el_name, + children, + attributes, + _is_static: false, + key: None, + })) + } + Node::Comment(_) => None, + } +} + +fn ifmt_from_text(text: String) -> IfmtInput { + IfmtInput { + source: Some(LitStr::new(text.as_str(), Span::call_site())), + segments: vec![], + } +} From c890051f875f84b40c377846738d636a3ab31eee Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 28 Dec 2022 16:23:05 -0500 Subject: [PATCH 04/29] chore: add back examples --- Cargo.toml | 80 +++++++++++++++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 51b4ac829..606aef6be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,45 +22,45 @@ members = [ "docs/guide", ] -# # This is a "virtual package" -# # It is not meant to be published, but is used so "cargo run --example XYZ" works properly -# [package] -# name = "dioxus-examples" -# version = "0.0.0" -# authors = ["Jonathan Kelley"] -# edition = "2021" -# description = "Top level crate for the Dioxus repository" -# 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.60.0" -# publish = false +# This is a "virtual package" +# It is not meant to be published, but is used so "cargo run --example XYZ" works properly +[package] +name = "dioxus-examples" +version = "0.0.0" +authors = ["Jonathan Kelley"] +edition = "2021" +description = "Top level crate for the Dioxus repository" +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.60.0" +publish = false -# [dev-dependencies] -# dioxus = { path = "./packages/dioxus" } -# dioxus-desktop = { path = "./packages/desktop" } -# dioxus-ssr = { path = "./packages/ssr" } -# dioxus-router = { path = "./packages/router" } -# fermi = { path = "./packages/fermi" } -# futures-util = "0.3.21" -# log = "0.4.14" -# num-format = "0.4.0" -# separator = "0.4.1" -# serde = { version = "1.0.136", features = ["derive"] } -# im-rc = "15.0.0" -# anyhow = "1.0.53" -# 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"] } -# fern = { version = "0.6.0", features = ["colored"] } -# thiserror = "1.0.30" -# env_logger = "0.9.0" -# simple_logger = "4.0.0" +[dev-dependencies] +dioxus = { path = "./packages/dioxus" } +dioxus-desktop = { path = "./packages/desktop" } +dioxus-ssr = { path = "./packages/ssr" } +dioxus-router = { path = "./packages/router" } +fermi = { path = "./packages/fermi" } +futures-util = "0.3.21" +log = "0.4.14" +num-format = "0.4.0" +separator = "0.4.1" +serde = { version = "1.0.136", features = ["derive"] } +im-rc = "15.0.0" +anyhow = "1.0.53" +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"] } +fern = { version = "0.6.0", features = ["colored"] } +thiserror = "1.0.30" +env_logger = "0.9.0" +simple_logger = "4.0.0" -# [profile.release] -# opt-level = 3 -# lto = true -# debug = true +[profile.release] +opt-level = 3 +lto = true +debug = true From d3c3c41d9efcf698be055afa18169d0272cf5629 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 28 Dec 2022 16:39:24 -0500 Subject: [PATCH 05/29] chore: remove comments module --- packages/rsx/src/comments.rs | 24 ------------------------ packages/rsx/src/lib.rs | 1 - packages/rsx/src/node.rs | 2 -- 3 files changed, 27 deletions(-) delete mode 100644 packages/rsx/src/comments.rs diff --git a/packages/rsx/src/comments.rs b/packages/rsx/src/comments.rs deleted file mode 100644 index 3c999eed9..000000000 --- a/packages/rsx/src/comments.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::hash::Hash; - -use proc_macro2::Span; - -// A form of whitespace -#[derive(Debug, Clone)] -pub struct UserComment { - pub span: Span, - pub comment: String, -} - -impl PartialEq for UserComment { - fn eq(&self, other: &Self) -> bool { - self.comment == other.comment - } -} - -impl Eq for UserComment {} - -impl Hash for UserComment { - fn hash(&self, state: &mut H) { - self.comment.hash(state); - } -} diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 7946a7132..d22b122e9 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -13,7 +13,6 @@ #[macro_use] mod errors; -mod comments; mod component; mod element; pub mod hot_reload; diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index c83655cb0..d7dfbf0d6 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -1,5 +1,3 @@ -use crate::comments::UserComment; - use super::*; use proc_macro2::{Span, TokenStream as TokenStream2}; From bc707db8325f9d95e3f94e962b1f378a6a04d0de Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 00:46:02 -0500 Subject: [PATCH 06/29] chore: make clippy happy and limit workspace checks --- .vscode/settings.json | 3 ++- packages/rsx-rosetta/src/lib.rs | 8 +++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 819ffec5d..ebd52887e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "editor.formatOnSave": true, "[toml]": { "editor.formatOnSave": false - } + }, + "rust-analyzer.checkOnSave.allTargets": false, } diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 1d1248852..83f79883c 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -8,8 +8,7 @@ pub fn convert_from_html(dom: Dom) -> CallBody { roots: dom .children .into_iter() - .map(|f| create_body_node_from_node(f)) - .filter_map(|f| f) + .filter_map(create_body_node_from_node) .collect(), } } @@ -37,7 +36,7 @@ fn create_body_node_from_node(node: Node) -> Option { ElementAttrNamed { attr: ElementAttr::AttrText { name: ident, - value: ifmt_from_text(value.unwrap_or("false".to_string())), + value: ifmt_from_text(value.unwrap_or_else(|| "false".to_string())), }, el_name: el_name.clone(), } @@ -68,8 +67,7 @@ fn create_body_node_from_node(node: Node) -> Option { let children = el .children .into_iter() - .map(|f| create_body_node_from_node(f)) - .filter_map(|f| f) + .filter_map(create_body_node_from_node) .collect(); Some(BodyNode::Element(Element { From 138976659645a53942790255fce72085014b5ef2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 00:55:04 -0500 Subject: [PATCH 07/29] chore: take domtree by reference --- packages/rsx-rosetta/examples/html.rs | 2 +- packages/rsx-rosetta/src/lib.rs | 36 ++++++++++++++------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/rsx-rosetta/examples/html.rs b/packages/rsx-rosetta/examples/html.rs index 4a64b6c57..ef3006f31 100644 --- a/packages/rsx-rosetta/examples/html.rs +++ b/packages/rsx-rosetta/examples/html.rs @@ -16,7 +16,7 @@ fn main() { let dom = Dom::parse(html).unwrap(); - let body = rsx_rosetta::convert_from_html(dom); + let body = rsx_rosetta::rsx_from_html(dom); let out = dioxus_autofmt::write_block_out(body).unwrap(); diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 83f79883c..440a41ebc 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -1,30 +1,30 @@ +use convert_case::{Case, Casing}; use dioxus_rsx::{BodyNode, CallBody, Element, ElementAttr, ElementAttrNamed, IfmtInput}; pub use html_parser::{Dom, Node}; use proc_macro2::{Ident, Span}; use syn::LitStr; -pub fn convert_from_html(dom: Dom) -> CallBody { +/// Convert an HTML DOM tree into an RSX CallBody +pub fn rsx_from_html(dom: &Dom) -> CallBody { CallBody { roots: dom .children - .into_iter() + .iter() .filter_map(create_body_node_from_node) .collect(), } } -fn create_body_node_from_node(node: Node) -> Option { +fn create_body_node_from_node(node: &Node) -> Option { match node { Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))), Node::Element(el) => { - use convert_case::{Case, Casing}; - let el_name = el.name.to_case(Case::Snake); let el_name = Ident::new(el_name.as_str(), Span::call_site()); let mut attributes: Vec<_> = el .attributes - .into_iter() + .iter() .map(|(name, value)| { let ident = if matches!(name.as_str(), "for" | "async" | "type" | "as") { Ident::new_raw(name.as_str(), Span::call_site()) @@ -34,11 +34,11 @@ fn create_body_node_from_node(node: Node) -> Option { }; ElementAttrNamed { - attr: ElementAttr::AttrText { - name: ident, - value: ifmt_from_text(value.unwrap_or_else(|| "false".to_string())), - }, el_name: el_name.clone(), + attr: ElementAttr::AttrText { + value: ifmt_from_text(value.as_deref().unwrap_or("false")), + name: ident, + }, } }) .collect(); @@ -46,27 +46,27 @@ fn create_body_node_from_node(node: Node) -> Option { let class = el.classes.join(" "); if !class.is_empty() { attributes.push(ElementAttrNamed { + el_name: el_name.clone(), attr: ElementAttr::AttrText { name: Ident::new("class", Span::call_site()), - value: ifmt_from_text(class), + value: ifmt_from_text(&class), }, - el_name: el_name.clone(), }); } - if let Some(id) = el.id { + if let Some(id) = &el.id { attributes.push(ElementAttrNamed { + el_name: el_name.clone(), attr: ElementAttr::AttrText { name: Ident::new("id", Span::call_site()), value: ifmt_from_text(id), }, - el_name: el_name.clone(), }); } let children = el .children - .into_iter() + .iter() .filter_map(create_body_node_from_node) .collect(); @@ -78,13 +78,15 @@ fn create_body_node_from_node(node: Node) -> Option { key: None, })) } + + // We ignore comments Node::Comment(_) => None, } } -fn ifmt_from_text(text: String) -> IfmtInput { +fn ifmt_from_text(text: &str) -> IfmtInput { IfmtInput { - source: Some(LitStr::new(text.as_str(), Span::call_site())), + source: Some(LitStr::new(text, Span::call_site())), segments: vec![], } } From ecb154db6f8e23fed16b7565b4ade906f1b7c23a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 00:56:23 -0500 Subject: [PATCH 08/29] chore: rename method --- packages/rsx-rosetta/src/lib.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 440a41ebc..430ab54dc 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -7,15 +7,14 @@ use syn::LitStr; /// Convert an HTML DOM tree into an RSX CallBody pub fn rsx_from_html(dom: &Dom) -> CallBody { CallBody { - roots: dom - .children - .iter() - .filter_map(create_body_node_from_node) - .collect(), + roots: dom.children.iter().filter_map(rsx_node_from_html).collect(), } } -fn create_body_node_from_node(node: &Node) -> Option { +/// Convert an HTML Node into an RSX BodyNode +/// +/// If the node is a comment, it will be ignored since RSX doesn't support comments +pub fn rsx_node_from_html(node: &Node) -> Option { match node { Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))), Node::Element(el) => { @@ -64,11 +63,7 @@ fn create_body_node_from_node(node: &Node) -> Option { }); } - let children = el - .children - .iter() - .filter_map(create_body_node_from_node) - .collect(); + let children = el.children.iter().filter_map(rsx_node_from_html).collect(); Some(BodyNode::Element(Element { name: el_name, From 5c63ae4b5db9c822febdb0322436077749260aa1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 01:02:07 -0500 Subject: [PATCH 09/29] chore: dont panic --- packages/autofmt/src/lib.rs | 2 +- packages/rsx/src/node.rs | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 37e5e1b33..a5da71c9b 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -118,7 +118,7 @@ pub fn write_block_out(body: CallBody) -> Option { } pub fn fmt_block(block: &str, indent_level: usize) -> Option { - let body = syn::parse_str::(block).unwrap(); + let body = syn::parse_str::(block).ok()?; let mut buf = Buffer { src: block.lines().map(|f| f.to_string()).collect(), diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index d7dfbf0d6..ef76d14ff 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -16,17 +16,6 @@ Parse -> component() -> "text {with_args}" -> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr --> // some comment here (no support for slash asterisk comments - those get deleted completely) - - - -div { - // Comment - div { // a comment here because it shares the line - - } -} - */ #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub enum BodyNode { From 04ca284704b3aa7e18cf7b4f2e573e25df3d47eb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 02:21:32 -0500 Subject: [PATCH 10/29] chore: add svg collection --- packages/rsx-rosetta/src/lib.rs | 47 +++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 430ab54dc..4e5d583ae 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -1,8 +1,10 @@ use convert_case::{Case, Casing}; -use dioxus_rsx::{BodyNode, CallBody, Element, ElementAttr, ElementAttrNamed, IfmtInput}; +use dioxus_rsx::{ + BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, IfmtInput, +}; pub use html_parser::{Dom, Node}; use proc_macro2::{Ident, Span}; -use syn::LitStr; +use syn::{punctuated::Punctuated, LitStr}; /// Convert an HTML DOM tree into an RSX CallBody pub fn rsx_from_html(dom: &Dom) -> CallBody { @@ -79,6 +81,47 @@ pub fn rsx_node_from_html(node: &Node) -> Option { } } +/// Pull out all the svgs from the body and replace them with components +pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec) { + for child in children { + match child { + BodyNode::Component(comp) => collect_svgs(&mut comp.children, out), + + BodyNode::Element(el) if el.name == "svg" => { + // we want to replace this instance with a component + let mut segments = Punctuated::new(); + + segments.push(Ident::new("icons", Span::call_site()).into()); + + let new_name: Ident = Ident::new(&format!("icon_{}", out.len()), Span::call_site()); + + segments.push(new_name.clone().into()); + + // Replace this instance with a component + let mut new_comp = BodyNode::Component(Component { + name: syn::Path { + leading_colon: None, + segments, + }, + prop_gen_args: None, + fields: vec![], + children: vec![], + manual_props: None, + }); + + std::mem::swap(child, &mut new_comp); + + // And push the original svg into the svg list + out.push(new_comp); + } + + BodyNode::Element(el) => collect_svgs(&mut el.children, out), + + _ => {} + } + } +} + fn ifmt_from_text(text: &str) -> IfmtInput { IfmtInput { source: Some(LitStr::new(text, Span::call_site())), From 6a4979b5748d8e921f945a24f9fde64cc8648385 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 02:37:46 -0500 Subject: [PATCH 11/29] fix example --- packages/rsx-rosetta/examples/html.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rsx-rosetta/examples/html.rs b/packages/rsx-rosetta/examples/html.rs index ef3006f31..d3aabb86c 100644 --- a/packages/rsx-rosetta/examples/html.rs +++ b/packages/rsx-rosetta/examples/html.rs @@ -16,7 +16,7 @@ fn main() { let dom = Dom::parse(html).unwrap(); - let body = rsx_rosetta::rsx_from_html(dom); + let body = rsx_rosetta::rsx_from_html(&dom); let out = dioxus_autofmt::write_block_out(body).unwrap(); From 4fdbce9a70e95f9d078e62d294c156a7114cafd2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 16:17:03 -0500 Subject: [PATCH 12/29] chore: clean up readmes --- packages/autofmt/README.md | 109 +-- packages/core-macro/README.md | 53 +- packages/desktop/ARCHITECTURE.md | 14 - packages/desktop/README.md | 58 +- packages/hooks/README.md | 58 +- packages/html/README.md | 65 +- packages/interpreter/README.md | 47 +- packages/interpreter/src/interpreter.old.js | 944 -------------------- packages/liveview/README.md | 56 +- packages/mobile/README.md | 47 +- packages/native-core-macro/README.md | 43 + packages/native-core/README.md | 46 +- packages/router/README.md | 93 +- packages/rsx-rosetta/README.md | 44 +- packages/rsx-rosetta/src/lib.rs | 2 +- packages/rsx/README.md | 44 +- packages/ssr/README.md | 29 +- packages/web/README.md | 49 +- 18 files changed, 618 insertions(+), 1183 deletions(-) delete mode 100644 packages/desktop/ARCHITECTURE.md delete mode 100644 packages/interpreter/src/interpreter.old.js create mode 100644 packages/native-core-macro/README.md diff --git a/packages/autofmt/README.md b/packages/autofmt/README.md index eec3b590e..128eb11b5 100644 --- a/packages/autofmt/README.md +++ b/packages/autofmt/README.md @@ -1,88 +1,49 @@ -# This crate autofmts blocks of rsx! - -This crate formats rsx! by parsing call bodies and pretty-printing them back out. +# dioxus-autofmt +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] -# Todo: -Sorted roughly in order of what's possible +[crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg +[crates-url]: https://crates.io/crates/dioxus-autofmt -- [x] Oneline rsx! calls - blocker because this wrecks formatting -- [ ] Nested RSX calls (important) - unnecessary but desirable -- [ ] RSX edits overstepping each other -- [ ] Collapse components and elements under syntax - -- [ ] Don't eat comments in exprs -- [ ] Format regular exprs -- [ ] Fix prettyplease around chaining -- [ ] Don't eat comments in prettyplease +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus-autofmt) | +[Chat](https://discord.gg/XgGxMSkvUM) -# Technique +## Overview + +`dioxus-autofmt` provides a pretty printer for the `rsx` syntax tree. -div { - div {} - div {} -} +This is done manually with a via set of formatting rules. The output is not guaranteed to be stable between minor versions of the crate as we might tweak the output. + +`dioxus-autofmt` provides an API to perform precision edits as well as just spit out a block of formatted RSX from any RSX syntax tree. This is used by the `rsx-rosetta` crate which can accept various input languages and output valid RSX. -div +## Contributing -possible line break -div -div +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! +## License +This project is licensed under the [MIT license]. +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT -string of possible items within a nesting -div { - attr_pair - expr - text - comment -} -a nesting is either a component or an element - -idea: -collect all items into a queue -q -```rust -section { - div { - h1 { p { "asdasd" } } - h1 { p { "asdasd" } } - } -} - -section {} -``` - - -// space -// space -// space - - -3 - section -3 - section div -3 - section div h1 -3 - section div h1 p -3 - section div h1 p text -3 - section -3 - section div -3 - section div h1 -3 - section div h1 p -3 - section div h1 p text - -block - -- when we hit the end of a trail, we can make a decision what needs to be hard breaked -- most nestings cannot be merged into a single one, so at some point we need to write the line break -- this is the scan section. we scan forward until it's obvious where to place a hard break -- when a line is finished, we can print it out by unloading our queued items -- never double nested - - -Terms -- break is a whitespace than can flex, dependent on the situation -- ‹⁠› +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/packages/core-macro/README.md b/packages/core-macro/README.md index d9527a411..4a2d101e7 100644 --- a/packages/core-macro/README.md +++ b/packages/core-macro/README.md @@ -1,7 +1,48 @@ -# core-macro +# dioxus-core-macro -This crate implements these macros: -- `format_args_f`: for f-string interpolation inside of text blocks -- `Props`: derive macro for typed-builder with props configurations -- -- + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-core-macro.svg +[crates-url]: https://crates.io/crates/dioxus-core-macro + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-core-macro/latest/dioxus-core-macro) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +`dioxus-core-macro` provides a handful of helpful macros used by the `dioxus` crate. These include: + +- The `rsx!` macro that underpins templates and node creation +- The `inline_props` that transforms function arguments into an auto-derived struct +- The `format_args_f` macro which allows f-string formatting with support for expressions + + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/packages/desktop/ARCHITECTURE.md b/packages/desktop/ARCHITECTURE.md deleted file mode 100644 index 5a937dbca..000000000 --- a/packages/desktop/ARCHITECTURE.md +++ /dev/null @@ -1,14 +0,0 @@ -# dioxus webview arch - - -dioxus webview represents a usecase of dioxus to stream edits from one dom to another. In this particular case, we stream edits from a native process into the webview's runtime. WebView is really portable, so we can leverage the UI of iOS, Android, and all desktop platforms while also maintaining direct access to platform-specific APIs. It's the best of both worlds! - - -For this to work properly, we need to ship a universal client that receives Dioxus edits from a remote process. To make our lives (as developers) a bit easier, the client code is currently implemented in JS (subject to change!). - -The provider (the thing that talks to the client on the other side of the network) is still written in Rust, and is supplied through the dioxus-webview crate. The client is much more "dumb" than the provider - the provider must handle NodeID generation, flushing changes, priorities, managing events, and more. - - -## update; wry! - -Wry is an effort by the Tauri team to make fully native diff --git a/packages/desktop/README.md b/packages/desktop/README.md index df0af040b..3ce4c5820 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -1,5 +1,57 @@ -# Dioxus-Desktop +# Dioxus Desktop (webview) -Because the host VirtualDOM is running in its own native process, native applications can unlock their full potential. Dioxus-Desktop is designed to be a 100% rust alternative to ElectronJS without the memory overhead or bloat of ElectronJS apps. -By bridging the native process, desktop apps can access full multithreading power, peripheral support, hardware access, and native filesystem controls without the hassle of web technologies. Our goal with this desktop crate is to make it easy to ship both a web and native application, and quickly see large performance boosts without having to re-write the whole stack. As the dioxus ecosystem grows, we hope to see 3rd parties providing wrappers for storage, offline mode, etc that supports both web and native technologies. +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-desktop.svg +[crates-url]: https://crates.io/crates/dioxus-desktop + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-desktop/latest/dioxus-desktop) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +`dioxus-desktop` provides a webview-based desktop renderer for the Dioxus VirtualDom. + +This requires that webview is installed on the target system. WebView is installed by default on macOS and iOS devices, but might not come preinstalled on Windows or Linux devices. To fix these issues, follow the [instructions in the guide](guide-url). + +[guide-url]: https://dioxuslabs.com/guide/setup.html#platform-specific-dependencies + + + +## Features + +- Simple, one-line launch for desktop apps +- Dioxus virtualdom running on a native thread +- Full HTML/CSS support via `wry` and `tao` +- Exposed `window` and `Proxy` types from tao for direct window manipulation +- Helpful hooks for + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/packages/hooks/README.md b/packages/hooks/README.md index 14c0e4209..f809aa57e 100644 --- a/packages/hooks/README.md +++ b/packages/hooks/README.md @@ -1,22 +1,58 @@ -# Common hooks for Dioxus +# Dioxus Hooks -This crate includes some basic useful hooks for dioxus: + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-hooks.svg +[crates-url]: https://crates.io/crates/dioxus-hooks + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-hooks/latest/dioxus-hooks) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +`dioxus-hooks` includes some basic useful hooks for dioxus: - use_state - use_ref - use_future - use_coroutine +- use_callback -## use_state +Unlike React, none of these hooks are foundational since they all build off the primitive `cx.use_hook`. -The primary mechanism of stored state. +This crate also provides a few helpful macros to get around some Rust lifetime management issues in async. -You can always use it "normally" with the `split` method: +- `to_owned![]` +- `use_future!()` +- `use_callback!()` -```rust -// Rusty-smart-pointer usage: -let value = use_state(cx, || 10); -// "Classic" usage: -let (value, set_value) = use_state(cx, || 0).split(); -``` +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/packages/html/README.md b/packages/html/README.md index cbc03b0f2..8d04ab042 100644 --- a/packages/html/README.md +++ b/packages/html/README.md @@ -1,4 +1,30 @@ -# Html (and SVG) Namespace for Dioxus +# `dioxus-html`: Html (and SVG) Namespace for Dioxus + + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-html.svg +[crates-url]: https://crates.io/crates/dioxus-html + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-html/latest/dioxus-html) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview The Dioxus `rsx!` and `html!` macros can accept any compile-time correct namespace on top of NodeFactory. This crate provides the HTML (and SVG) namespaces which get imported in the Dioxus prelude. @@ -73,35 +99,20 @@ mod dioxus_elements { } ``` -## Limitations: -- -## How to work around it: -If an attribute in Dioxus is invalid (defined incorrectly) - first, make an issue - but then, you can work around it. The raw builder API is actually somewhat ergonomic to work with, and the NodeFactory type exposes a bunch of methods to make any type of tree - even invalid ones! So obviously, be careful, but there's basically anything you can do. -```rust -cx.render(rsx!{ - div { - h1 {} - // Oh no! I need a super custom element - {LazyNodes::new(move |f| { - f.raw_element( - // tag name - "custom_element", - // attributes - &[f.attr("billy", format_args!("goat"))], - // listeners - &[f.listener(onclick(move |_| {}))], +## Contributing - // children - &[cx.render(rsx!(div {} ))], +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! - // key - None - ) - })} - } -}) -``` +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/packages/interpreter/README.md b/packages/interpreter/README.md index 767fe31c0..af3f7debc 100644 --- a/packages/interpreter/README.md +++ b/packages/interpreter/README.md @@ -1,7 +1,46 @@ -# JS Interpreter +# `dioxus-interpreter-js` -After diffing old and new trees, the Dioxus VirtualDom produces patches that are used to modify the existing Dom. We can send these patches anywhere - including targets without WASM support. -In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets. +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] -For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks. +[crates-badge]: https://img.shields.io/crates/v/dioxus-interpreter-js.svg +[crates-url]: https://crates.io/crates/dioxus-interpreter-js + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-interpreter-js/latest/dioxus-interpreter-js) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +`dioxus-interpreter-js` provides the high-performance JavaScript glue that interprets the stream of edits produced by the Dioxus VirtualDom and converts them into mutations on the actual web DOM. + +This crate features bindings for the web and sledgehammer for increased performance. + + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/packages/interpreter/src/interpreter.old.js b/packages/interpreter/src/interpreter.old.js deleted file mode 100644 index a12639053..000000000 --- a/packages/interpreter/src/interpreter.old.js +++ /dev/null @@ -1,944 +0,0 @@ -export function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.ipc.postMessage(serializeIpcMessage("initialize")); - } -} - -class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = root; - } - - create(event_name, element, handler, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.global[event_name].callback = handler; - this.root.addEventListener(event_name, handler); - } else { - this.global[event_name].active++; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - this.local[id][event_name] = handler; - element.addEventListener(event_name, handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, handler); - } - } -} - -export class Interpreter { - constructor(root) { - this.root = root; - this.lastNode = root; - this.listeners = new ListenerMap(root); - this.handlers = {}; - this.nodes = [root]; - this.parents = []; - } - checkAppendParent() { - if (this.parents.length > 0) { - const lastParent = this.parents[this.parents.length - 1]; - lastParent[1]--; - if (lastParent[1] === 0) { - this.parents.pop(); - } - lastParent[0].appendChild(this.lastNode); - } - } - AppendChildren(root, children) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - for (let i = 0; i < children.length; i++) { - node.appendChild(this.nodes[children[i]]); - } - } - ReplaceWith(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.replaceWith(...els); - } - InsertAfter(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.after(...els); - } - InsertBefore(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.before(...els); - } - Remove(root) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (node !== undefined) { - node.remove(); - } - } - CreateTextNode(text, root) { - this.lastNode = document.createTextNode(text); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - } - CreateElement(tag, root, children) { - this.lastNode = document.createElement(tag); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - if (children > 0) { - this.parents.push([this.lastNode, children]); - } - } - CreateElementNs(tag, root, ns, children) { - this.lastNode = document.createElementNS(ns, tag); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - if (children > 0) { - this.parents.push([this.lastNode, children]); - } - } - CreatePlaceholder(root) { - this.lastNode = document.createElement("pre"); - this.lastNode.hidden = true; - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - } - NewEventListener(event_name, root, handler, bubbles) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.setAttribute("data-dioxus-id", `${root}`); - this.listeners.create(event_name, node, handler, bubbles); - } - RemoveEventListener(root, event_name, bubbles) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.removeAttribute(`data-dioxus-id`); - this.listeners.remove(node, event_name, bubbles); - } - SetText(root, text) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.data = text; - } - SetAttribute(root, field, value, ns) { - const name = field; - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (ns === "style") { - // @ts-ignore - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value !== node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value === "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root, field, ns) { - const name = field; - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (ns == "style") { - node.style.removeProperty(name); - } else if (ns !== null || ns !== undefined) { - node.removeAttributeNS(ns, name); - } else if (name === "value") { - node.value = ""; - } else if (name === "checked") { - node.checked = false; - } else if (name === "selected") { - node.selected = false; - } else if (name === "dangerous_inner_html") { - node.innerHTML = ""; - } else { - node.removeAttribute(name); - } - } - CloneNode(old, new_id) { - let node; - if (old === null) { - node = this.lastNode; - } else { - node = this.nodes[old]; - } - this.nodes[new_id] = node.cloneNode(true); - } - CloneNodeChildren(old, new_ids) { - let node; - if (old === null) { - node = this.lastNode; - } else { - node = this.nodes[old]; - } - const old_node = node.cloneNode(true); - let i = 0; - for (let node = old_node.firstChild; i < new_ids.length; node = node.nextSibling) { - this.nodes[new_ids[i++]] = node; - } - } - FirstChild() { - this.lastNode = this.lastNode.firstChild; - } - NextSibling() { - this.lastNode = this.lastNode.nextSibling; - } - ParentNode() { - this.lastNode = this.lastNode.parentNode; - } - StoreWithId(id) { - this.nodes[id] = this.lastNode; - } - SetLastNode(root) { - this.lastNode = this.nodes[root]; - } - handleEdits(edits) { - for (let edit of edits) { - this.handleEdit(edit); - } - } - handleEdit(edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.root, edit.children); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.nodes); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.nodes); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.nodes); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root, edit.children); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns, edit.children); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event) => { - let target = event.target; - if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); - let shouldPreventDefault = target.getAttribute( - `dioxus-prevent-default` - ); - - if (event.type === "click") { - // todo call prevent default if it's the right type of event - if (shouldPreventDefault !== `onclick`) { - if (target.tagName === "A") { - event.preventDefault(); - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage( - serializeIpcMessage("browser_open", { href }) - ); - } - } - } - - // also prevent buttons from submitting - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - } - // walk the tree to find the real element - while (realId == null) { - // we've reached the root we don't want to send an event - if (target.parentElement === null) { - return; - } - - target = target.parentElement; - realId = target.getAttribute(`data-dioxus-id`); - } - - shouldPreventDefault = target.getAttribute( - `dioxus-prevent-default` - ); - - let contents = serialize_event(event); - - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - if ( - target.tagName === "FORM" && - (event.type === "submit" || event.type === "input") - ) { - for (let x = 0; x < target.elements.length; x++) { - let element = target.elements[x]; - let name = element.getAttribute("name"); - if (name != null) { - if (element.getAttribute("type") === "checkbox") { - // @ts-ignore - contents.values[name] = element.checked ? "true" : "false"; - } else if (element.getAttribute("type") === "radio") { - if (element.checked) { - contents.values[name] = element.value; - } - } else { - // @ts-ignore - contents.values[name] = - element.value ?? element.textContent; - } - } - } - } - - if (realId === null) { - return; - } - realId = parseInt(realId); - window.ipc.postMessage( - serializeIpcMessage("user_event", { - event: edit.event_name, - mounted_dom_id: realId, - contents: contents, - }) - ); - } - }; - this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name)); - - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name, edit.ns); - break; - case "CloneNode": - this.CloneNode(edit.id, edit.new_id); - break; - case "CloneNodeChildren": - this.CloneNodeChildren(edit.id, edit.new_ids); - break; - case "FirstChild": - this.FirstChild(); - break; - case "NextSibling": - this.NextSibling(); - break; - case "ParentNode": - this.ParentNode(); - break; - case "StoreWithId": - this.StoreWithId(BigInt(edit.id)); - break; - case "SetLastNode": - this.SetLastNode(BigInt(edit.id)); - break; - } - } -} - -export function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { - charCode, - key, - altKey, - ctrlKey, - metaKey, - keyCode, - shiftKey, - location, - repeat, - which, - code, - } = event; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - code, - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - return { - value: value, - values: {}, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - - if (target.type === "checkbox") { - value = target.checked ? "true" : "false"; - } - - return { - value: value, - values: {}, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "dblclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - offsetX, - offsetY, - pageX, - pageY, - screenX, - screenY, - shiftKey, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - offset_x: offsetX, - offset_y: offsetY, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - pointerId, - width, - height, - pressure, - tangentialPressure, - tiltX, - tiltY, - twist, - pointerType, - isPrimary, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -function serializeIpcMessage(method, params = {}) { - return JSON.stringify({ method, params }); -} -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; - -function is_element_node(node) { - return node.nodeType == 1; -} - -function event_bubbles(event) { - switch (event) { - case "copy": - return true; - case "cut": - return true; - case "paste": - return true; - case "compositionend": - return true; - case "compositionstart": - return true; - case "compositionupdate": - return true; - case "keydown": - return true; - case "keypress": - return true; - case "keyup": - return true; - case "focus": - return false; - case "focusout": - return true; - case "focusin": - return true; - case "blur": - return false; - case "change": - return true; - case "input": - return true; - case "invalid": - return true; - case "reset": - return true; - case "submit": - return true; - case "click": - return true; - case "contextmenu": - return true; - case "doubleclick": - return true; - case "dblclick": - return true; - case "drag": - return true; - case "dragend": - return true; - case "dragenter": - return false; - case "dragexit": - return false; - case "dragleave": - return true; - case "dragover": - return true; - case "dragstart": - return true; - case "drop": - return true; - case "mousedown": - return true; - case "mouseenter": - return false; - case "mouseleave": - return false; - case "mousemove": - return true; - case "mouseout": - return true; - case "scroll": - return false; - case "mouseover": - return true; - case "mouseup": - return true; - case "pointerdown": - return true; - case "pointermove": - return true; - case "pointerup": - return true; - case "pointercancel": - return true; - case "gotpointercapture": - return true; - case "lostpointercapture": - return true; - case "pointerenter": - return false; - case "pointerleave": - return false; - case "pointerover": - return true; - case "pointerout": - return true; - case "select": - return true; - case "touchcancel": - return true; - case "touchend": - return true; - case "touchmove": - return true; - case "touchstart": - return true; - case "wheel": - return true; - case "abort": - return false; - case "canplay": - return false; - case "canplaythrough": - return false; - case "durationchange": - return false; - case "emptied": - return false; - case "encrypted": - return true; - case "ended": - return false; - case "error": - return false; - case "loadeddata": - return false; - case "loadedmetadata": - return false; - case "loadstart": - return false; - case "pause": - return false; - case "play": - return false; - case "playing": - return false; - case "progress": - return false; - case "ratechange": - return false; - case "seeked": - return false; - case "seeking": - return false; - case "stalled": - return false; - case "suspend": - return false; - case "timeupdate": - return false; - case "volumechange": - return false; - case "waiting": - return false; - case "animationstart": - return true; - case "animationend": - return true; - case "animationiteration": - return true; - case "transitionend": - return true; - case "toggle": - return true; - } -} diff --git a/packages/liveview/README.md b/packages/liveview/README.md index 46ceabf04..3a3721a3b 100644 --- a/packages/liveview/README.md +++ b/packages/liveview/README.md @@ -1,3 +1,55 @@ -# Dioxus LiveView +# Dioxus Liveview -Server rendered apps with minimal latency + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-liveview.svg +[crates-url]: https://crates.io/crates/dioxus-liveview + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-liveview/latest/dioxus-liveview) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +`dioxus-liveview` provides adapters for running the Dioxus VirtualDom over a websocket connection. + +The current backend frameworks supported include: + +- Axum +- Warp +- Salvo + +Dioxus-LiveView exports a number of primitives to wire up an app into an existing backend framework. + +- A threadpool for spawning the `!Send` VirtualDom and interacting with it from the websocket +- An adapter for transforming various socket types into the `LiveViewSocket` type +- The glue to load the interpreter into your app + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/packages/mobile/README.md b/packages/mobile/README.md index 87c6e12a1..ceb043d9c 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -1,11 +1,34 @@ -# Getting started: mobile +# Dioxus Mobile + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-mobile.svg +[crates-url]: https://crates.io/crates/dioxus-mobile + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-mobile/latest/dioxus-mobile) | +[Chat](https://discord.gg/XgGxMSkvUM) -Dioxus is unique in that it actually supports mobile. However, support is very young and you might need to dip down into some of the primitives until better supported is ready. +## Overview -Currently, only iOS is supported through us, however you *can* add android support by following the same instructions below, but using the `android` guide in `cargo-mobile`. +`dioxus-mobile` is a re-export of `dioxus-desktop` with some minor tweaks and documentation changes. As this crate evolves, it will provide some more unique features to mobile, but for now, it's very similar to the desktop crate. + +Dioxus Mobile supports both iOS and Android. However, Android support is still quite experimental and requires a lot of configuration. A good area to contribute here would be to improve the CLI tool to include bundling and mobile configuration. -Also, Dioxus Desktop and Dioxus Mobile share the same codebase, and dioxus-mobile currently just re-exports dioxus-desktop. ## Getting Set up @@ -76,3 +99,19 @@ To configure the webview, menubar, and other important desktop-specific features ## Future Steps Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! + + + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/packages/native-core-macro/README.md b/packages/native-core-macro/README.md new file mode 100644 index 000000000..a8c9db60b --- /dev/null +++ b/packages/native-core-macro/README.md @@ -0,0 +1,43 @@ +# Dioxus Native Core Macro + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-native-core-macro.svg +[crates-url]: https://crates.io/crates/dioxus-native-core-macro + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-native-core-macro/latest/dioxus-native-core-macro) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +`dioxus-native-core-macro` provides a handful of macros used by native-core for native renderers like TUI, Blitz, and Freya to derive their own state. + + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/packages/native-core/README.md b/packages/native-core/README.md index b35a8df5a..210d95ba0 100644 --- a/packages/native-core/README.md +++ b/packages/native-core/README.md @@ -1 +1,45 @@ -# Dioxus Native-Core: An lazy approach to GUI state and trees +# Dioxus Native Core + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-native-core.svg +[crates-url]: https://crates.io/crates/dioxus-native-core + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-native-core/latest/dioxus-native-core) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +`dioxus-native-core` provides a number of helpful utilities for lazily resolving computed values of the Dioxus VirtualDom to be used in conjunction with a native rendering engine. + +The main "value-add" of this crate over implementing your own native tree is that this tree is incrementally recomputed using the Dioxus VirtualDom's edit stream. Only parts of the tree that rely on each other will be redrawn - all else will be ignored. + + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/packages/router/README.md b/packages/router/README.md index ffd04ef0e..f5db366a9 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -1,55 +1,31 @@ -
-

Dioxus Router

-
+# Dioxus Native Core + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-router.svg +[crates-url]: https://crates.io/crates/dioxus-router + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-router/latest/dioxus-router) | +[Chat](https://discord.gg/XgGxMSkvUM) -
- - - Crates.io version - - - - Download - - - - docs.rs docs - - - - CI status - +## Overview - - - Awesome Page - - - - Discord Link - -
- - - - - -Dioxus Router is a first-party Router for all your Dioxus Apps. It provides a React-Router style interface that works anywhere: across the browser, SSR, and natively. +Dioxus Router is a first-party Router for all your Dioxus Apps. It provides a React-Router style interface using somewhat loose typing rules. ```rust, ignore fn app() { @@ -63,10 +39,19 @@ fn app() { } ``` +You need to enable the right features for the platform you're targeting since these are not determined automatically! -## Resources +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. -- See the mdbook -- See the one-page brief -- See the guide on the doc site -- The crates.io API diff --git a/packages/rsx-rosetta/README.md b/packages/rsx-rosetta/README.md index 35f86d00a..bf811a5fe 100644 --- a/packages/rsx-rosetta/README.md +++ b/packages/rsx-rosetta/README.md @@ -1,5 +1,30 @@ -# Rosetta for RSX ---- + + +# `rsx-rosetta` + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/rsx-rosetta.svg +[crates-url]: https://crates.io/crates/rsx-rosetta + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/rsx-rosetta/latest/rsx-rosetta) | +[Chat](https://discord.gg/XgGxMSkvUM) + +## Overview Dioxus sports its own templating language inspired by C#/Kotlin/RTMP, etc. It's pretty straightforward. @@ -17,3 +42,18 @@ This library provids a central AST that can accept a number of inputs: - Jinja (todo) From there, you can convert directly to a string or into some other AST. + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. + diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 4e5d583ae..e6a7872b1 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -81,7 +81,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { } } -/// Pull out all the svgs from the body and replace them with components +/// Pull out all the svgs from the body and replace them with components of the same name pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec) { for child in children { match child { diff --git a/packages/rsx/README.md b/packages/rsx/README.md index 920fcd88c..7d99bcae5 100644 --- a/packages/rsx/README.md +++ b/packages/rsx/README.md @@ -1 +1,43 @@ -# The actual RSX language implemented using syn parsers. +# Dioxus-RSX + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-rsx.svg +[crates-url]: https://crates.io/crates/dioxus-rsx + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-rsx/latest/dioxus-rsx) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +This crate provides the actual DSL that Dioxus uses in the `rsx!` macro. This crate is separate from the macro crate to enable tooling like autoformat, translation, and AST manipulation (extract to component). + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. + diff --git a/packages/ssr/README.md b/packages/ssr/README.md index bcaea3044..7619d6626 100644 --- a/packages/ssr/README.md +++ b/packages/ssr/README.md @@ -51,33 +51,6 @@ let _ = dom.rebuild(); let content = dioxus_ssr::render_vdom(&dom); ``` -## Configuring output -It's possible to configure the output of the generated HTML. - -```rust, ignore -let content = dioxus_ssr::render_vdom(&dom, |config| config.pretty(true).prerender(true)); -``` - -## Usage as a writer - -We provide the basic `SsrFormatter` object that implements `Display`, so you can integrate SSR into an existing string, or write directly to a file. - -```rust, ignore -use std::fmt::{Error, Write}; - -let mut buf = String::new(); - -let dom = VirtualDom::new(app); -let _ = dom.rebuild(); - -let args = dioxus_ssr::formatter(dom, |config| config); -buf.write_fmt!(format_args!("{}", args)); -``` - -## Configuration - - - @@ -95,7 +68,7 @@ To enable pre-rendering, simply configure the `SsrConfig` with pre-rendering ena ```rust, ignore let dom = VirtualDom::new(App); -let text = dioxus_ssr::render_vdom(App, |cfg| cfg.pre_render(true)); +let text = dioxus_ssr::render_vdom(App, Config { pre_render: true, ..Default::default() }); ``` ## Usage in server-side rendering diff --git a/packages/web/README.md b/packages/web/README.md index f2c8a9c75..0789ac522 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -1,11 +1,46 @@ -# Dioxus-Web +# Dioxus-web -Build interactive user experiences directly in the web browser! +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] -Dioxus-web provides a `WebsysRenderer` for the Dioxus Virtual Dom that handles events, progresses components, and updates the actual DOM using web-sys methods. +[crates-badge]: https://img.shields.io/crates/v/rsx-rosetta.svg +[crates-url]: https://crates.io/crates/rsx-rosetta +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/rsx-rosetta/latest/rsx-rosetta) | +[Chat](https://discord.gg/XgGxMSkvUM) + +## Overview + +Run Dioxus in the browser using WebAssembly. + +- Relies on sledgehammer and websys to modify the dom +- Supports instant hotreloading via the Dioxus CLI +- Around 60k gzipped + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. -## Web-specific Optimizations -- Uses string interning of all common node types -- Optimistically interns short strings -- Builds trees completely before mounting them From 6dc9da3398733e953d01be656b29cdace06afa0b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 16:20:20 -0500 Subject: [PATCH 13/29] chore: remove old file --- notes/SOLVEDPROBLEMS.md | 494 ---------------------------------------- 1 file changed, 494 deletions(-) delete mode 100644 notes/SOLVEDPROBLEMS.md diff --git a/notes/SOLVEDPROBLEMS.md b/notes/SOLVEDPROBLEMS.md deleted file mode 100644 index b978c923d..000000000 --- a/notes/SOLVEDPROBLEMS.md +++ /dev/null @@ -1,494 +0,0 @@ -# Solved problems while building Dioxus - -focuses: - -- ergonomics -- render agnostic -- remote coupling -- memory efficient -- concurrent -- global context -- scheduled updates -- - -## FC Macro for more elegant components - -Originally the syntax of the FC macro was meant to look like: - -```rust -#[fc] -fn example(cx: &Context<{ name: String }>) -> DomTree { - html! {
"Hello, {name}!"
} -} -``` - -`Context` was originally meant to be more obviously parameterized around a struct definition. However, while this works with rustc, this does not work well with Rust Analyzer. Instead, the new form was chosen which works with Rust Analyzer and happens to be more ergonomic. - -```rust -#[fc] -fn example(cx: &Context, name: String) -> DomTree { - html! {
"Hello, {name}!"
} -} -``` - -## Anonymous Components - -In Yew, the function_component macro turns a struct into a Trait `impl` with associated type `props`. Like so: - -```rust -#[derive(Properties)] -struct Props { - // some props -} - -struct SomeComponent; -impl FunctionProvider for SomeComponent { - type TProps = Props; - - fn run(&mut self, props: &Props) -> Html { - // user's functional component goes here - } -} - -pub type SomeComponent = FunctionComponent; -``` - -By default, the underlying component is defined as a "functional" implementation of the `Component` trait with all the lifecycle methods. In Dioxus, we don't allow components as structs, and instead take a "hooks-only" approach. However, we still need cx. To get these without dealing with traits, we just assume functional components are modules. This lets the macros assume an FC is a module, and `FC::Props` is its props and `FC::component` is the component. Yew's method does a similar thing, but with associated types on traits. - -Perhaps one day we might use traits instead. - -The FC macro needs to work like this to generate a final module signature: - -```rust -// "Example" can be used directly -// The "associated types" are just children of the module -// That way, files can just be components (yay, no naming craziness) -mod Example { - // Associated metadata important for liveview - static NAME: &'static str = "Example"; - - struct Props { - name: String - } - - fn component(cx: &Context) -> DomTree { - html! {
"Hello, {name}!"
} - } -} - -// or, Example.rs - -static NAME: &'static str = "Example"; - -struct Props { - name: String -} - -fn component(cx: &Context) -> DomTree { - html! {
"Hello, {name}!"
} -} -``` - -These definitions might be ugly, but the fc macro cleans it all up. The fc macro also allows some configuration - -```rust -#[fc] -fn example(cx: &Context, name: String) -> DomTree { - html! {
"Hello, {name}!"
} -} - -// .. expands to - -mod Example { - use super::*; - static NAME: &'static str = "Example"; - struct Props { - name: String - } - fn component(cx: &Context) -> DomTree { - html! {
"Hello, {name}!"
} - } -} -``` - -## Live Components - -Live components are a very important part of the Dioxus ecosystem. However, the goal with live components was to constrain their implementation purely to APIs available through Context (concurrency, context, subscription). - -From a certain perspective, live components are simply server-side-rendered components that update when their props change. Here's more-or-less how live components work: - -```rust -#[fc] -static LiveFc: FC = |cx, refresh_handler: impl FnOnce| { - // Grab the "live context" - let live_context = cx.use_context::(); - - // Ensure this component is registered as "live" - live_context.register_scope(); - - // send our props to the live context and get back a future - let vnodes = live_context.request_update(cx); - - // Suspend the rendering of this component until the vnodes are finished arriving - // Render them once available - cx.suspend(async move { - let output = vnodes.await; - - // inject any listener handles (ie button clicks, views, etc) to the parsed nodes - output[1].add_listener("onclick", refresh_handler); - - // Return these nodes - // Nodes skip diffing and go straight to rendering - output - }) -} -``` - -Notice that LiveComponent receivers (the client-side interpretation of a LiveComponent) are simply suspended components waiting for updates from the LiveContext (the context that wraps the app to make it "live"). - -## Allocation Strategy (ie incorporating Dodrio research) - ---- - -The `VNodeTree` type is a very special type that allows VNodes to be created using a pluggable allocator. The html! macro creates something that looks like: - -```rust -pub static Example: Component = |cx| { - html! {
"blah"
} -}; - -// expands to... - -pub static Example: Component = |cx| { - // This function converts a Fn(allocator) -> DomTree closure to a VNode struct that will later be evaluated. - html_macro_to_vnodetree(move |allocator| { - let mut node0 = allocator.alloc(VElement::div); - let node1 = allocator.alloc_text("blah"); - node0.children = [node1]; - node0 - }) -}; -``` - -At runtime, the new closure is created that captures references to `cx`. Therefore, this closure can only be evaluated while `cx` is borrowed and in scope. However, this closure can only be evaluated with an `allocator`. Currently, the global and Bumpalo allocators are available, though in the future we will add support for creating a VDom with any allocator or arena system (IE Jemalloc, wee-alloc, etc). The intention here is to allow arena allocation of VNodes (no need to box nested VNodes). Between diffing phases, the arena will be overwritten as old nodes are replaced with new nodes. This saves allocation time and enables bump allocators. - -## Context and lifetimes - -We want components to be able to fearlessly "use_context" for use in state management solutions. - -However, we cannot provide these guarantees without compromising the references. If a context mutates, it cannot lend out references. - -Functionally, this can be solved with UnsafeCell and runtime dynamics. Essentially, if a context mutates, then any affected components would need to be updated, even if they themselves aren't updated. Otherwise, a reference would be pointing at data that could have potentially been moved. - -To do this safely is a pretty big challenge. We need to provide a method of sharing data that is safe, ergonomic, and that fits the abstraction model. - -Enter, the "ContextGuard". - -The "ContextGuard" is very similar to a Ref/RefMut from the RefCell implementation, but one that derefs into actual underlying value. - -However, derefs of the ContextGuard are a bit more sophisticated than the Ref model. - -For RefCell, when a Ref is taken, the RefCell is now "locked." This means you cannot take another `borrow_mut` instance while the Ref is still active. For our purposes, our modification phase is very limited, so we can make more assumptions about what is safe. - -1. We can pass out ContextGuards from any use of use_context. These don't actually lock the value until used. -2. The ContextGuards only lock the data while the component is executing and when a callback is running. -3. Modifications of the underlying context occur after a component is rendered and after the event has been run. - -With the knowledge that usage of ContextGuard can only be achieved in a component context and the above assumptions, we can design a guard that prevents any poor usage but also is ergonomic. - -As such, the design of the ContextGuard must: - -- be /copy/ for easy moves into closures -- never point to invalid data (no dereferencing of raw pointers after movable data has been changed (IE a vec has been resized)) -- not allow references of underlying data to leak into closures - -To solve this, we can be clever with lifetimes to ensure that any data is protected, but context is still ergonomic. - -1. As such, deref context guard returns an element with a lifetime bound to the borrow of the guard. -2. Because we cannot return locally borrowed data AND we consume context, this borrow cannot be moved into a closure. -3. ContextGuard is _copy_ so the guard itself can be moved into closures -4. ContextGuard derefs with its unique lifetime _inside_ closures -5. Derefing a ContextGuard evaluates the underlying selector to ensure safe temporary access to underlying data - -```rust -struct ExampleContext { - // unpinnable objects with dynamic sizing - items: Vec -} - -fn Example<'src>(cx: Scope<'src, ()>) -> DomTree<'src> { - let val: &'b ContextGuard = (&'b cx).use_context(|context: &'other ExampleContext| { - // always select the last element - context.items.last() - }); - - let handler1 = move |_| println!("Value is {}", val); // deref coercion performed here for printing - let handler2 = move |_| println!("Value is {}", val); // deref coercion performed here for printing - - cx.render(html! { -
- - -
-

"Value is: {val}"

-
-
- }) -} -``` - -A few notes: - -- this does _not_ protect you from data races!!! -- this does _not_ force rendering of components -- this _does_ protect you from invalid + UB use of data -- this approach leaves lots of room for fancy state management libraries -- this approach is fairly quick, especially if borrows can be cached during usage phases - -## Concurrency - -For Dioxus, concurrency is built directly into the VirtualDOM lifecycle and event system. Suspended components prove "no changes" while diffing, and will cause a lifecycle update later. This is considered a "trigger" and will cause targeted diffs and re-renders. Renderers will need to await the Dioxus suspense queue if they want to process these updates. This will typically involve joining the suspense queue and event listeners together like: - -```rust -// wait for an even either from the suspense queue or our own custom listener system -let (left, right) = join!(vdom.suspense_queue, self.custom_event_listener); -``` - -LiveView is built on this model, and updates from the WebSocket connection to the host server are treated as external updates. This means any renderer can feed targeted EditLists (the underlying message of this event) directly into the VirtualDOM. - -## Execution Model - - - -## Diffing - -Diffing is an interesting story. Since we don't re-render the entire DOM, we need a way to patch up the DOM without visiting every component. To get this working, we need to think in cycles, queues, and stacks. Most of the original logic is pulled from Dodrio as Dioxus and Dodrio share much of the same DNA. - -When an event is triggered, we find the callback that installed the listener and run it. We then record all components affected by the running of the "subscription" primitive. In practice, many hooks will initiate a subscription, so it is likely that many components throughout the entire tree will need to be re-rendered. For each component, we attach its index and the type of update it needs. - -In practice, removals trump prop updates which trump subscription updates. Therefore, we only process updates where props are directly changed first, as this will likely flow into child components. - -Roughly, the flow looks like: - -- Process the initiating event -- Mark components affected by the subscription API (the only way of causing forward updates) -- Descend from the root into children, ignoring those not affected by the subscription API. (walking the tree until we hit the first affected component, or choosing the highest component) -- Run this component and then immediately diff its output, marking any children that also need to be updated and putting them into the immediate queue -- Mark this component as already-ran and remove it from the need_to_diff list, instead moving it into the "already diffed list" -- Run the marked children until the immediate queue is empty - -```rust -struct DiffMachine { - immediate_queue: Vec, - diffed: HashSet, - need_to_diff: HashSet - marked_for_removal: Vec -} -``` - -On the actual diffing level, we're using the diffing algorithm pulled from Dodrio, but plan to move to a dedicated crate that implements Meyers/Patience for us. During the diffing phase, we track our current position using a "Traversal" which implements the "MoveTo". When "MoveTo" is combined with "Edit", it is possible for renderers to fully interpret a series of Moves and Edits together to update their internal node structures for rendering. - -## Patch Stream - -One of the most important parts of Dioxus is the ability to stream patches from server to client. However, this inherently has challenges where raw VNodes attach listeners to themselves, and are therefore not serializable. - -### How do properties work? - -How should properties passing work? Should we directly call the child? Should we box the props? Should we replace the pops inside the box? - -Here's (generally) my head is at: - -Components need to store their props on them if they want to be updated remotely. These props _can_ be updated after the fact. - -Perf concerns: -unnecessary function runs - list-y components - hook calls? - making vnodes? - -Does any of this matter? -Should we just run any component we see, immediately and imperatively? That will cause checks throughout the whole tree, no matter where the update occurred - -https://calendar.perfplanet.com/2013/diff/ - -Here's how react does it: - -Any "dirty" node causes an entire subtree render. Calling "setState" at the very top will cascade all the way down. This is particularly bad for this component design: - -```rust -static APP: Component = |cx| { - let title = use_context(Title); - cx.render(html!{ -
-

"{title}"

- // VComponent::new(|| (FC, PropsForFc)) -> needs a context to immediately update the component's props imperatively? store the props in a box on bump? store the props on the child? - // if props didnt change, then let the refernece stay invalid?.... no, cant do that, bump gets reset - // immediately update props on the child component if it can be found? -> interesting, feels wrong, but faster, at the very least. - // can box on bump for the time being (fast enough), and then move it over? during the comparison phase? props only need to matter - // cant downcast (can with transmute, but yikes) - // how does chain borrowing work? a -> b -> c -> d - // if b gets marked as dirty, then c and d are invalidated (semantically, UB, but not *bad* UB, just data races) - // make props static? -> easy to move, gross to use - // - // treat like a context selector? - // use_props::

(2) - // child_props: Map> - // vs children: BTreeSet -> to get nth -

- }) -}; -static HEAVY_LIST: Component = |cx| { - cx.render({ - {0.100.map(i => )} - }) -}; -``` - -An update to the use_context subscription will mark the node as dirty. The node then is forced to re-analyze HeavyList, even though HeavyList did not change. We should automatically implement this suppression knowing that props are immutable and can be partialeq. - -## FC Layout - -The FC layout was altered to make life easier for us inside the VirtualDom. The "view" function returns an unbounded VNode object. Calling the "view" function is unsafe under the hood, but prevents lifetimes from leaking out of the function call. Plus, it's easier to write. Because there are no lifetimes on the output (occur purely under the hood), we can escape needing to annotate them. - -```rust -fn component(cx: Scope) -> DomTree { - -} -``` - -The VNode object purely represents a viewable "key". It also forces components to use the "view" function as there is no other way to generate the VNode object. Because the VNode is a required type of FC, we can guarantee the same usage and flow patterns for all components. - -## Events - -Events are finally in! To do events properly, we are abstracting over the event source with synthetic events. This forces 3rd party renderers to create the appropriate cross-platform event - -## Optional Props on Components - -A major goal here is ergonomics. Any field that is Option should default to none. - -```rust - -rsx! { - Example { /* args go here */ a: 10, b: 20 } -} - - -``` - -```rust -#[derive(Properties)] -struct Props { - -} - -static Component: Component = |cx| { - -} -``` - -or - -```rust -#[fc] -static Component: FC = |cx, name: &str| { - -} -``` - -## Noderefs - -How do we resolve noderefs in a world of patches? Patches _must_ be serializable, so if we do something like `Option<&RefCell>`, then that must serialize as _something_ to indicate to a remote host that access to the node itself is desired. Our `Slot` type will need to be somewhat abstract. - -If we add a new patch type called "BindRef" we could do something like: - -```rust -enum Patch { - //... - BindAsRef { raw_node: &RefCell> } -} -``` - -```rust -let node_ref = use_node_ref(cx); -use_effect(cx, || { - -}, []); -div { ref: node_ref, - "hello me" - h3 {"yo dom"} -} -``` - -refs only work when you're native to the platform. it doesn't make sense to gain a ref when you're not native. - -## In-sync or separate? - -React makes refs - and protection against dom manipulation - work by modifying the real dom while diffing the virtual dom. This lets it bind real dom elements to the virtual dom elements. Dioxus currently does not do this, instead creating a list of changes for an interpreter to apply once diffing has completed. - -This behavior fit dodrio well as all dom manipulations would occur batched. The original intention for this approach was to make it faster to read out of Wasm and into JS. Dodrio is essentially performing the Wasm job that Wasm<->JS for strings does. In theory, this particular optimization is not necessary. - -https://github.com/fitzgen/dodrio/issues/77 - -This issue/pr on the dodrio repository points to a future where elements are held on to by the virtualdom. - -Can we solve events, refs, and protection against 3rd party dom mutation all in one shot? - -I think we can.... - -every node gets a globally unique ID - -abstract the real dom - -```rust - -struct VirtualDom - -trait RealDom { - type Node: RealNode; - fn get_node(&self, id: u32) -> &Self::Node; - fn get_node_mut(&mut self, id: u32) -> &mut Self::Node; - fn replace_node(); - fn create_text_node(); - fn create_element(); - fn create_element_ns(); -} - -trait RealNode { - fn add_listener(&mut self, event: &str); - fn set_inner_text(&mut self, text: &str); - fn set_attr(&mut self, name, value); - fn set_class(&mut self); - fn remove_attr(&mut self); - // We can't have a generic type in trait objects, so instead we provide the inner as Any - fn raw_node_as_any_mut(&mut self) -> &mut dyn Any; -} - -impl VirtualDom { - fn diff() { - - } -} -enum VNode<'bump, 'realdom, RealDom> { - VElement { - real: &RealDom::Node - } - VText { - real: &RealDom::Node - } -} - - -fn main() { - let mut real_dom = websys::Document(); - let virtual_dom = Dioxus::VirtualDom::new(); - - virtual_dom.rebuild(&mut real_dom); - - loop { - let event = switch! { - real_dom.events.await => event, - virtual_dom.inner_events.await => event - }; - - virtual_dom.apply_event(&mut real_dom, event); - } -} - -``` From e22f4ad6cfd4f59d6e91fc95fd1cd2409c2aab1d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 16:21:24 -0500 Subject: [PATCH 14/29] fix: resolve clippy warnings --- packages/html/src/events/drag.rs | 2 +- packages/html/src/events/mouse.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index f8ee6ff92..fa3219631 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -9,7 +9,7 @@ pub type DragEvent = Event; /// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an /// application-specific way. #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct DragData { /// Inherit mouse data pub mouse: MouseData, diff --git a/packages/html/src/events/mouse.rs b/packages/html/src/events/mouse.rs index f8446b7d1..7899393ff 100644 --- a/packages/html/src/events/mouse.rs +++ b/packages/html/src/events/mouse.rs @@ -10,7 +10,7 @@ pub type MouseEvent = Event; /// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Default, PartialEq)] +#[derive(Clone, Default, PartialEq, Eq)] /// Data associated with a mouse event /// /// Do not use the deprecated fields; they may change or become private in the future. From ff697d41e39edd9616bc6eef038f5c337c0556a4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 17:11:29 -0500 Subject: [PATCH 15/29] wip: move desktop to main thread and use tokio main --- examples/readme.rs | 5 +- packages/desktop/src/controller.rs | 212 ++++++++++++------------ packages/desktop/src/desktop_context.rs | 10 +- packages/desktop/src/lib.rs | 132 +++++++++++---- 4 files changed, 218 insertions(+), 141 deletions(-) diff --git a/examples/readme.rs b/examples/readme.rs index f4668488b..7e574904f 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -4,8 +4,9 @@ use dioxus::prelude::*; -fn main() { - dioxus_desktop::launch(app); +#[tokio::main] +async fn main() { + dioxus_desktop::launch(app).await; } fn app(cx: Scope) -> Element { diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 2d08ad79a..0d198da21 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -21,7 +21,7 @@ use wry::{ pub(super) struct DesktopController { pub(super) webviews: HashMap, pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, - pub(super) pending_edits: Arc>>, + pub(super) quit_app_on_close: bool, pub(super) is_ready: Arc, pub(super) proxy: EventLoopProxy, @@ -35,97 +35,6 @@ pub(super) struct DesktopController { } impl DesktopController { - // Launch the virtualdom on its own thread managed by tokio - // returns the desktop state - pub(super) fn new_on_tokio( - root: Component

, - props: P, - proxy: EventLoopProxy, - ) -> Self { - let edit_queue = Arc::new(Mutex::new(Vec::new())); - let (event_tx, mut event_rx) = unbounded(); - let (templates_tx, mut templates_rx) = unbounded(); - let proxy2 = proxy.clone(); - - let pending_edits = edit_queue.clone(); - let desktop_context_proxy = proxy.clone(); - let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::(); - - std::thread::spawn(move || { - // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads - // I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it - - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - - runtime.block_on(async move { - let mut dom = VirtualDom::new_with_props(root, props) - .with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever)); - { - let edits = dom.rebuild(); - let mut queue = edit_queue.lock().unwrap(); - queue.push(serde_json::to_string(&edits).unwrap()); - proxy.send_event(UserWindowEvent::EditsReady).unwrap(); - } - - loop { - tokio::select! { - template = { - #[allow(unused)] - fn maybe_future<'a>(templates_rx: &'a mut UnboundedReceiver>) -> impl Future> + 'a { - #[cfg(debug_assertions)] - return templates_rx.select_next_some(); - #[cfg(not(debug_assertions))] - return std::future::pending(); - } - maybe_future(&mut templates_rx) - } => { - dom.replace_template(template); - } - _ = dom.wait_for_work() => {} - Some(json_value) = event_rx.next() => { - if let Ok(value) = serde_json::from_value::(json_value) { - let HtmlEvent { - name, - element, - bubbles, - data - } = value; - dom.handle_event(&name, data.into_any(), element, bubbles); - } - } - } - - let muts = dom - .render_with_deadline(tokio::time::sleep(Duration::from_millis(16))) - .await; - - edit_queue - .lock() - .unwrap() - .push(serde_json::to_string(&muts).unwrap()); - let _ = proxy.send_event(UserWindowEvent::EditsReady); - } - }) - }); - - Self { - pending_edits, - eval_sender, - webviews: HashMap::new(), - is_ready: Arc::new(AtomicBool::new(false)), - quit_app_on_close: true, - proxy: proxy2, - event_tx, - #[cfg(debug_assertions)] - templates_tx, - #[cfg(target_os = "ios")] - views: vec![], - } - } - pub(super) fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { self.webviews.remove(&window_id); @@ -134,21 +43,112 @@ impl DesktopController { } } - pub(super) fn try_load_ready_webviews(&mut self) { - if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { - let mut new_queue = Vec::new(); + // pub(super) fn try_load_ready_webviews(&mut self) { + // if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { + // let mut new_queue = Vec::new(); - { - let mut queue = self.pending_edits.lock().unwrap(); - std::mem::swap(&mut new_queue, &mut *queue); - } + // { + // let mut queue = self.pending_edits.lock().unwrap(); + // std::mem::swap(&mut new_queue, &mut *queue); + // } - let (_id, view) = self.webviews.iter_mut().next().unwrap(); + // let (_id, view) = self.webviews.iter_mut().next().unwrap(); - for edit in new_queue.drain(..) { - view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) - .unwrap(); - } - } - } + // for edit in new_queue.drain(..) { + // view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) + // .unwrap(); + // } + // } + // } } + +// // Launch the virtualdom on its own thread managed by tokio +// // returns the desktop state +// pub(super) fn new_on_tokio( +// root: Component

, +// props: P, +// proxy: EventLoopProxy, +// ) -> Self { +// let edit_queue = Arc::new(Mutex::new(Vec::new())); +// let (event_tx, mut event_rx) = unbounded(); +// let (templates_tx, mut templates_rx) = unbounded(); +// let proxy2 = proxy.clone(); + +// let pending_edits = edit_queue.clone(); +// let desktop_context_proxy = proxy.clone(); +// let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::(); + +// std::thread::spawn(move || { +// // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads +// // I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it + +// let runtime = tokio::runtime::Builder::new_multi_thread() +// .enable_all() +// .build() +// .unwrap(); + +// runtime.block_on(async move { +// let mut dom = VirtualDom::new_with_props(root, props) +// .with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever)); +// { +// let edits = dom.rebuild(); +// let mut queue = edit_queue.lock().unwrap(); +// queue.push(serde_json::to_string(&edits).unwrap()); +// proxy.send_event(UserWindowEvent::EditsReady).unwrap(); +// } + +// loop { +// tokio::select! { +// template = { +// #[allow(unused)] +// fn maybe_future<'a>(templates_rx: &'a mut UnboundedReceiver>) -> impl Future> + 'a { +// #[cfg(debug_assertions)] +// return templates_rx.select_next_some(); +// #[cfg(not(debug_assertions))] +// return std::future::pending(); +// } +// maybe_future(&mut templates_rx) +// } => { +// dom.replace_template(template); +// } +// _ = dom.wait_for_work() => {} +// Some(json_value) = event_rx.next() => { +// if let Ok(value) = serde_json::from_value::(json_value) { +// let HtmlEvent { +// name, +// element, +// bubbles, +// data +// } = value; +// dom.handle_event(&name, data.into_any(), element, bubbles); +// } +// } +// } + +// let muts = dom +// .render_with_deadline(tokio::time::sleep(Duration::from_millis(16))) +// .await; + +// edit_queue +// .lock() +// .unwrap() +// .push(serde_json::to_string(&muts).unwrap()); +// let _ = proxy.send_event(UserWindowEvent::EditsReady); +// } +// }) +// }); + +// Self { +// pending_edits, +// eval_sender, +// webviews: HashMap::new(), +// is_ready: Arc::new(AtomicBool::new(false)), +// quit_app_on_close: true, +// proxy: proxy2, +// event_tx, +// #[cfg(debug_assertions)] +// templates_tx, +// #[cfg(target_os = "ios")] +// views: vec![], +// } +// } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 943fbf91e..929f1ce4a 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -175,6 +175,8 @@ pub enum UserWindowEvent { EditsReady, Initialize, + Poll, + CloseWindow, DragWindow, FocusWindow, @@ -226,7 +228,9 @@ impl DesktopController { let window = webview.window(); match user_event { - Initialize | EditsReady => self.try_load_ready_webviews(), + Initialize | EditsReady => { + // self.try_load_ready_webviews(); + } CloseWindow => *control_flow = ControlFlow::Exit, DragWindow => { // if the drag_window has any errors, we don't do anything @@ -268,6 +272,10 @@ impl DesktopController { SetZoomLevel(scale_factor) => webview.zoom(scale_factor), SetInnerSize(logical_size) => window.set_inner_size(logical_size), + Poll => { + // todo + } + Print => { if let Err(e) = webview.print() { // we can't panic this error. diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 337a9485c..14ce5f2f7 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -13,12 +13,20 @@ mod protocol; #[cfg(all(feature = "hot-reload", debug_assertions))] mod hot_reload; +use futures_util::task::ArcWake; +use serde_json::Value; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; use std::sync::atomic::AtomicBool; use std::sync::Arc; +use std::task::Waker; use desktop_context::UserWindowEvent; pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult}; use futures_channel::mpsc::UnboundedSender; +use futures_util::future::poll_fn; +use futures_util::{pin_mut, FutureExt}; pub use wry; pub use wry::application as tao; @@ -33,6 +41,8 @@ use tao::{ event_loop::{ControlFlow, EventLoop}, window::Window, }; +use wry::application::event_loop::EventLoopProxy; +use wry::application::platform::run_return::EventLoopExtRunReturn; use wry::webview::WebViewBuilder; /// Launch the WebView and run the event loop. @@ -52,8 +62,8 @@ use wry::webview::WebViewBuilder; /// }) /// } /// ``` -pub fn launch(root: Component) { - launch_with_props(root, (), Config::default()) +pub async fn launch(root: Component) { + launch_with_props(root, (), Config::default()).await } /// Launch the WebView and run the event loop, with configuration. @@ -75,12 +85,14 @@ pub fn launch(root: Component) { /// }) /// } /// ``` -pub fn launch_cfg(root: Component, config_builder: Config) { - launch_with_props(root, (), config_builder) +pub async fn launch_cfg(root: Component, config_builder: Config) { + launch_with_props(root, (), config_builder).await } /// Launch the WebView and run the event loop, with configuration and root props. /// +/// THIS WILL BLOCK THE CURRENT THREAD +/// /// This function will start a multithreaded Tokio runtime as well the WebView event loop. /// /// You can configure the WebView window with a configuration closure @@ -102,28 +114,92 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// }) /// } /// ``` -pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { - let event_loop = EventLoop::with_user_event(); - let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy()); +pub async fn launch_with_props(root: Component

, props: P, mut cfg: Config) { + let mut event_loop = EventLoop::with_user_event(); - #[cfg(debug_assertions)] - hot_reload::init(desktop.templates_tx.clone()); + let is_ready = Arc::new(AtomicBool::new(false)); + let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel(); + + let mut dom = VirtualDom::new_with_props(root, props).with_root_context(DesktopContext::new( + event_loop.create_proxy(), + eval_reciever, + )); + + let proxy = event_loop.create_proxy(); + + let waker = futures_util::task::waker(Arc::new(DomHandle { + proxy: proxy.clone(), + })); + + let mut events = Rc::new(RefCell::new(vec![])); + let mut webviews = HashMap::new(); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; match window_event { - Event::NewEvents(StartCause::Init) => desktop.start(&mut cfg, event_loop), + Event::NewEvents(StartCause::Init) => { + let window = build_webview( + &mut cfg, + event_loop, + is_ready.clone(), + proxy.clone(), + eval_sender.clone(), + events.clone(), + ); - Event::WindowEvent { - event, window_id, .. - } => match event { + webviews.insert(window.window().id(), window); + + // desktop.start(&mut cfg, event_loop); + } + + Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow), + WindowEvent::Destroyed { .. } => { + // desktop.close_window(window_id, control_flow); + } _ => {} }, - Event::UserEvent(user_event) => desktop.handle_event(user_event, control_flow), + Event::UserEvent(user_event) => { + println!("user event: {:?}", user_event); + + match user_event { + UserWindowEvent::Poll => { + let mut cx = std::task::Context::from_waker(&waker); + + let render = { + let fut = dom.wait_for_work(); + pin_mut!(fut); + matches!(fut.poll_unpin(&mut cx), std::task::Poll::Ready(_)) + }; + + if render { + let edits = dom.render_immediate(); + + // apply the edits + } + } + + UserWindowEvent::EditsReady => { + let edits = dom.rebuild(); + + let (_id, view) = webviews.iter_mut().next().unwrap(); + + let serialized = serde_json::to_string(&edits).unwrap(); + + view.evaluate_script(&format!( + "window.interpreter.handleEdits({})", + serialized + )) + .unwrap(); + } + + other => { + // desktop.handle_event(user_event, control_flow); + } + } + } Event::MainEventsCleared => {} Event::Resumed => {} Event::Suspended => {} @@ -134,22 +210,13 @@ pub fn launch_with_props(root: Component

, props: P, mut cf }) } -impl DesktopController { - fn start( - &mut self, - cfg: &mut Config, - event_loop: &tao::event_loop::EventLoopWindowTarget, - ) { - let webview = build_webview( - cfg, - event_loop, - self.is_ready.clone(), - self.proxy.clone(), - self.eval_sender.clone(), - self.event_tx.clone(), - ); +struct DomHandle { + proxy: EventLoopProxy, +} - self.webviews.insert(webview.window().id(), webview); +impl ArcWake for DomHandle { + fn wake_by_ref(arc_self: &Arc) { + arc_self.proxy.send_event(UserWindowEvent::Poll).unwrap(); } } @@ -159,7 +226,7 @@ fn build_webview( is_ready: Arc, proxy: tao::event_loop::EventLoopProxy, eval_sender: tokio::sync::mpsc::UnboundedSender, - event_tx: UnboundedSender, + event_tx: Rc>>, ) -> wry::webview::WebView { let builder = cfg.window.clone(); let window = builder.build(event_loop).unwrap(); @@ -201,7 +268,8 @@ fn build_webview( eval_sender.send(result).unwrap(); } "user_event" => { - _ = event_tx.unbounded_send(message.params()); + _ = event_tx.borrow_mut().push(message.params()); + let _ = proxy.send_event(UserWindowEvent::Poll); } "initialize" => { is_ready.store(true, std::sync::atomic::Ordering::Relaxed); From bd720e2268952d2a33aeff563d22c3b18c132ec1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 19:53:43 -0500 Subject: [PATCH 16/29] fix: use futuresunordered instead of bespoke waker system --- examples/clock.rs | 41 ++++++++ examples/todomvc.rs | 5 +- packages/core/Cargo.toml | 2 +- packages/core/src/scheduler/mod.rs | 9 +- packages/core/src/scheduler/suspense.rs | 13 ++- packages/core/src/scheduler/task.rs | 50 +++++++-- packages/core/src/scheduler/wait.rs | 134 +++++++++++++----------- packages/core/src/scheduler/waker.rs | 37 ------- packages/core/src/scope_arena.rs | 13 ++- packages/core/src/virtual_dom.rs | 27 ++++- packages/desktop/src/desktop_context.rs | 121 ++++++--------------- packages/desktop/src/lib.rs | 128 +++++++++++++--------- 12 files changed, 308 insertions(+), 272 deletions(-) create mode 100644 examples/clock.rs delete mode 100644 packages/core/src/scheduler/waker.rs diff --git a/examples/clock.rs b/examples/clock.rs new file mode 100644 index 000000000..5df326c1d --- /dev/null +++ b/examples/clock.rs @@ -0,0 +1,41 @@ +//! Example: README.md showcase +//! +//! The example from the README.md. + +use dioxus::prelude::*; +use futures_util::Future; + +#[tokio::main] +async fn main() { + let mut dom = VirtualDom::new(app); + dom.rebuild(); + + loop { + dom.wait_for_work().await; + } + + // dioxus_desktop::launch(app).await; +} + +fn app(cx: Scope) -> Element { + let mut count = use_ref(cx, || 0); + + let mut ct = count.to_owned(); + use_coroutine(cx, |_: UnboundedReceiver<()>| async move { + loop { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + + *ct.write() += 1; + + let current = *ct.read(); + + println!("current: {}", current); + } + }); + + let mut count = count.read(); + + cx.render(rsx! { + div { "High-Five counter: {count}" } + }) +} diff --git a/examples/todomvc.rs b/examples/todomvc.rs index b5277133e..afd1f36b3 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -3,8 +3,9 @@ use dioxus::prelude::*; use dioxus_elements::input_data::keyboard_types::Key; -fn main() { - dioxus_desktop::launch(app); +#[tokio::main] +async fn main() { + dioxus_desktop::launch(app).await; } #[derive(PartialEq, Eq)] diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index bca8bd031..47652e262 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -23,7 +23,7 @@ rustc-hash = "1.1.0" # Used in diffing longest-increasing-subsequence = "0.1.0" -futures-util = { version = "0.3", default-features = false } +futures-util = { version = "0.3", default-features = false, features = ["alloc"]} slab = "0.4" diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index 7aed417b1..3996633a2 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -1,14 +1,13 @@ use crate::ScopeId; +use futures_util::stream::FuturesUnordered; use slab::Slab; mod suspense; mod task; mod wait; -mod waker; pub use suspense::*; pub use task::*; -pub use waker::ArcWake; /// The type of message that can be sent to the scheduler. /// @@ -31,17 +30,17 @@ pub(crate) struct Scheduler { pub sender: futures_channel::mpsc::UnboundedSender, /// Tasks created with cx.spawn - pub tasks: RefCell>>, + pub tasks: RefCell>, /// Async components - pub leaves: RefCell>>, + pub leaves: RefCell>, } impl Scheduler { pub fn new(sender: futures_channel::mpsc::UnboundedSender) -> Rc { Rc::new(Scheduler { sender, - tasks: RefCell::new(Slab::new()), + tasks: RefCell::new(FuturesUnordered::new()), leaves: RefCell::new(Slab::new()), }) } diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index c7cd650bb..8449559c8 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -1,4 +1,6 @@ -use super::{waker::ArcWake, SchedulerMsg}; +use futures_util::task::ArcWake; + +use super::SchedulerMsg; use crate::ElementId; use crate::{innerlude::Mutations, Element, ScopeId}; use std::future::Future; @@ -40,11 +42,16 @@ pub(crate) struct SuspenseLeaf { pub(crate) tx: futures_channel::mpsc::UnboundedSender, pub(crate) notified: Cell, pub(crate) task: *mut dyn Future>, + pub(crate) waker: Arc, } -impl ArcWake for SuspenseLeaf { +pub struct SuspenseHandle { + pub(crate) id: SuspenseId, + pub(crate) tx: futures_channel::mpsc::UnboundedSender, +} + +impl ArcWake for SuspenseHandle { fn wake_by_ref(arc_self: &Arc) { - arc_self.notified.set(true); _ = arc_self .tx .unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id)); diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index 8c1d7ca76..d79b47cb4 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -1,9 +1,13 @@ -use super::{waker::ArcWake, Scheduler, SchedulerMsg}; +use futures_util::task::ArcWake; +use futures_util::FutureExt; + +use super::{Scheduler, SchedulerMsg}; use crate::ScopeId; use std::cell::RefCell; use std::future::Future; use std::pin::Pin; use std::sync::Arc; +use std::task::Waker; /// A task's unique identifier. /// @@ -19,6 +23,17 @@ pub(crate) struct LocalTask { pub(super) task: RefCell + 'static>>>, id: TaskId, tx: futures_channel::mpsc::UnboundedSender, + pub waker: Waker, +} +impl Future for LocalTask { + type Output = (); + + fn poll( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + self.task.borrow_mut().poll_unpin(cx) + } } impl Scheduler { @@ -32,16 +47,23 @@ impl Scheduler { /// Spawning a future onto the root scope will cause it to be dropped when the root component is dropped - which /// will only occur when the VirtuaalDom itself has been dropped. pub fn spawn(&self, scope: ScopeId, task: impl Future + 'static) -> TaskId { - let mut tasks = self.tasks.borrow_mut(); - let entry = tasks.vacant_entry(); - let task_id = TaskId(entry.key()); + // let entry = tasks.vacant_entry(); + let task_id = TaskId(0); - entry.insert(Arc::new(LocalTask { + let task = LocalTask { id: task_id, tx: self.sender.clone(), task: RefCell::new(Box::pin(task)), scope, - })); + waker: futures_util::task::waker(Arc::new(LocalTaskHandle { + id: task_id, + tx: self.sender.clone(), + })), + }; + + self.tasks.borrow_mut().push(task); + + // println!("Spawning task: {:?}", task_id); self.sender .unbounded_send(SchedulerMsg::TaskNotified(task_id)) @@ -54,14 +76,22 @@ impl Scheduler { /// /// This does nto abort the task, so you'll want to wrap it in an aborthandle if that's important to you pub fn remove(&self, id: TaskId) { - self.tasks.borrow_mut().remove(id.0); + // self.tasks.borrow_mut().remove(id.0); } } -impl ArcWake for LocalTask { +pub struct LocalTaskHandle { + id: TaskId, + tx: futures_channel::mpsc::UnboundedSender, +} + +impl ArcWake for LocalTaskHandle { fn wake_by_ref(arc_self: &Arc) { - _ = arc_self + println!("waking task: {:?} {:p}", arc_self.id, arc_self); + + arc_self .tx - .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)); + .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)) + .unwrap(); } } diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 205bd8ea0..deac7fe6f 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -1,3 +1,4 @@ +use futures_util::task::ArcWake; use futures_util::FutureExt; use std::{ rc::Rc, @@ -10,7 +11,7 @@ use crate::{ ScopeId, TaskId, VNode, VirtualDom, }; -use super::{waker::ArcWake, SuspenseId}; +use super::SuspenseId; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -20,22 +21,27 @@ impl VirtualDom { pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) { let mut tasks = self.scheduler.tasks.borrow_mut(); - let task = match tasks.get(id.0) { - Some(task) => task, - None => return, - }; + // let task = match tasks.get(id.0) { + // Some(task) => task, + // None => { + // // The task was removed from the scheduler, so we can just ignore it + // return; + // } + // }; - let waker = task.waker(); - let mut cx = Context::from_waker(&waker); + // let mut cx = Context::from_waker(&task.waker); - // If the task completes... - if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { - // Remove it from the scope so we dont try to double drop it when the scope dropes - self.scopes[task.scope.0].spawned_tasks.remove(&id); + // // If the task completes... + // println!("polling task"); + // if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { + // // Remove it from the scope so we dont try to double drop it when the scope dropes + // self.scopes[task.scope.0].spawned_tasks.remove(&id); - // Remove it from the scheduler - tasks.remove(id.0); - } + // // Remove it from the scheduler + // tasks.remove(id.0); + // } else { + // println!("task not ready yet, but we gave it a handle to the waker"); + // } } pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc { @@ -45,69 +51,69 @@ impl VirtualDom { } pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) { - let leaf = self - .scheduler - .leaves - .borrow_mut() - .get(id.0) - .unwrap() - .clone(); + // let leaf = self + // .scheduler + // .leaves + // .borrow_mut() + // .get(id.0) + // .unwrap() + // .clone(); - let scope_id = leaf.scope_id; + // let scope_id = leaf.scope_id; - // todo: cache the waker - let waker = leaf.waker(); - let mut cx = Context::from_waker(&waker); + // // todo: cache the waker + // let waker = leaf.waker(); + // let mut cx = Context::from_waker(&waker); - // Safety: the future is always pinned to the bump arena - let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; - let as_pinned_mut = &mut pinned; + // // Safety: the future is always pinned to the bump arena + // let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; + // let as_pinned_mut = &mut pinned; - // the component finished rendering and gave us nodes - // we should attach them to that component and then render its children - // continue rendering the tree until we hit yet another suspended component - if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { - let fiber = self.acquire_suspense_boundary(leaf.scope_id); + // // the component finished rendering and gave us nodes + // // we should attach them to that component and then render its children + // // continue rendering the tree until we hit yet another suspended component + // if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { + // let fiber = self.acquire_suspense_boundary(leaf.scope_id); - let scope = &mut self.scopes[scope_id.0]; - let arena = scope.current_frame(); + // let scope = &mut self.scopes[scope_id.0]; + // let arena = scope.current_frame(); - let ret = arena.bump.alloc(match new_nodes { - Some(new) => RenderReturn::Ready(new), - None => RenderReturn::default(), - }); + // let ret = arena.bump.alloc(match new_nodes { + // Some(new) => RenderReturn::Ready(new), + // None => RenderReturn::default(), + // }); - arena.node.set(ret); + // arena.node.set(ret); - fiber.waiting_on.borrow_mut().remove(&id); + // fiber.waiting_on.borrow_mut().remove(&id); - if let RenderReturn::Ready(template) = ret { - let mutations_ref = &mut fiber.mutations.borrow_mut(); - let mutations = &mut **mutations_ref; - let template: &VNode = unsafe { std::mem::transmute(template) }; - let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) }; + // if let RenderReturn::Ready(template) = ret { + // let mutations_ref = &mut fiber.mutations.borrow_mut(); + // let mutations = &mut **mutations_ref; + // let template: &VNode = unsafe { std::mem::transmute(template) }; + // let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) }; - std::mem::swap(&mut self.mutations, mutations); + // std::mem::swap(&mut self.mutations, mutations); - let place_holder_id = scope.placeholder.get().unwrap(); - self.scope_stack.push(scope_id); - let created = self.create(template); - self.scope_stack.pop(); - mutations.push(Mutation::ReplaceWith { - id: place_holder_id, - m: created, - }); + // let place_holder_id = scope.placeholder.get().unwrap(); + // self.scope_stack.push(scope_id); + // let created = self.create(template); + // self.scope_stack.pop(); + // mutations.push(Mutation::ReplaceWith { + // id: place_holder_id, + // m: created, + // }); - for leaf in self.collected_leaves.drain(..) { - fiber.waiting_on.borrow_mut().insert(leaf); - } + // for leaf in self.collected_leaves.drain(..) { + // fiber.waiting_on.borrow_mut().insert(leaf); + // } - std::mem::swap(&mut self.mutations, mutations); + // std::mem::swap(&mut self.mutations, mutations); - if fiber.waiting_on.borrow().is_empty() { - self.finished_fibers.push(fiber.id); - } - } - } + // if fiber.waiting_on.borrow().is_empty() { + // self.finished_fibers.push(fiber.id); + // } + // } + // } } } diff --git a/packages/core/src/scheduler/waker.rs b/packages/core/src/scheduler/waker.rs deleted file mode 100644 index fc75fe8fd..000000000 --- a/packages/core/src/scheduler/waker.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::mem; -use std::sync::Arc; -use std::task::{RawWaker, RawWakerVTable, Waker}; - -pub trait ArcWake: Sized { - /// Create a waker from this self-wakening object - fn waker(self: &Arc) -> Waker { - unsafe fn rc_vtable() -> &'static RawWakerVTable { - &RawWakerVTable::new( - |data| { - let arc = mem::ManuallyDrop::new(Arc::::from_raw(data.cast::())); - let _rc_clone: mem::ManuallyDrop<_> = arc.clone(); - RawWaker::new(data, rc_vtable::()) - }, - |data| Arc::from_raw(data.cast::()).wake(), - |data| { - let arc = mem::ManuallyDrop::new(Arc::::from_raw(data.cast::())); - ArcWake::wake_by_ref(&arc); - }, - |data| drop(Arc::::from_raw(data.cast::())), - ) - } - - unsafe { - Waker::from_raw(RawWaker::new( - Arc::into_raw(self.clone()).cast(), - rc_vtable::(), - )) - } - } - - fn wake_by_ref(arc_self: &Arc); - - fn wake(self: Arc) { - Self::wake_by_ref(&self) - } -} diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index fbe865021..6d574bf7e 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -2,9 +2,8 @@ use crate::{ any_props::AnyProps, bump_frame::BumpFrame, innerlude::DirtyScope, - innerlude::{SuspenseId, SuspenseLeaf}, + innerlude::{SuspenseHandle, SuspenseId, SuspenseLeaf}, nodes::RenderReturn, - scheduler::ArcWake, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; @@ -90,15 +89,19 @@ impl VirtualDom { let entry = leaves.vacant_entry(); let suspense_id = SuspenseId(entry.key()); - let leaf = Arc::new(SuspenseLeaf { + let leaf = SuspenseLeaf { scope_id, task: task.as_mut(), id: suspense_id, tx: self.scheduler.sender.clone(), notified: Default::default(), - }); + waker: Arc::new(SuspenseHandle { + id: suspense_id, + tx: self.scheduler.sender.clone(), + }), + }; - let waker = leaf.waker(); + let waker = futures_util::task::waker(leaf.waker.clone()); let mut cx = Context::from_waker(&waker); // safety: the task is already pinned in the bump arena diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 47cbe7245..2306073cf 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -13,10 +13,21 @@ use crate::{ scopes::{ScopeId, ScopeState}, AttributeValue, Element, Event, Scope, SuspenseContext, }; -use futures_util::{pin_mut, StreamExt}; +use futures_util::{ + pin_mut, + stream::{futures_unordered, FuturesUnordered}, + StreamExt, +}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; +use std::{ + any::Any, + borrow::BorrowMut, + cell::Cell, + collections::{BTreeSet, HashSet}, + future::Future, + rc::Rc, +}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -420,7 +431,7 @@ impl VirtualDom { loop { match some_msg.take() { // If a bunch of messages are ready in a sequence, try to pop them off synchronously - Some(msg) => match msg { + Some(msg) => match dbg!(msg) { SchedulerMsg::Immediate(id) => self.mark_dirty(id), SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), @@ -430,14 +441,20 @@ impl VirtualDom { None => { match self.rx.try_next() { Ok(Some(val)) => some_msg = Some(val), - Ok(None) => return, + Ok(None) => panic!("Scheduler channel closed"), Err(_) => { // If we have any dirty scopes, or finished fiber trees then we should exit if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() { return; } - some_msg = self.rx.next().await + println!("Waiting for next message..."); + let mut tasks = self.scheduler.tasks.borrow_mut(); + + let takss = &mut *tasks; + + use futures_util::StreamExt; + takss.next().await; } } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 929f1ce4a..fdc38db3a 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -13,6 +13,8 @@ use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] use wry::application::platform::ios::WindowExtIOS; use wry::application::window::Fullscreen as WryFullscreen; +use wry::application::window::Window; +use wry::webview::WebView; use UserWindowEvent::*; @@ -40,16 +42,32 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { #[derive(Clone)] pub struct DesktopContext { /// The wry/tao proxy to the current window + pub webview: Rc, + + /// The proxy to the event loop pub proxy: ProxyType, + + /// The receiver for eval results since eval is async pub(super) eval_reciever: Rc>>, } +/// A smart pointer to the current window. +impl std::ops::Deref for DesktopContext { + type Target = Window; + + fn deref(&self) -> &Self::Target { + &self.webview.window() + } +} + impl DesktopContext { pub(crate) fn new( + webview: Rc, proxy: ProxyType, eval_reciever: tokio::sync::mpsc::UnboundedReceiver, ) -> Self { Self { + webview, proxy, eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)), } @@ -66,82 +84,21 @@ impl DesktopContext { pub fn drag(&self) { let _ = self.proxy.send_event(DragWindow); } - - /// set window minimize state - pub fn set_minimized(&self, minimized: bool) { - let _ = self.proxy.send_event(Minimize(minimized)); - } - - /// set window maximize state - pub fn set_maximized(&self, maximized: bool) { - let _ = self.proxy.send_event(Maximize(maximized)); - } - /// toggle window maximize state pub fn toggle_maximized(&self) { let _ = self.proxy.send_event(MaximizeToggle); } - /// set window visible or not - pub fn set_visible(&self, visible: bool) { - let _ = self.proxy.send_event(Visible(visible)); - } - /// close window pub fn close(&self) { let _ = self.proxy.send_event(CloseWindow); } - /// set window to focus - pub fn focus(&self) { - let _ = self.proxy.send_event(FocusWindow); - } - /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { let _ = self.proxy.send_event(Fullscreen(fullscreen)); } - /// set resizable state - pub fn set_resizable(&self, resizable: bool) { - let _ = self.proxy.send_event(Resizable(resizable)); - } - - /// set the window always on top - pub fn set_always_on_top(&self, top: bool) { - let _ = self.proxy.send_event(AlwaysOnTop(top)); - } - - /// set cursor visible or not - pub fn set_cursor_visible(&self, visible: bool) { - let _ = self.proxy.send_event(CursorVisible(visible)); - } - - /// set cursor grab - pub fn set_cursor_grab(&self, grab: bool) { - let _ = self.proxy.send_event(CursorGrab(grab)); - } - - /// set window title - pub fn set_title(&self, title: &str) { - let _ = self.proxy.send_event(SetTitle(String::from(title))); - } - - /// change window to borderless - pub fn set_decorations(&self, decoration: bool) { - let _ = self.proxy.send_event(SetDecorations(decoration)); - } - - /// set window zoom level - pub fn set_zoom_level(&self, scale_factor: f64) { - let _ = self.proxy.send_event(SetZoomLevel(scale_factor)); - } - - /// modifies the inner size of the window - pub fn set_inner_size(&self, logical_size: LogicalSize) { - let _ = self.proxy.send_event(SetInnerSize(logical_size)); - } - /// launch print modal pub fn print(&self) { let _ = self.proxy.send_event(Print); @@ -177,31 +134,20 @@ pub enum UserWindowEvent { Poll, + UserEvent(serde_json::Value), + CloseWindow, DragWindow, - FocusWindow, - Visible(bool), - Minimize(bool), - Maximize(bool), MaximizeToggle, - Resizable(bool), - AlwaysOnTop(bool), + Fullscreen(bool), - CursorVisible(bool), - CursorGrab(bool), - - SetTitle(String), - SetDecorations(bool), - - SetZoomLevel(f64), - SetInnerSize(LogicalSize), - Print, DevTool, Eval(String), + EvalResult(serde_json::Value), #[cfg(target_os = "ios")] PushView(objc_id::ShareId), @@ -236,18 +182,17 @@ impl DesktopController { // 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_some(WryFullscreen::Borderless(Some(handle)))); } } - FocusWindow => window.set_focus(), - Resizable(state) => window.set_resizable(state), - AlwaysOnTop(state) => window.set_always_on_top(state), + + UserEvent(event) => { + // + } Eval(code) => { let script = format!( @@ -261,17 +206,11 @@ impl DesktopController { log::warn!("Eval script error: {e}"); } } - CursorVisible(state) => window.set_cursor_visible(state), - CursorGrab(state) => { - let _ = window.set_cursor_grab(state); + + EvalResult(result) => { + // todo } - SetTitle(content) => window.set_title(&content), - SetDecorations(state) => window.set_decorations(state), - - SetZoomLevel(scale_factor) => webview.zoom(scale_factor), - SetInnerSize(logical_size) => window.set_inner_size(logical_size), - Poll => { // todo } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 14ce5f2f7..b620e7857 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -13,6 +13,7 @@ mod protocol; #[cfg(all(feature = "hot-reload", debug_assertions))] mod hot_reload; +use dioxus_html::{a, HtmlEvent}; use futures_util::task::ArcWake; use serde_json::Value; use std::cell::RefCell; @@ -115,23 +116,18 @@ pub async fn launch_cfg(root: Component, config_builder: Config) { /// } /// ``` pub async fn launch_with_props(root: Component

, props: P, mut cfg: Config) { - let mut event_loop = EventLoop::with_user_event(); + let event_loop = EventLoop::with_user_event(); - let is_ready = Arc::new(AtomicBool::new(false)); - let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel(); - - let mut dom = VirtualDom::new_with_props(root, props).with_root_context(DesktopContext::new( - event_loop.create_proxy(), - eval_reciever, - )); + let mut dom = VirtualDom::new_with_props(root, props); let proxy = event_loop.create_proxy(); + // We want to poll the virtualdom and the event loop at the same time + // So the waker will be connected to both let waker = futures_util::task::waker(Arc::new(DomHandle { proxy: proxy.clone(), })); - let mut events = Rc::new(RefCell::new(vec![])); let mut webviews = HashMap::new(); event_loop.run(move |window_event, event_loop, control_flow| { @@ -139,20 +135,21 @@ pub async fn launch_with_props(root: Component

, props: P, match window_event { Event::NewEvents(StartCause::Init) => { - let window = build_webview( - &mut cfg, - event_loop, - is_ready.clone(), - proxy.clone(), - eval_sender.clone(), - events.clone(), - ); + let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel(); + let window = Rc::new(build_webview(&mut cfg, event_loop, proxy.clone())); + let ctx = DesktopContext::new(window.clone(), proxy.clone(), eval_reciever); + dom.base_scope().provide_context(ctx); + webviews.insert(window.window().id(), window.clone()); - webviews.insert(window.window().id(), window); - - // desktop.start(&mut cfg, event_loop); + proxy.send_event(UserWindowEvent::Poll).unwrap(); } + Event::MainEventsCleared => {} + Event::Resumed => {} + Event::Suspended => {} + Event::LoopDestroyed => {} + Event::RedrawRequested(_id) => {} + Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::Destroyed { .. } => { @@ -165,19 +162,65 @@ pub async fn launch_with_props(root: Component

, props: P, println!("user event: {:?}", user_event); match user_event { + UserWindowEvent::UserEvent(json_value) => { + if let Ok(value) = serde_json::from_value::(json_value) { + let HtmlEvent { + name, + element, + bubbles, + data, + } = value; + + dom.handle_event(&name, data.into_any(), element, bubbles); + + let edits = dom.render_immediate(); + + let serialized = serde_json::to_string(&edits).unwrap(); + + let (_id, view) = webviews.iter_mut().next().unwrap(); + + view.evaluate_script(&format!( + "window.interpreter.handleEdits({})", + serialized + )) + .unwrap(); + } + } + UserWindowEvent::Poll => { let mut cx = std::task::Context::from_waker(&waker); - let render = { - let fut = dom.wait_for_work(); - pin_mut!(fut); - matches!(fut.poll_unpin(&mut cx), std::task::Poll::Ready(_)) - }; + println!("polling.."); + + loop { + { + println!("wait for next work"); + + let fut = dom.wait_for_work(); + pin_mut!(fut); + + match fut.poll_unpin(&mut cx) { + std::task::Poll::Ready(_) => { + println!("work ready"); + } + std::task::Poll::Pending => break, + } + } + + println!("rendering.."); - if render { let edits = dom.render_immediate(); // apply the edits + let serialized = serde_json::to_string(&edits).unwrap(); + + let (_id, view) = webviews.iter_mut().next().unwrap(); + + view.evaluate_script(&format!( + "window.interpreter.handleEdits({})", + serialized + )) + .unwrap(); } } @@ -200,11 +243,7 @@ pub async fn launch_with_props(root: Component

, props: P, } } } - Event::MainEventsCleared => {} - Event::Resumed => {} - Event::Suspended => {} - Event::LoopDestroyed => {} - Event::RedrawRequested(_id) => {} + _ => {} } }) @@ -223,10 +262,7 @@ impl ArcWake for DomHandle { fn build_webview( cfg: &mut Config, event_loop: &tao::event_loop::EventLoopWindowTarget, - is_ready: Arc, proxy: tao::event_loop::EventLoopProxy, - eval_sender: tokio::sync::mpsc::UnboundedSender, - event_tx: Rc>>, ) -> wry::webview::WebView { let builder = cfg.window.clone(); let window = builder.build(event_loop).unwrap(); @@ -264,29 +300,23 @@ fn build_webview( match message.method() { "eval_result" => { - let result = message.params(); - eval_sender.send(result).unwrap(); + let _ = proxy.send_event(UserWindowEvent::EvalResult(message.params())); } "user_event" => { - _ = event_tx.borrow_mut().push(message.params()); - let _ = proxy.send_event(UserWindowEvent::Poll); + let _ = proxy.send_event(UserWindowEvent::UserEvent(message.params())); } "initialize" => { - is_ready.store(true, std::sync::atomic::Ordering::Relaxed); let _ = proxy.send_event(UserWindowEvent::EditsReady); } - "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) { - log::error!("Open Browser error: {:?}", e); - } + "browser_open" => match message.params().as_object() { + Some(temp) if temp.contains_key("href") => { + let open = webbrowser::open(temp["href"].as_str().unwrap()); + if let Err(e) = open { + log::error!("Open Browser error: {:?}", e); } } - } + _ => (), + }, _ => (), } }) From 7bc8150da738176bf910999a62bf5bb49de95628 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 29 Dec 2022 20:39:22 -0500 Subject: [PATCH 17/29] fix desktop to poll without budget --- examples/clock.rs | 12 +---- examples/todomvc.rs | 5 +- packages/core/src/virtual_dom.rs | 34 +++++-------- packages/desktop/src/desktop_context.rs | 5 +- packages/desktop/src/lib.rs | 67 +++++++++++-------------- 5 files changed, 48 insertions(+), 75 deletions(-) diff --git a/examples/clock.rs b/examples/clock.rs index 5df326c1d..c76c49faf 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -5,16 +5,8 @@ use dioxus::prelude::*; use futures_util::Future; -#[tokio::main] -async fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - loop { - dom.wait_for_work().await; - } - - // dioxus_desktop::launch(app).await; +fn main() { + dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { diff --git a/examples/todomvc.rs b/examples/todomvc.rs index afd1f36b3..b5277133e 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -3,9 +3,8 @@ use dioxus::prelude::*; use dioxus_elements::input_data::keyboard_types::Key; -#[tokio::main] -async fn main() { - dioxus_desktop::launch(app).await; +fn main() { + dioxus_desktop::launch(app); } #[derive(PartialEq, Eq)] diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 2306073cf..435ccea3d 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -13,21 +13,10 @@ use crate::{ scopes::{ScopeId, ScopeState}, AttributeValue, Element, Event, Scope, SuspenseContext, }; -use futures_util::{ - pin_mut, - stream::{futures_unordered, FuturesUnordered}, - StreamExt, -}; +use futures_util::{pin_mut, StreamExt}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{ - any::Any, - borrow::BorrowMut, - cell::Cell, - collections::{BTreeSet, HashSet}, - future::Future, - rc::Rc, -}; +use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -431,7 +420,7 @@ impl VirtualDom { loop { match some_msg.take() { // If a bunch of messages are ready in a sequence, try to pop them off synchronously - Some(msg) => match dbg!(msg) { + Some(msg) => match msg { SchedulerMsg::Immediate(id) => self.mark_dirty(id), SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), @@ -441,20 +430,23 @@ impl VirtualDom { None => { match self.rx.try_next() { Ok(Some(val)) => some_msg = Some(val), - Ok(None) => panic!("Scheduler channel closed"), + Ok(None) => return, Err(_) => { // If we have any dirty scopes, or finished fiber trees then we should exit if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() { return; } - println!("Waiting for next message..."); - let mut tasks = self.scheduler.tasks.borrow_mut(); + let res = (&mut *self.scheduler.tasks.borrow_mut()).next().await; - let takss = &mut *tasks; - - use futures_util::StreamExt; - takss.next().await; + if res.is_none() { + // If we have no tasks, then we should wait for a message + if let Some(msg) = self.rx.next().await { + some_msg = Some(msg); + } else { + return; + } + } } } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index fdc38db3a..f6f7df32d 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -82,7 +82,7 @@ impl DesktopContext { /// onmousedown: move |_| { desktop.drag_window(); } /// ``` pub fn drag(&self) { - let _ = self.proxy.send_event(DragWindow); + let _ = self.webview.window().drag_window(); } /// toggle window maximize state pub fn toggle_maximized(&self) { @@ -129,7 +129,6 @@ impl DesktopContext { #[derive(Debug)] pub enum UserWindowEvent { - EditsReady, Initialize, Poll, @@ -174,7 +173,7 @@ impl DesktopController { let window = webview.window(); match user_event { - Initialize | EditsReady => { + EditsReady => { // self.try_load_ready_webviews(); } CloseWindow => *control_flow = ControlFlow::Exit, diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index b620e7857..a7b47109b 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -13,28 +13,17 @@ mod protocol; #[cfg(all(feature = "hot-reload", debug_assertions))] mod hot_reload; -use dioxus_html::{a, HtmlEvent}; -use futures_util::task::ArcWake; -use serde_json::Value; -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; -use std::task::Waker; - +pub use cfg::Config; use desktop_context::UserWindowEvent; pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult}; -use futures_channel::mpsc::UnboundedSender; -use futures_util::future::poll_fn; -use futures_util::{pin_mut, FutureExt}; -pub use wry; -pub use wry::application as tao; - -pub use cfg::Config; -use controller::DesktopController; use dioxus_core::*; +use dioxus_html::HtmlEvent; use events::parse_ipc_message; +use futures_util::task::ArcWake; +use futures_util::{pin_mut, FutureExt}; +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::Arc; pub use tao::dpi::{LogicalSize, PhysicalSize}; pub use tao::window::WindowBuilder; use tao::{ @@ -42,8 +31,9 @@ use tao::{ event_loop::{ControlFlow, EventLoop}, window::Window, }; +pub use wry; +pub use wry::application as tao; use wry::application::event_loop::EventLoopProxy; -use wry::application::platform::run_return::EventLoopExtRunReturn; use wry::webview::WebViewBuilder; /// Launch the WebView and run the event loop. @@ -63,8 +53,8 @@ use wry::webview::WebViewBuilder; /// }) /// } /// ``` -pub async fn launch(root: Component) { - launch_with_props(root, (), Config::default()).await +pub fn launch(root: Component) { + launch_with_props(root, (), Config::default()) } /// Launch the WebView and run the event loop, with configuration. @@ -86,8 +76,8 @@ pub async fn launch(root: Component) { /// }) /// } /// ``` -pub async fn launch_cfg(root: Component, config_builder: Config) { - launch_with_props(root, (), config_builder).await +pub fn launch_cfg(root: Component, config_builder: Config) { + launch_with_props(root, (), config_builder) } /// Launch the WebView and run the event loop, with configuration and root props. @@ -115,21 +105,27 @@ pub async fn launch_cfg(root: Component, config_builder: Config) { /// }) /// } /// ``` -pub async fn launch_with_props(root: Component

, props: P, mut cfg: Config) { +pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { let event_loop = EventLoop::with_user_event(); let mut dom = VirtualDom::new_with_props(root, props); let proxy = event_loop.create_proxy(); + let mut webviews = HashMap::new(); + + // todo: make this configurable + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + // We want to poll the virtualdom and the event loop at the same time // So the waker will be connected to both let waker = futures_util::task::waker(Arc::new(DomHandle { proxy: proxy.clone(), })); - let mut webviews = HashMap::new(); - event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; @@ -150,6 +146,8 @@ pub async fn launch_with_props(root: Component

, props: P, Event::LoopDestroyed => {} Event::RedrawRequested(_id) => {} + Event::NewEvents(cause) => {} + Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::Destroyed { .. } => { @@ -159,8 +157,6 @@ pub async fn launch_with_props(root: Component

, props: P, }, Event::UserEvent(user_event) => { - println!("user event: {:?}", user_event); - match user_event { UserWindowEvent::UserEvent(json_value) => { if let Ok(value) = serde_json::from_value::(json_value) { @@ -190,25 +186,20 @@ pub async fn launch_with_props(root: Component

, props: P, UserWindowEvent::Poll => { let mut cx = std::task::Context::from_waker(&waker); - println!("polling.."); + // using this will reset the budget for the task that we're blocking the main thread with + let _guard = rt.enter(); loop { { - println!("wait for next work"); - let fut = dom.wait_for_work(); pin_mut!(fut); match fut.poll_unpin(&mut cx) { - std::task::Poll::Ready(_) => { - println!("work ready"); - } + std::task::Poll::Ready(_) => {} std::task::Poll::Pending => break, } } - println!("rendering.."); - let edits = dom.render_immediate(); // apply the edits @@ -224,7 +215,7 @@ pub async fn launch_with_props(root: Component

, props: P, } } - UserWindowEvent::EditsReady => { + UserWindowEvent::Initialize => { let edits = dom.rebuild(); let (_id, view) = webviews.iter_mut().next().unwrap(); @@ -306,7 +297,7 @@ fn build_webview( let _ = proxy.send_event(UserWindowEvent::UserEvent(message.params())); } "initialize" => { - let _ = proxy.send_event(UserWindowEvent::EditsReady); + let _ = proxy.send_event(UserWindowEvent::Initialize); } "browser_open" => match message.params().as_object() { Some(temp) if temp.contains_key("href") => { From 633bf1f834bd4afbe7a6dd2e4eb84d40a5432127 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 01:08:25 -0500 Subject: [PATCH 18/29] aggressively clean up desktop with new inline poll --- packages/desktop/Cargo.toml | 1 + packages/desktop/src/controller.rs | 3 - packages/desktop/src/desktop_context.rs | 255 ++++++++--------------- packages/desktop/src/eval.rs | 50 +++++ packages/desktop/src/lib.rs | 265 ++++++------------------ packages/desktop/src/waker.rs | 22 ++ packages/desktop/src/webview.rs | 111 ++++++++++ 7 files changed, 325 insertions(+), 382 deletions(-) create mode 100644 packages/desktop/src/eval.rs create mode 100644 packages/desktop/src/waker.rs create mode 100644 packages/desktop/src/webview.rs diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 4aaae6c64..85dd02bd0 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -35,6 +35,7 @@ dunce = "1.0.2" interprocess = { version = "1.1.1", optional = true} futures-util = "0.3.25" +sledgehammer = "0.2.0" [target.'cfg(target_os = "ios")'.dependencies] objc = "0.2.7" diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 0d198da21..109985ca8 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -29,9 +29,6 @@ pub(super) struct DesktopController { pub(super) event_tx: UnboundedSender, #[cfg(debug_assertions)] pub(super) templates_tx: UnboundedSender>, - - #[cfg(target_os = "ios")] - pub(super) views: Vec<*mut Object>, } impl DesktopController { diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index f6f7df32d..b7a4405fd 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,13 +1,10 @@ +use std::cell::RefCell; use std::rc::Rc; use crate::controller::DesktopController; +use crate::eval::EvalResult; use dioxus_core::ScopeState; -use serde::de::Error; use serde_json::Value; -use std::future::Future; -use std::future::IntoFuture; -use std::pin::Pin; -use wry::application::dpi::LogicalSize; use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] @@ -16,8 +13,6 @@ use wry::application::window::Fullscreen as WryFullscreen; use wry::application::window::Window; use wry::webview::WebView; -use UserWindowEvent::*; - pub type ProxyType = EventLoopProxy; /// Get an imperative handle to the current window @@ -48,7 +43,10 @@ pub struct DesktopContext { pub proxy: ProxyType, /// The receiver for eval results since eval is async - pub(super) eval_reciever: Rc>>, + pub(super) eval_reciever: Rc>>, + + #[cfg(target_os = "ios")] + pub(crate) views: Rc>>>, } /// A smart pointer to the current window. @@ -56,7 +54,7 @@ impl std::ops::Deref for DesktopContext { type Target = Window; fn deref(&self) -> &Self::Target { - &self.webview.window() + self.webview.window() } } @@ -69,7 +67,7 @@ impl DesktopContext { Self { webview, proxy, - eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)), + eval_reciever: Rc::new(RefCell::new(eval_reciever)), } } @@ -82,48 +80,108 @@ impl DesktopContext { /// onmousedown: move |_| { desktop.drag_window(); } /// ``` pub fn drag(&self) { - let _ = self.webview.window().drag_window(); + let window = self.webview.window(); + + // if the drag_window has any errors, we don't do anything + window.fullscreen().is_none().then(|| window.drag_window()); } - /// toggle window maximize state + + /// Toggle whether the window is maximized or not pub fn toggle_maximized(&self) { - let _ = self.proxy.send_event(MaximizeToggle); + let window = self.webview.window(); + + window.set_maximized(!window.is_maximized()) } /// close window pub fn close(&self) { - let _ = self.proxy.send_event(CloseWindow); + let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); } /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { - let _ = self.proxy.send_event(Fullscreen(fullscreen)); + if let Some(handle) = self.webview.window().current_monitor() { + self.webview + .window() + .set_fullscreen(fullscreen.then_some(WryFullscreen::Borderless(Some(handle)))); + } } /// launch print modal pub fn print(&self) { - let _ = self.proxy.send_event(Print); + if let Err(e) = self.webview.print() { + log::warn!("Open print modal failed: {e}"); + } } /// opens DevTool window pub fn devtool(&self) { - let _ = self.proxy.send_event(DevTool); + #[cfg(debug_assertions)] + self.webview.open_devtools(); + + #[cfg(not(debug_assertions))] + log::warn!("Devtools are disabled in release builds"); } - /// run (evaluate) a script in the WebView context - pub fn eval(&self, script: impl std::string::ToString) { - let _ = self.proxy.send_event(Eval(script.to_string())); + /// Evaluate a javascript expression + pub fn eval(&self, code: &str) -> EvalResult { + // Embed the return of the eval in a function so we can send it back to the main thread + let script = format!( + r#" + window.ipc.postMessage( + JSON.stringify({{ + "method":"eval_result", + "params": ( + function(){{ + {} + }} + )() + }}) + ); + "#, + code + ); + + if let Err(e) = self.webview.evaluate_script(&script) { + // send an error to the eval receiver + log::warn!("Eval script error: {e}"); + } + + EvalResult { + reciever: self.eval_reciever.clone(), + } } - /// Push view + /// Push an objc view to the window #[cfg(target_os = "ios")] pub fn push_view(&self, view: objc_id::ShareId) { - let _ = self.proxy.send_event(PushView(view)); + unsafe { + use objc::runtime::Object; + use objc::*; + assert!(is_main_thread()); + let ui_view = window.ui_view() as *mut Object; + let ui_view_frame: *mut Object = msg_send![ui_view, frame]; + let _: () = msg_send![view, setFrame: ui_view_frame]; + let _: () = msg_send![view, setAutoresizingMask: 31]; + + let ui_view_controller = window.ui_view_controller() as *mut Object; + let _: () = msg_send![ui_view_controller, setView: view]; + self.views.borrow_mut().push(ui_view); + } } - /// Push view + /// Pop an objc view from the window #[cfg(target_os = "ios")] pub fn pop_view(&self) { - let _ = self.proxy.send_event(PopView); + unsafe { + use objc::runtime::Object; + use objc::*; + assert!(is_main_thread()); + if let Some(view) = self.views.borrow_mut().pop() { + let ui_view_controller = window.ui_view_controller() as *mut Object; + let _: () = msg_send![ui_view_controller, setView: view]; + } + } } } @@ -136,157 +194,8 @@ pub enum UserWindowEvent { UserEvent(serde_json::Value), CloseWindow, - DragWindow, - MaximizeToggle, - - Fullscreen(bool), - - Print, - DevTool, - - Eval(String), EvalResult(serde_json::Value), - - #[cfg(target_os = "ios")] - PushView(objc_id::ShareId), - #[cfg(target_os = "ios")] - PopView, -} - -impl DesktopController { - pub(super) fn handle_event( - &mut self, - user_event: UserWindowEvent, - control_flow: &mut ControlFlow, - ) { - // currently dioxus-desktop supports a single window only, - // so we can grab the only webview from the map; - // on wayland it is possible that a user event is emitted - // before the webview is initialized. ignore the event. - let webview = if let Some(webview) = self.webviews.values().next() { - webview - } else { - return; - }; - - let window = webview.window(); - - match user_event { - EditsReady => { - // self.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()); - } - - MaximizeToggle => window.set_maximized(!window.is_maximized()), - Fullscreen(state) => { - if let Some(handle) = window.current_monitor() { - window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle)))); - } - } - - UserEvent(event) => { - // - } - - Eval(code) => { - let script = format!( - r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{ - {} - }})()}}));"#, - code - ); - if let Err(e) = webview.evaluate_script(&script) { - // we can't panic this error. - log::warn!("Eval script error: {e}"); - } - } - - EvalResult(result) => { - // todo - } - - Poll => { - // todo - } - - Print => { - if let Err(e) = webview.print() { - // we can't panic this error. - log::warn!("Open print modal failed: {e}"); - } - } - DevTool => { - #[cfg(debug_assertions)] - webview.open_devtools(); - #[cfg(not(debug_assertions))] - log::warn!("Devtools are disabled in release builds"); - } - - #[cfg(target_os = "ios")] - PushView(view) => unsafe { - use objc::runtime::Object; - use objc::*; - assert!(is_main_thread()); - let ui_view = window.ui_view() as *mut Object; - let ui_view_frame: *mut Object = msg_send![ui_view, frame]; - let _: () = msg_send![view, setFrame: ui_view_frame]; - let _: () = msg_send![view, setAutoresizingMask: 31]; - - let ui_view_controller = window.ui_view_controller() as *mut Object; - let _: () = msg_send![ui_view_controller, setView: view]; - self.views.push(ui_view); - }, - - #[cfg(target_os = "ios")] - PopView => unsafe { - use objc::runtime::Object; - use objc::*; - assert!(is_main_thread()); - if let Some(view) = self.views.pop() { - let ui_view_controller = window.ui_view_controller() as *mut Object; - let _: () = msg_send![ui_view_controller, setView: view]; - } - }, - } - } -} - -/// Get a closure that executes any JavaScript in the WebView context. -pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult { - let desktop = use_window(cx).clone(); - cx.use_hook(|| { - move |script| { - desktop.eval(script); - let recv = desktop.eval_reciever.clone(); - EvalResult { reciever: recv } - } - }) -} - -/// A future that resolves to the result of a JavaScript evaluation. -pub struct EvalResult { - reciever: Rc>>, -} - -impl IntoFuture for EvalResult { - type Output = Result; - - type IntoFuture = Pin>>>; - - fn into_future(self) -> Self::IntoFuture { - Box::pin(async move { - let mut reciever = self.reciever.lock().await; - match reciever.recv().await { - Some(result) => Ok(result), - None => Err(serde_json::Error::custom("No result returned")), - } - }) as Pin>>> - } } #[cfg(target_os = "ios")] diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs new file mode 100644 index 000000000..358825fa3 --- /dev/null +++ b/packages/desktop/src/eval.rs @@ -0,0 +1,50 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use crate::controller::DesktopController; +use crate::use_window; +use dioxus_core::ScopeState; +use serde::de::Error; +use serde_json::Value; +use std::future::Future; +use std::future::IntoFuture; +use std::pin::Pin; +use wry::application::dpi::LogicalSize; +use wry::application::event_loop::ControlFlow; +use wry::application::event_loop::EventLoopProxy; +#[cfg(target_os = "ios")] +use wry::application::platform::ios::WindowExtIOS; +use wry::application::window::Fullscreen as WryFullscreen; +use wry::application::window::Window; +use wry::webview::WebView; + +/// A future that resolves to the result of a JavaScript evaluation. +pub struct EvalResult { + pub(crate) reciever: Rc>>, +} + +impl IntoFuture for EvalResult { + type Output = Result; + + type IntoFuture = Pin>>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + let mut reciever = self.reciever.borrow_mut(); + match reciever.recv().await { + Some(result) => Ok(result), + None => Err(serde_json::Error::custom("No result returned")), + } + }) as Pin>>> + } +} + +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_eval(cx: &ScopeState) -> &Rc EvalResult> { + let desktop = use_window(cx); + &*cx.use_hook(|| { + let desktop = desktop.clone(); + + Rc::new(move |script: String| desktop.eval(&script)) as Rc EvalResult> + }) +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index a7b47109b..7dda7e679 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -13,28 +13,27 @@ mod protocol; #[cfg(all(feature = "hot-reload", debug_assertions))] mod hot_reload; +mod eval; +mod waker; +mod webview; + pub use cfg::Config; use desktop_context::UserWindowEvent; -pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult}; +pub use desktop_context::{use_window, DesktopContext}; use dioxus_core::*; use dioxus_html::HtmlEvent; -use events::parse_ipc_message; -use futures_util::task::ArcWake; use futures_util::{pin_mut, FutureExt}; use std::collections::HashMap; use std::rc::Rc; -use std::sync::Arc; +use std::task::Waker; pub use tao::dpi::{LogicalSize, PhysicalSize}; pub use tao::window::WindowBuilder; use tao::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, - window::Window, }; pub use wry; pub use wry::application as tao; -use wry::application::event_loop::EventLoopProxy; -use wry::webview::WebViewBuilder; /// Launch the WebView and run the event loop. /// @@ -82,9 +81,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// Launch the WebView and run the event loop, with configuration and root props. /// -/// THIS WILL BLOCK THE CURRENT THREAD -/// -/// This function will start a multithreaded Tokio runtime as well the WebView event loop. +/// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread. /// /// You can configure the WebView window with a configuration closure /// @@ -106,25 +103,27 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// } /// ``` pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { - let event_loop = EventLoop::with_user_event(); - let mut dom = VirtualDom::new_with_props(root, props); + let event_loop = EventLoop::with_user_event(); + let proxy = event_loop.create_proxy(); - let mut webviews = HashMap::new(); - - // todo: make this configurable + // We start the tokio runtime *on this thread* + // Any future we poll later will use this runtime to spawn tasks and for IO let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap(); - // We want to poll the virtualdom and the event loop at the same time - // So the waker will be connected to both - let waker = futures_util::task::waker(Arc::new(DomHandle { - proxy: proxy.clone(), - })); + // We enter the runtime but we poll futures manually, circumventing the per-task runtime budget + let _guard = rt.enter(); + + // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both + let waker = waker::tao_waker(&proxy); + + // We only have one webview right now, but we'll have more later + let mut webviews = HashMap::new(); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; @@ -132,11 +131,10 @@ pub fn launch_with_props(root: Component

, props: P, mut cf match window_event { Event::NewEvents(StartCause::Init) => { let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel(); - let window = Rc::new(build_webview(&mut cfg, event_loop, proxy.clone())); + let window = Rc::new(webview::build(&mut cfg, event_loop, proxy.clone())); let ctx = DesktopContext::new(window.clone(), proxy.clone(), eval_reciever); dom.base_scope().provide_context(ctx); webviews.insert(window.window().id(), window.clone()); - proxy.send_event(UserWindowEvent::Poll).unwrap(); } @@ -156,200 +154,55 @@ pub fn launch_with_props(root: Component

, props: P, mut cf _ => {} }, - Event::UserEvent(user_event) => { - match user_event { - UserWindowEvent::UserEvent(json_value) => { - if let Ok(value) = serde_json::from_value::(json_value) { - let HtmlEvent { - name, - element, - bubbles, - data, - } = value; - - dom.handle_event(&name, data.into_any(), element, bubbles); - - let edits = dom.render_immediate(); - - let serialized = serde_json::to_string(&edits).unwrap(); - - let (_id, view) = webviews.iter_mut().next().unwrap(); - - view.evaluate_script(&format!( - "window.interpreter.handleEdits({})", - serialized - )) - .unwrap(); - } - } - - UserWindowEvent::Poll => { - let mut cx = std::task::Context::from_waker(&waker); - - // using this will reset the budget for the task that we're blocking the main thread with - let _guard = rt.enter(); - - loop { - { - let fut = dom.wait_for_work(); - pin_mut!(fut); - - match fut.poll_unpin(&mut cx) { - std::task::Poll::Ready(_) => {} - std::task::Poll::Pending => break, - } - } - - let edits = dom.render_immediate(); - - // apply the edits - let serialized = serde_json::to_string(&edits).unwrap(); - - let (_id, view) = webviews.iter_mut().next().unwrap(); - - view.evaluate_script(&format!( - "window.interpreter.handleEdits({})", - serialized - )) - .unwrap(); - } - } - - UserWindowEvent::Initialize => { - let edits = dom.rebuild(); - - let (_id, view) = webviews.iter_mut().next().unwrap(); - - let serialized = serde_json::to_string(&edits).unwrap(); - - view.evaluate_script(&format!( - "window.interpreter.handleEdits({})", - serialized - )) - .unwrap(); - } - - other => { - // desktop.handle_event(user_event, control_flow); - } - } + Event::UserEvent(UserWindowEvent::Initialize) => { + send_edits(dom.rebuild(), &mut webviews); } - _ => {} + Event::UserEvent(UserWindowEvent::CloseWindow) => *control_flow = ControlFlow::Exit, + + Event::UserEvent(UserWindowEvent::EvalResult(_)) => todo!(), + + Event::UserEvent(UserWindowEvent::UserEvent(json_value)) => { + let evt = match serde_json::from_value::(json_value) { + Ok(value) => value, + Err(_) => return, + }; + + dom.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles); + + send_edits(dom.render_immediate(), &mut webviews); + } + + Event::UserEvent(UserWindowEvent::Poll) => { + poll_vdom(&waker, &mut dom, &mut webviews); + } + + _ => todo!(), } }) } -struct DomHandle { - proxy: EventLoopProxy, -} +type Webviews = HashMap>; -impl ArcWake for DomHandle { - fn wake_by_ref(arc_self: &Arc) { - arc_self.proxy.send_event(UserWindowEvent::Poll).unwrap(); - } -} +fn poll_vdom(waker: &Waker, dom: &mut VirtualDom, webviews: &mut Webviews) { + let mut cx = std::task::Context::from_waker(waker); + loop { + { + let fut = dom.wait_for_work(); + pin_mut!(fut); -fn build_webview( - cfg: &mut Config, - event_loop: &tao::event_loop::EventLoopWindowTarget, - proxy: tao::event_loop::EventLoopProxy, -) -> wry::webview::WebView { - let builder = cfg.window.clone(); - let window = builder.build(event_loop).unwrap(); - let file_handler = cfg.file_drop_handler.take(); - let custom_head = cfg.custom_head.clone(); - let resource_dir = cfg.resource_dir.clone(); - let index_file = cfg.custom_index.clone(); - let root_name = cfg.root_name.clone(); - - // We assume that if the icon is None in cfg, then the user just didnt set it - if cfg.window.window.window_icon.is_none() { - window.set_window_icon(Some( - tao::window::Icon::from_rgba( - include_bytes!("./assets/default_icon.bin").to_vec(), - 460, - 460, - ) - .expect("image parse failed"), - )); - } - - let mut webview = WebViewBuilder::new(window) - .unwrap() - .with_transparent(cfg.window.window.transparent) - .with_url("dioxus://index.html/") - .unwrap() - .with_ipc_handler(move |_window: &Window, payload: String| { - let message = match parse_ipc_message(&payload) { - Some(message) => message, - None => { - log::error!("Failed to parse IPC message: {}", payload); - return; - } - }; - - match message.method() { - "eval_result" => { - let _ = proxy.send_event(UserWindowEvent::EvalResult(message.params())); - } - "user_event" => { - let _ = proxy.send_event(UserWindowEvent::UserEvent(message.params())); - } - "initialize" => { - let _ = proxy.send_event(UserWindowEvent::Initialize); - } - "browser_open" => match message.params().as_object() { - Some(temp) if temp.contains_key("href") => { - let open = webbrowser::open(temp["href"].as_str().unwrap()); - if let Err(e) = open { - log::error!("Open Browser error: {:?}", e); - } - } - _ => (), - }, - _ => (), + match fut.poll_unpin(&mut cx) { + std::task::Poll::Ready(_) => {} + std::task::Poll::Pending => break, } - }) - .with_custom_protocol(String::from("dioxus"), move |r| { - protocol::desktop_handler( - r, - resource_dir.clone(), - custom_head.clone(), - index_file.clone(), - &root_name, - ) - }) - .with_file_drop_handler(move |window, evet| { - file_handler - .as_ref() - .map(|handler| handler(window, evet)) - .unwrap_or_default() - }); + } - for (name, handler) in cfg.protocols.drain(..) { - webview = webview.with_custom_protocol(name, handler) + send_edits(dom.render_immediate(), webviews); } +} - if cfg.disable_context_menu { - // in release mode, we don't want to show the dev tool or reload menus - webview = webview.with_initialization_script( - r#" - if (document.addEventListener) { - document.addEventListener('contextmenu', function(e) { - e.preventDefault(); - }, false); - } else { - document.attachEvent('oncontextmenu', function() { - window.event.returnValue = false; - }); - } - "#, - ) - } else { - // in debug, we are okay with the reload menu showing and dev tool - webview = webview.with_devtools(true); - } - - webview.build().unwrap() +fn send_edits(edits: Mutations, webviews: &mut Webviews) { + let serialized = serde_json::to_string(&edits).unwrap(); + let (_id, view) = webviews.iter_mut().next().unwrap(); + _ = view.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized)); } diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs new file mode 100644 index 000000000..a08c93bd3 --- /dev/null +++ b/packages/desktop/src/waker.rs @@ -0,0 +1,22 @@ +use futures_util::task::ArcWake; +use std::sync::Arc; +use wry::application::event_loop::EventLoopProxy; + +use crate::desktop_context::UserWindowEvent; + +/// Create a waker that will send a poll event to the event loop. +/// +/// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView. +/// +/// All other IO lives in the Tokio runtime, +pub fn tao_waker(proxy: &EventLoopProxy) -> std::task::Waker { + struct DomHandle(EventLoopProxy); + + impl ArcWake for DomHandle { + fn wake_by_ref(arc_self: &Arc) { + arc_self.0.send_event(UserWindowEvent::Poll).unwrap(); + } + } + + futures_util::task::waker(Arc::new(DomHandle(proxy.clone()))) +} diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs new file mode 100644 index 000000000..5cb80e41b --- /dev/null +++ b/packages/desktop/src/webview.rs @@ -0,0 +1,111 @@ +use crate::events::parse_ipc_message; +use crate::protocol; +use crate::{desktop_context::UserWindowEvent, Config}; +pub use wry; +pub use wry::application as tao; +use wry::application::window::Window; +use wry::webview::WebViewBuilder; + +pub fn build( + cfg: &mut Config, + event_loop: &tao::event_loop::EventLoopWindowTarget, + proxy: tao::event_loop::EventLoopProxy, +) -> wry::webview::WebView { + let builder = cfg.window.clone(); + let window = builder.build(event_loop).unwrap(); + let file_handler = cfg.file_drop_handler.take(); + let custom_head = cfg.custom_head.clone(); + let resource_dir = cfg.resource_dir.clone(); + let index_file = cfg.custom_index.clone(); + let root_name = cfg.root_name.clone(); + + // We assume that if the icon is None in cfg, then the user just didnt set it + if cfg.window.window.window_icon.is_none() { + window.set_window_icon(Some( + tao::window::Icon::from_rgba( + include_bytes!("./assets/default_icon.bin").to_vec(), + 460, + 460, + ) + .expect("image parse failed"), + )); + } + + let mut webview = WebViewBuilder::new(window) + .unwrap() + .with_transparent(cfg.window.window.transparent) + .with_url("dioxus://index.html/") + .unwrap() + .with_ipc_handler(move |_window: &Window, payload: String| { + let message = match parse_ipc_message(&payload) { + Some(message) => message, + None => { + log::error!("Failed to parse IPC message: {}", payload); + return; + } + }; + + match message.method() { + "eval_result" => { + let _ = proxy.send_event(UserWindowEvent::EvalResult(message.params())); + } + "user_event" => { + let _ = proxy.send_event(UserWindowEvent::UserEvent(message.params())); + } + "initialize" => { + let _ = proxy.send_event(UserWindowEvent::Initialize); + } + "browser_open" => match message.params().as_object() { + Some(temp) if temp.contains_key("href") => { + let open = webbrowser::open(temp["href"].as_str().unwrap()); + if let Err(e) = open { + log::error!("Open Browser error: {:?}", e); + } + } + _ => (), + }, + _ => (), + } + }) + .with_custom_protocol(String::from("dioxus"), move |r| { + protocol::desktop_handler( + r, + resource_dir.clone(), + custom_head.clone(), + index_file.clone(), + &root_name, + ) + }) + .with_file_drop_handler(move |window, evet| { + file_handler + .as_ref() + .map(|handler| handler(window, evet)) + .unwrap_or_default() + }); + + for (name, handler) in cfg.protocols.drain(..) { + webview = webview.with_custom_protocol(name, handler) + } + + if cfg.disable_context_menu { + // in release mode, we don't want to show the dev tool or reload menus + webview = webview.with_initialization_script( + r#" + if (document.addEventListener) { + document.addEventListener('contextmenu', function(e) { + e.preventDefault(); + }, false); + } else { + document.attachEvent('oncontextmenu', function() { + window.event.returnValue = false; + }); + } + "#, + ) + } else { + // in debug, we are okay with the reload menu showing and dev tool + webview = webview.with_devtools(true); + } + + webview.build().unwrap() +} From 531f7c6d3fb483767efde7008988739036c113a8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 01:52:54 -0500 Subject: [PATCH 19/29] chore: clean up msg in desktop even more --- packages/desktop/src/controller.rs | 151 ------------------------ packages/desktop/src/desktop_context.rs | 30 +++-- packages/desktop/src/eval.rs | 10 -- packages/desktop/src/events.rs | 4 +- packages/desktop/src/lib.rs | 102 ++++++++++------ packages/desktop/src/waker.rs | 10 +- packages/desktop/src/webview.rs | 44 ++----- 7 files changed, 104 insertions(+), 247 deletions(-) delete mode 100644 packages/desktop/src/controller.rs diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs deleted file mode 100644 index 109985ca8..000000000 --- a/packages/desktop/src/controller.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::desktop_context::{DesktopContext, UserWindowEvent}; -use dioxus_core::*; -use dioxus_html::HtmlEvent; -use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use futures_util::StreamExt; -#[cfg(target_os = "ios")] -use objc::runtime::Object; -use serde_json::Value; -use std::{ - collections::HashMap, - sync::Arc, - sync::{atomic::AtomicBool, Mutex}, - time::Duration, -}; -use wry::{ - self, - application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId}, - webview::WebView, -}; - -pub(super) struct DesktopController { - pub(super) webviews: HashMap, - pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, - - pub(super) quit_app_on_close: bool, - pub(super) is_ready: Arc, - pub(super) proxy: EventLoopProxy, - - pub(super) event_tx: UnboundedSender, - #[cfg(debug_assertions)] - pub(super) templates_tx: UnboundedSender>, -} - -impl DesktopController { - 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 new_queue = Vec::new(); - - // { - // let mut queue = self.pending_edits.lock().unwrap(); - // std::mem::swap(&mut new_queue, &mut *queue); - // } - - // let (_id, view) = self.webviews.iter_mut().next().unwrap(); - - // for edit in new_queue.drain(..) { - // view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) - // .unwrap(); - // } - // } - // } -} - -// // Launch the virtualdom on its own thread managed by tokio -// // returns the desktop state -// pub(super) fn new_on_tokio( -// root: Component

, -// props: P, -// proxy: EventLoopProxy, -// ) -> Self { -// let edit_queue = Arc::new(Mutex::new(Vec::new())); -// let (event_tx, mut event_rx) = unbounded(); -// let (templates_tx, mut templates_rx) = unbounded(); -// let proxy2 = proxy.clone(); - -// let pending_edits = edit_queue.clone(); -// let desktop_context_proxy = proxy.clone(); -// let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::(); - -// std::thread::spawn(move || { -// // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads -// // I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it - -// let runtime = tokio::runtime::Builder::new_multi_thread() -// .enable_all() -// .build() -// .unwrap(); - -// runtime.block_on(async move { -// let mut dom = VirtualDom::new_with_props(root, props) -// .with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever)); -// { -// let edits = dom.rebuild(); -// let mut queue = edit_queue.lock().unwrap(); -// queue.push(serde_json::to_string(&edits).unwrap()); -// proxy.send_event(UserWindowEvent::EditsReady).unwrap(); -// } - -// loop { -// tokio::select! { -// template = { -// #[allow(unused)] -// fn maybe_future<'a>(templates_rx: &'a mut UnboundedReceiver>) -> impl Future> + 'a { -// #[cfg(debug_assertions)] -// return templates_rx.select_next_some(); -// #[cfg(not(debug_assertions))] -// return std::future::pending(); -// } -// maybe_future(&mut templates_rx) -// } => { -// dom.replace_template(template); -// } -// _ = dom.wait_for_work() => {} -// Some(json_value) = event_rx.next() => { -// if let Ok(value) = serde_json::from_value::(json_value) { -// let HtmlEvent { -// name, -// element, -// bubbles, -// data -// } = value; -// dom.handle_event(&name, data.into_any(), element, bubbles); -// } -// } -// } - -// let muts = dom -// .render_with_deadline(tokio::time::sleep(Duration::from_millis(16))) -// .await; - -// edit_queue -// .lock() -// .unwrap() -// .push(serde_json::to_string(&muts).unwrap()); -// let _ = proxy.send_event(UserWindowEvent::EditsReady); -// } -// }) -// }); - -// Self { -// pending_edits, -// eval_sender, -// webviews: HashMap::new(), -// is_ready: Arc::new(AtomicBool::new(false)), -// quit_app_on_close: true, -// proxy: proxy2, -// event_tx, -// #[cfg(debug_assertions)] -// templates_tx, -// #[cfg(target_os = "ios")] -// views: vec![], -// } -// } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index b7a4405fd..75a458cb7 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,11 +1,10 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::controller::DesktopController; use crate::eval::EvalResult; +use crate::events::IpcMessage; use dioxus_core::ScopeState; use serde_json::Value; -use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] use wry::application::platform::ios::WindowExtIOS; @@ -42,11 +41,14 @@ pub struct DesktopContext { /// The proxy to the event loop pub proxy: ProxyType, + /// The receiver for eval results since eval is async + pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, + /// The receiver for eval results since eval is async pub(super) eval_reciever: Rc>>, #[cfg(target_os = "ios")] - pub(crate) views: Rc>>>, + pub(crate) views: Rc>>, } /// A smart pointer to the current window. @@ -59,15 +61,17 @@ impl std::ops::Deref for DesktopContext { } impl DesktopContext { - pub(crate) fn new( - webview: Rc, - proxy: ProxyType, - eval_reciever: tokio::sync::mpsc::UnboundedReceiver, - ) -> Self { + pub(crate) fn new(webview: Rc, proxy: ProxyType) -> Self { + let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel(); + Self { webview, proxy, eval_reciever: Rc::new(RefCell::new(eval_reciever)), + eval_sender, + + #[cfg(target_os = "ios")] + views: Default::default(), } } @@ -155,6 +159,8 @@ impl DesktopContext { /// Push an objc view to the window #[cfg(target_os = "ios")] pub fn push_view(&self, view: objc_id::ShareId) { + let window = self.webview.window(); + unsafe { use objc::runtime::Object; use objc::*; @@ -173,6 +179,8 @@ impl DesktopContext { /// Pop an objc view from the window #[cfg(target_os = "ios")] pub fn pop_view(&self) { + let window = self.webview.window(); + unsafe { use objc::runtime::Object; use objc::*; @@ -187,15 +195,11 @@ impl DesktopContext { #[derive(Debug)] pub enum UserWindowEvent { - Initialize, - Poll, - UserEvent(serde_json::Value), + Ipc(IpcMessage), CloseWindow, - - EvalResult(serde_json::Value), } #[cfg(target_os = "ios")] diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index 358825fa3..80baba2d4 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -1,22 +1,12 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::controller::DesktopController; use crate::use_window; use dioxus_core::ScopeState; use serde::de::Error; -use serde_json::Value; use std::future::Future; use std::future::IntoFuture; use std::pin::Pin; -use wry::application::dpi::LogicalSize; -use wry::application::event_loop::ControlFlow; -use wry::application::event_loop::EventLoopProxy; -#[cfg(target_os = "ios")] -use wry::application::platform::ios::WindowExtIOS; -use wry::application::window::Fullscreen as WryFullscreen; -use wry::application::window::Window; -use wry::webview::WebView; /// A future that resolves to the result of a JavaScript evaluation. pub struct EvalResult { diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index f9b3fe5cb..ae4d77c68 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -2,8 +2,8 @@ use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize)] -pub(crate) struct IpcMessage { +#[derive(Deserialize, Serialize, Debug)] +pub struct IpcMessage { method: String, params: serde_json::Value, } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 7dda7e679..9d10f36f5 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -4,24 +4,23 @@ #![deny(missing_docs)] mod cfg; -mod controller; mod desktop_context; mod escape; +mod eval; mod events; mod protocol; +mod waker; +mod webview; #[cfg(all(feature = "hot-reload", debug_assertions))] mod hot_reload; -mod eval; -mod waker; -mod webview; - pub use cfg::Config; use desktop_context::UserWindowEvent; pub use desktop_context::{use_window, DesktopContext}; use dioxus_core::*; use dioxus_html::HtmlEvent; +pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; use std::collections::HashMap; use std::rc::Rc; @@ -89,7 +88,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// use dioxus::prelude::*; /// /// fn main() { -/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, |c| c); +/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); /// } /// /// struct AppProps { @@ -102,7 +101,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// }) /// } /// ``` -pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { +pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { let mut dom = VirtualDom::new_with_props(root, props); let event_loop = EventLoop::with_user_event(); @@ -123,47 +122,46 @@ pub fn launch_with_props(root: Component

, props: P, mut cf let waker = waker::tao_waker(&proxy); // We only have one webview right now, but we'll have more later + // Store them in a hashmap so we can remove them when they're closed let mut webviews = HashMap::new(); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; match window_event { - Event::NewEvents(StartCause::Init) => { - let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel(); - let window = Rc::new(webview::build(&mut cfg, event_loop, proxy.clone())); - let ctx = DesktopContext::new(window.clone(), proxy.clone(), eval_reciever); - dom.base_scope().provide_context(ctx); - webviews.insert(window.window().id(), window.clone()); - proxy.send_event(UserWindowEvent::Poll).unwrap(); - } + Event::UserEvent(UserWindowEvent::CloseWindow) => *control_flow = ControlFlow::Exit, - Event::MainEventsCleared => {} - Event::Resumed => {} - Event::Suspended => {} - Event::LoopDestroyed => {} - Event::RedrawRequested(_id) => {} - - Event::NewEvents(cause) => {} - - Event::WindowEvent { event, .. } => match event { + Event::WindowEvent { + event, window_id, .. + } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::Destroyed { .. } => { - // desktop.close_window(window_id, control_flow); + webviews.remove(&window_id); + + if webviews.is_empty() { + *control_flow = ControlFlow::Exit; + } } _ => {} }, - Event::UserEvent(UserWindowEvent::Initialize) => { - send_edits(dom.rebuild(), &mut webviews); + Event::NewEvents(StartCause::Init) => { + let window = webview::build(&mut cfg, event_loop, proxy.clone()); + + dom.base_scope() + .provide_context(DesktopContext::new(window.clone(), proxy.clone())); + + webviews.insert(window.window().id(), window); + + _ = proxy.send_event(UserWindowEvent::Poll); } - Event::UserEvent(UserWindowEvent::CloseWindow) => *control_flow = ControlFlow::Exit, + Event::UserEvent(UserWindowEvent::Poll) => { + poll_vdom(&waker, &mut dom, &mut webviews); + } - Event::UserEvent(UserWindowEvent::EvalResult(_)) => todo!(), - - Event::UserEvent(UserWindowEvent::UserEvent(json_value)) => { - let evt = match serde_json::from_value::(json_value) { + Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "user_event" => { + let evt = match serde_json::from_value::(msg.params()) { Ok(value) => value, Err(_) => return, }; @@ -173,19 +171,49 @@ pub fn launch_with_props(root: Component

, props: P, mut cf send_edits(dom.render_immediate(), &mut webviews); } - Event::UserEvent(UserWindowEvent::Poll) => { - poll_vdom(&waker, &mut dom, &mut webviews); + Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "initialize" => { + send_edits(dom.rebuild(), &mut webviews); } - _ => todo!(), + // When the webview chirps back with the result of the eval, we send it to the active receiver + // + // This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once, + // you might the wrong result. This should be fixed + Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "eval_result" => { + dom.base_scope() + .consume_context::() + .unwrap() + .eval_sender + .send(msg.params()) + .unwrap(); + } + + Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "browser_open" => { + if let Some(temp) = msg.params().as_object() { + if temp.contains_key("href") { + let open = webbrowser::open(temp["href"].as_str().unwrap()); + if let Err(e) = open { + log::error!("Open Browser error: {:?}", e); + } + } + } + } + + _ => {} } }) } type Webviews = HashMap>; +/// Poll the virtualdom until it's pending +/// +/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again +/// +/// All IO is done on the tokio runtime we started earlier fn poll_vdom(waker: &Waker, dom: &mut VirtualDom, webviews: &mut Webviews) { let mut cx = std::task::Context::from_waker(waker); + loop { { let fut = dom.wait_for_work(); @@ -201,8 +229,12 @@ fn poll_vdom(waker: &Waker, dom: &mut VirtualDom, webviews: &mut Webviews) { } } +/// Send a list of mutations to the webview fn send_edits(edits: Mutations, webviews: &mut Webviews) { let serialized = serde_json::to_string(&edits).unwrap(); + let (_id, view) = webviews.iter_mut().next().unwrap(); + + // todo: use SSE and binary data to send the edits with lower overhead _ = view.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized)); } diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index a08c93bd3..a2586ffed 100644 --- a/packages/desktop/src/waker.rs +++ b/packages/desktop/src/waker.rs @@ -1,9 +1,8 @@ +use crate::desktop_context::UserWindowEvent; use futures_util::task::ArcWake; use std::sync::Arc; use wry::application::event_loop::EventLoopProxy; -use crate::desktop_context::UserWindowEvent; - /// Create a waker that will send a poll event to the event loop. /// /// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView. @@ -12,9 +11,14 @@ use crate::desktop_context::UserWindowEvent; pub fn tao_waker(proxy: &EventLoopProxy) -> std::task::Waker { struct DomHandle(EventLoopProxy); + // this should be implemented by most platforms, but ios is missing this until + // https://github.com/tauri-apps/wry/issues/830 is resolved + unsafe impl Send for DomHandle {} + unsafe impl Sync for DomHandle {} + impl ArcWake for DomHandle { fn wake_by_ref(arc_self: &Arc) { - arc_self.0.send_event(UserWindowEvent::Poll).unwrap(); + _ = arc_self.0.send_event(UserWindowEvent::Poll); } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 5cb80e41b..6c286c35b 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,16 +1,19 @@ +use std::rc::Rc; + use crate::events::parse_ipc_message; use crate::protocol; use crate::{desktop_context::UserWindowEvent, Config}; +use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use wry; pub use wry::application as tao; use wry::application::window::Window; -use wry::webview::WebViewBuilder; +use wry::webview::{WebView, WebViewBuilder}; pub fn build( cfg: &mut Config, - event_loop: &tao::event_loop::EventLoopWindowTarget, - proxy: tao::event_loop::EventLoopProxy, -) -> wry::webview::WebView { + event_loop: &EventLoopWindowTarget, + proxy: EventLoopProxy, +) -> Rc { let builder = cfg.window.clone(); let window = builder.build(event_loop).unwrap(); let file_handler = cfg.file_drop_handler.take(); @@ -37,34 +40,9 @@ pub fn build( .with_url("dioxus://index.html/") .unwrap() .with_ipc_handler(move |_window: &Window, payload: String| { - let message = match parse_ipc_message(&payload) { - Some(message) => message, - None => { - log::error!("Failed to parse IPC message: {}", payload); - return; - } - }; - - match message.method() { - "eval_result" => { - let _ = proxy.send_event(UserWindowEvent::EvalResult(message.params())); - } - "user_event" => { - let _ = proxy.send_event(UserWindowEvent::UserEvent(message.params())); - } - "initialize" => { - let _ = proxy.send_event(UserWindowEvent::Initialize); - } - "browser_open" => match message.params().as_object() { - Some(temp) if temp.contains_key("href") => { - let open = webbrowser::open(temp["href"].as_str().unwrap()); - if let Err(e) = open { - log::error!("Open Browser error: {:?}", e); - } - } - _ => (), - }, - _ => (), + // defer the event to the main thread + if let Some(message) = parse_ipc_message(&payload) { + _ = proxy.send_event(UserWindowEvent::Ipc(message)); } }) .with_custom_protocol(String::from("dioxus"), move |r| { @@ -107,5 +85,5 @@ pub fn build( webview = webview.with_devtools(true); } - webview.build().unwrap() + Rc::new(webview.build().unwrap()) } From d160a5c8ff81cc1678309518e80585ab75be236b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 01:54:12 -0500 Subject: [PATCH 20/29] chore: remove unnecessary parse method --- packages/desktop/src/events.rs | 10 ---------- packages/desktop/src/webview.rs | 3 +-- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index ae4d77c68..50d3fbf3d 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -17,13 +17,3 @@ impl IpcMessage { self.params } } - -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); - None - } - } -} diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 6c286c35b..4a270bfab 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,6 +1,5 @@ use std::rc::Rc; -use crate::events::parse_ipc_message; use crate::protocol; use crate::{desktop_context::UserWindowEvent, Config}; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; @@ -41,7 +40,7 @@ pub fn build( .unwrap() .with_ipc_handler(move |_window: &Window, payload: String| { // defer the event to the main thread - if let Some(message) = parse_ipc_message(&payload) { + if let Ok(message) = serde_json::from_str(&payload) { _ = proxy.send_event(UserWindowEvent::Ipc(message)); } }) From 6dfe84834d2c42e3b3220df08a1e26875540c504 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 02:06:33 -0500 Subject: [PATCH 21/29] fix eval example --- examples/eval.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/examples/eval.rs b/examples/eval.rs index 22e036b9c..3a16b774e 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -1,25 +1,17 @@ use dioxus::prelude::*; -use dioxus_desktop::EvalResult; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - let script = use_state(cx, String::new); let eval = dioxus_desktop::use_eval(cx); - let future: &UseRef> = use_ref(cx, || None); - if future.read().is_some() { - let future_clone = future.clone(); - cx.spawn(async move { - if let Some(fut) = future_clone.with_mut(|o| o.take()) { - println!("{:?}", fut.await) - } - }); - } + let script = use_state(cx, String::new); + let output = use_state(cx, String::new); cx.render(rsx! { div { + p { "Output: {output}" } input { placeholder: "Enter an expression", value: "{script}", @@ -27,8 +19,12 @@ fn app(cx: Scope) -> Element { } button { onclick: move |_| { - let fut = eval(script); - future.set(Some(fut)); + to_owned![script, eval, output]; + cx.spawn(async move { + if let Ok(res) = eval(script.to_string()).await { + output.set(res.to_string()); + } + }); }, "Execute" } From 7f418bbfeefe81717a2d51e3be6fcc7ef76eb62f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 02:09:02 -0500 Subject: [PATCH 22/29] fix: window zoom --- examples/readme.rs | 5 ++--- examples/window_zoom.rs | 4 ++-- packages/desktop/src/desktop_context.rs | 5 +++++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/readme.rs b/examples/readme.rs index 7e574904f..f4668488b 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -4,9 +4,8 @@ use dioxus::prelude::*; -#[tokio::main] -async fn main() { - dioxus_desktop::launch(app).await; +fn main() { + dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { diff --git a/examples/window_zoom.rs b/examples/window_zoom.rs index b7c190896..98786c75b 100644 --- a/examples/window_zoom.rs +++ b/examples/window_zoom.rs @@ -7,8 +7,8 @@ fn main() { fn app(cx: Scope) -> Element { let window = use_window(cx); - let level = use_state(cx, || 1.0); + cx.render(rsx! { input { r#type: "number", @@ -16,7 +16,7 @@ fn app(cx: Scope) -> Element { oninput: |e| { let num = e.value.parse::().unwrap_or(1.0); level.set(num); - window.set_zoom_level(num); + window.webview.zoom(num); } } }) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 75a458cb7..09bed730e 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -118,6 +118,11 @@ impl DesktopContext { } } + /// Set the zoom level of the webview + pub fn set_zoom_level(&self, level: f64) { + self.webview.zoom(level); + } + /// opens DevTool window pub fn devtool(&self) { #[cfg(debug_assertions)] From a13b69788bececfeea16d876fc1080e7799d4931 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 02:11:43 -0500 Subject: [PATCH 23/29] chore: rename zoom --- examples/window_zoom.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/window_zoom.rs b/examples/window_zoom.rs index 98786c75b..facff0b40 100644 --- a/examples/window_zoom.rs +++ b/examples/window_zoom.rs @@ -14,9 +14,9 @@ fn app(cx: Scope) -> Element { r#type: "number", value: "{level}", oninput: |e| { - let num = e.value.parse::().unwrap_or(1.0); - level.set(num); - window.webview.zoom(num); + let new_zoom = e.value.parse::().unwrap_or(1.0); + level.set(new_zoom); + window.webview.zoom(new_zoom); } } }) From 972084ee3233505ecf3c9ab06b461311c079b158 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 02:32:53 -0500 Subject: [PATCH 24/29] chore: clean up zoom example --- examples/window_zoom.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/window_zoom.rs b/examples/window_zoom.rs index facff0b40..e45d71be2 100644 --- a/examples/window_zoom.rs +++ b/examples/window_zoom.rs @@ -14,9 +14,10 @@ fn app(cx: Scope) -> Element { r#type: "number", value: "{level}", oninput: |e| { - let new_zoom = e.value.parse::().unwrap_or(1.0); - level.set(new_zoom); - window.webview.zoom(new_zoom); + if let Ok(new_zoom) = e.value.parse::() { + level.set(new_zoom); + window.webview.zoom(new_zoom); + } } } }) From daa5449b6bb5b87381627ec8d89b6f7e0ffed9bb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 02:39:47 -0500 Subject: [PATCH 25/29] chore: use existing scheduler --- packages/core/src/scheduler/mod.rs | 7 +- packages/core/src/scheduler/suspense.rs | 5 +- packages/core/src/scheduler/task.rs | 27 ++--- packages/core/src/scheduler/wait.rs | 129 +++++++++++------------- packages/core/src/scope_arena.rs | 9 +- packages/core/src/virtual_dom.rs | 20 ++-- 6 files changed, 85 insertions(+), 112 deletions(-) diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index 3996633a2..8fb476fbe 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -1,5 +1,4 @@ use crate::ScopeId; -use futures_util::stream::FuturesUnordered; use slab::Slab; mod suspense; @@ -24,13 +23,13 @@ pub(crate) enum SchedulerMsg { SuspenseNotified(SuspenseId), } -use std::{cell::RefCell, rc::Rc, sync::Arc}; +use std::{cell::RefCell, rc::Rc}; pub(crate) struct Scheduler { pub sender: futures_channel::mpsc::UnboundedSender, /// Tasks created with cx.spawn - pub tasks: RefCell>, + pub tasks: RefCell>, /// Async components pub leaves: RefCell>, @@ -40,7 +39,7 @@ impl Scheduler { pub fn new(sender: futures_channel::mpsc::UnboundedSender) -> Rc { Rc::new(Scheduler { sender, - tasks: RefCell::new(FuturesUnordered::new()), + tasks: RefCell::new(Slab::new()), leaves: RefCell::new(Slab::new()), }) } diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index 8449559c8..39e404198 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -5,6 +5,7 @@ use crate::ElementId; use crate::{innerlude::Mutations, Element, ScopeId}; use std::future::Future; use std::sync::Arc; +use std::task::Waker; use std::{ cell::{Cell, RefCell}, collections::HashSet, @@ -37,12 +38,10 @@ impl SuspenseContext { } pub(crate) struct SuspenseLeaf { - pub(crate) id: SuspenseId, pub(crate) scope_id: ScopeId, - pub(crate) tx: futures_channel::mpsc::UnboundedSender, pub(crate) notified: Cell, pub(crate) task: *mut dyn Future>, - pub(crate) waker: Arc, + pub(crate) waker: Waker, } pub struct SuspenseHandle { diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index d79b47cb4..a905cf993 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -1,5 +1,4 @@ use futures_util::task::ArcWake; -use futures_util::FutureExt; use super::{Scheduler, SchedulerMsg}; use crate::ScopeId; @@ -21,20 +20,8 @@ pub struct TaskId(pub usize); pub(crate) struct LocalTask { pub scope: ScopeId, pub(super) task: RefCell + 'static>>>, - id: TaskId, - tx: futures_channel::mpsc::UnboundedSender, pub waker: Waker, } -impl Future for LocalTask { - type Output = (); - - fn poll( - self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - self.task.borrow_mut().poll_unpin(cx) - } -} impl Scheduler { /// Start a new future on the same thread as the rest of the VirtualDom. @@ -47,12 +34,12 @@ impl Scheduler { /// Spawning a future onto the root scope will cause it to be dropped when the root component is dropped - which /// will only occur when the VirtuaalDom itself has been dropped. pub fn spawn(&self, scope: ScopeId, task: impl Future + 'static) -> TaskId { - // let entry = tasks.vacant_entry(); - let task_id = TaskId(0); + let mut tasks = self.tasks.borrow_mut(); + + let entry = tasks.vacant_entry(); + let task_id = TaskId(entry.key()); let task = LocalTask { - id: task_id, - tx: self.sender.clone(), task: RefCell::new(Box::pin(task)), scope, waker: futures_util::task::waker(Arc::new(LocalTaskHandle { @@ -61,9 +48,7 @@ impl Scheduler { })), }; - self.tasks.borrow_mut().push(task); - - // println!("Spawning task: {:?}", task_id); + entry.insert(task); self.sender .unbounded_send(SchedulerMsg::TaskNotified(task_id)) @@ -76,7 +61,7 @@ impl Scheduler { /// /// This does nto abort the task, so you'll want to wrap it in an aborthandle if that's important to you pub fn remove(&self, id: TaskId) { - // self.tasks.borrow_mut().remove(id.0); + self.tasks.borrow_mut().remove(id.0); } } diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index deac7fe6f..9268cfeb0 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -1,4 +1,3 @@ -use futures_util::task::ArcWake; use futures_util::FutureExt; use std::{ rc::Rc, @@ -21,27 +20,22 @@ impl VirtualDom { pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) { let mut tasks = self.scheduler.tasks.borrow_mut(); - // let task = match tasks.get(id.0) { - // Some(task) => task, - // None => { - // // The task was removed from the scheduler, so we can just ignore it - // return; - // } - // }; + let task = match tasks.get(id.0) { + Some(task) => task, + // The task was removed from the scheduler, so we can just ignore it + None => return, + }; - // let mut cx = Context::from_waker(&task.waker); + let mut cx = Context::from_waker(&task.waker); - // // If the task completes... - // println!("polling task"); - // if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { - // // Remove it from the scope so we dont try to double drop it when the scope dropes - // self.scopes[task.scope.0].spawned_tasks.remove(&id); + // If the task completes... + if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { + // Remove it from the scope so we dont try to double drop it when the scope dropes + self.scopes[task.scope.0].spawned_tasks.remove(&id); - // // Remove it from the scheduler - // tasks.remove(id.0); - // } else { - // println!("task not ready yet, but we gave it a handle to the waker"); - // } + // Remove it from the scheduler + tasks.remove(id.0); + } } pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc { @@ -51,69 +45,66 @@ impl VirtualDom { } pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) { - // let leaf = self - // .scheduler - // .leaves - // .borrow_mut() - // .get(id.0) - // .unwrap() - // .clone(); + let leaves = self.scheduler.leaves.borrow_mut(); + let leaf = leaves.get(id.0).unwrap(); - // let scope_id = leaf.scope_id; + let scope_id = leaf.scope_id; - // // todo: cache the waker - // let waker = leaf.waker(); - // let mut cx = Context::from_waker(&waker); + // todo: cache the waker + let mut cx = Context::from_waker(&leaf.waker); - // // Safety: the future is always pinned to the bump arena - // let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; - // let as_pinned_mut = &mut pinned; + // Safety: the future is always pinned to the bump arena + let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; + let as_pinned_mut = &mut pinned; - // // the component finished rendering and gave us nodes - // // we should attach them to that component and then render its children - // // continue rendering the tree until we hit yet another suspended component - // if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { - // let fiber = self.acquire_suspense_boundary(leaf.scope_id); + // the component finished rendering and gave us nodes + // we should attach them to that component and then render its children + // continue rendering the tree until we hit yet another suspended component + if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { + let fiber = self.acquire_suspense_boundary(leaf.scope_id); - // let scope = &mut self.scopes[scope_id.0]; - // let arena = scope.current_frame(); + let scope = &mut self.scopes[scope_id.0]; + let arena = scope.current_frame(); - // let ret = arena.bump.alloc(match new_nodes { - // Some(new) => RenderReturn::Ready(new), - // None => RenderReturn::default(), - // }); + let ret = arena.bump.alloc(match new_nodes { + Some(new) => RenderReturn::Ready(new), + None => RenderReturn::default(), + }); - // arena.node.set(ret); + arena.node.set(ret); - // fiber.waiting_on.borrow_mut().remove(&id); + fiber.waiting_on.borrow_mut().remove(&id); - // if let RenderReturn::Ready(template) = ret { - // let mutations_ref = &mut fiber.mutations.borrow_mut(); - // let mutations = &mut **mutations_ref; - // let template: &VNode = unsafe { std::mem::transmute(template) }; - // let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) }; + if let RenderReturn::Ready(template) = ret { + let mutations_ref = &mut fiber.mutations.borrow_mut(); + let mutations = &mut **mutations_ref; + let template: &VNode = unsafe { std::mem::transmute(template) }; + let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) }; - // std::mem::swap(&mut self.mutations, mutations); + std::mem::swap(&mut self.mutations, mutations); - // let place_holder_id = scope.placeholder.get().unwrap(); - // self.scope_stack.push(scope_id); - // let created = self.create(template); - // self.scope_stack.pop(); - // mutations.push(Mutation::ReplaceWith { - // id: place_holder_id, - // m: created, - // }); + let place_holder_id = scope.placeholder.get().unwrap(); + self.scope_stack.push(scope_id); - // for leaf in self.collected_leaves.drain(..) { - // fiber.waiting_on.borrow_mut().insert(leaf); - // } + drop(leaves); - // std::mem::swap(&mut self.mutations, mutations); + let created = self.create(template); + self.scope_stack.pop(); + mutations.push(Mutation::ReplaceWith { + id: place_holder_id, + m: created, + }); - // if fiber.waiting_on.borrow().is_empty() { - // self.finished_fibers.push(fiber.id); - // } - // } - // } + for leaf in self.collected_leaves.drain(..) { + fiber.waiting_on.borrow_mut().insert(leaf); + } + + std::mem::swap(&mut self.mutations, mutations); + + if fiber.waiting_on.borrow().is_empty() { + self.finished_fibers.push(fiber.id); + } + } + } } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 6d574bf7e..8d3de2b45 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -92,17 +92,14 @@ impl VirtualDom { let leaf = SuspenseLeaf { scope_id, task: task.as_mut(), - id: suspense_id, - tx: self.scheduler.sender.clone(), notified: Default::default(), - waker: Arc::new(SuspenseHandle { + waker: futures_util::task::waker(Arc::new(SuspenseHandle { id: suspense_id, tx: self.scheduler.sender.clone(), - }), + })), }; - let waker = futures_util::task::waker(leaf.waker.clone()); - let mut cx = Context::from_waker(&waker); + let mut cx = Context::from_waker(&leaf.waker); // safety: the task is already pinned in the bump arena let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) }; diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 435ccea3d..4450a5170 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -437,16 +437,18 @@ impl VirtualDom { return; } - let res = (&mut *self.scheduler.tasks.borrow_mut()).next().await; + some_msg = self.rx.next().await - if res.is_none() { - // If we have no tasks, then we should wait for a message - if let Some(msg) = self.rx.next().await { - some_msg = Some(msg); - } else { - return; - } - } + // let res = (&mut *self.scheduler.tasks.borrow_mut()).next().await; + + // if res.is_none() { + // // If we have no tasks, then we should wait for a message + // if let Some(msg) = self.rx.next().await { + // some_msg = Some(msg); + // } else { + // return; + // } + // } } } } From ba20bc08b866c4681b1710ac406c9f6bff84cd16 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 02:41:09 -0500 Subject: [PATCH 26/29] chore: clean up clock --- examples/clock.rs | 7 +++---- packages/core/src/scheduler/task.rs | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/clock.rs b/examples/clock.rs index c76c49faf..a159ef73a 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -3,16 +3,15 @@ //! The example from the README.md. use dioxus::prelude::*; -use futures_util::Future; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - let mut count = use_ref(cx, || 0); + let count = use_ref(cx, || 0); - let mut ct = count.to_owned(); + let ct = count.to_owned(); use_coroutine(cx, |_: UnboundedReceiver<()>| async move { loop { tokio::time::sleep(std::time::Duration::from_millis(10)).await; @@ -25,7 +24,7 @@ fn app(cx: Scope) -> Element { } }); - let mut count = count.read(); + let count = count.read(); cx.render(rsx! { div { "High-Five counter: {count}" } diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index a905cf993..39c770a06 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -72,8 +72,6 @@ pub struct LocalTaskHandle { impl ArcWake for LocalTaskHandle { fn wake_by_ref(arc_self: &Arc) { - println!("waking task: {:?} {:p}", arc_self.id, arc_self); - arc_self .tx .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)) From 9db69cc61331b4b71b3a719ff2d343fab58042b3 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 02:43:00 -0500 Subject: [PATCH 27/29] chore: remove comments --- packages/core/src/virtual_dom.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 4450a5170..47cbe7245 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -438,17 +438,6 @@ impl VirtualDom { } some_msg = self.rx.next().await - - // let res = (&mut *self.scheduler.tasks.borrow_mut()).next().await; - - // if res.is_none() { - // // If we have no tasks, then we should wait for a message - // if let Some(msg) = self.rx.next().await { - // some_msg = Some(msg); - // } else { - // return; - // } - // } } } } From 1518f07d7e2de0feece7f566dcad4e31f6270b70 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 02:44:37 -0500 Subject: [PATCH 28/29] chore: remove sledgehammer --- packages/desktop/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 85dd02bd0..8e8cc1a90 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -33,9 +33,8 @@ webbrowser = "0.8.0" infer = "0.11.0" dunce = "1.0.2" -interprocess = { version = "1.1.1", optional = true} +interprocess = { version = "1.1.1", optional = true } futures-util = "0.3.25" -sledgehammer = "0.2.0" [target.'cfg(target_os = "ios")'.dependencies] objc = "0.2.7" From 819bd3fc24219352c93af7eead40bfee734d7c3d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 02:56:10 -0500 Subject: [PATCH 29/29] chore: use broadcast instead of receiver --- packages/desktop/src/desktop_context.rs | 16 +++------------- packages/desktop/src/eval.rs | 15 ++++++++++----- packages/desktop/src/lib.rs | 2 +- packages/html/src/events/drag.rs | 2 +- packages/html/src/events/mouse.rs | 2 +- 5 files changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 09bed730e..cb5a62635 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::rc::Rc; use crate::eval::EvalResult; @@ -42,10 +41,7 @@ pub struct DesktopContext { pub proxy: ProxyType, /// The receiver for eval results since eval is async - pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, - - /// The receiver for eval results since eval is async - pub(super) eval_reciever: Rc>>, + pub(super) eval: tokio::sync::broadcast::Sender, #[cfg(target_os = "ios")] pub(crate) views: Rc>>, @@ -62,14 +58,10 @@ impl std::ops::Deref for DesktopContext { impl DesktopContext { pub(crate) fn new(webview: Rc, proxy: ProxyType) -> Self { - let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel(); - Self { webview, proxy, - eval_reciever: Rc::new(RefCell::new(eval_reciever)), - eval_sender, - + eval: tokio::sync::broadcast::channel(8).0, #[cfg(target_os = "ios")] views: Default::default(), } @@ -156,9 +148,7 @@ impl DesktopContext { log::warn!("Eval script error: {e}"); } - EvalResult { - reciever: self.eval_reciever.clone(), - } + EvalResult::new(self.eval.clone()) } /// Push an objc view to the window diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index 80baba2d4..7a92bedf5 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::rc::Rc; use crate::use_window; @@ -10,7 +9,13 @@ use std::pin::Pin; /// A future that resolves to the result of a JavaScript evaluation. pub struct EvalResult { - pub(crate) reciever: Rc>>, + pub(crate) broadcast: tokio::sync::broadcast::Sender, +} + +impl EvalResult { + pub(crate) fn new(sender: tokio::sync::broadcast::Sender) -> Self { + Self { broadcast: sender } + } } impl IntoFuture for EvalResult { @@ -20,10 +25,10 @@ impl IntoFuture for EvalResult { fn into_future(self) -> Self::IntoFuture { Box::pin(async move { - let mut reciever = self.reciever.borrow_mut(); + let mut reciever = self.broadcast.subscribe(); match reciever.recv().await { - Some(result) => Ok(result), - None => Err(serde_json::Error::custom("No result returned")), + Ok(result) => Ok(result), + Err(_) => Err(serde_json::Error::custom("No result returned")), } }) as Pin>>> } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 9d10f36f5..e3002f539 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -183,7 +183,7 @@ pub fn launch_with_props(root: Component

, props: P, mut cfg: Conf dom.base_scope() .consume_context::() .unwrap() - .eval_sender + .eval .send(msg.params()) .unwrap(); } diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index f8ee6ff92..fa3219631 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -9,7 +9,7 @@ pub type DragEvent = Event; /// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an /// application-specific way. #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct DragData { /// Inherit mouse data pub mouse: MouseData, diff --git a/packages/html/src/events/mouse.rs b/packages/html/src/events/mouse.rs index f8446b7d1..7899393ff 100644 --- a/packages/html/src/events/mouse.rs +++ b/packages/html/src/events/mouse.rs @@ -10,7 +10,7 @@ pub type MouseEvent = Event; /// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Default, PartialEq)] +#[derive(Clone, Default, PartialEq, Eq)] /// Data associated with a mouse event /// /// Do not use the deprecated fields; they may change or become private in the future.