From ada246c12e336a4700611d4d9cab318074842dea Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 27 Apr 2023 10:21:05 -0500 Subject: [PATCH 001/126] add file property to drag data --- examples/file_upload.rs | 27 +++++- packages/desktop/src/cfg.rs | 2 +- packages/desktop/src/webview.rs | 10 +-- packages/html/Cargo.toml | 1 + packages/html/src/events/drag.rs | 34 +++++++- packages/html/src/events/form.rs | 2 +- packages/html/src/web_sys_bind/events.rs | 1 + packages/interpreter/src/interpreter.js | 105 ++++++++++++----------- packages/liveview/src/lib.rs | 14 +-- packages/web/Cargo.toml | 3 +- packages/web/src/dom.rs | 11 ++- 11 files changed, 133 insertions(+), 77 deletions(-) diff --git a/examples/file_upload.rs b/examples/file_upload.rs index b23648b50..a1d2ff88c 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -2,10 +2,10 @@ use dioxus::prelude::*; fn main() { - dioxus_desktop::launch(App); + dioxus_desktop::launch(app); } -fn App(cx: Scope) -> Element { +fn app(cx: Scope) -> Element { let files_uploaded: &UseRef> = use_ref(cx, Vec::new); cx.render(rsx! { @@ -27,6 +27,29 @@ fn App(cx: Scope) -> Element { } }, } + div { + width: "100px", + height: "100px", + border: "1px solid black", + prevent_default: "ondrop dragover dragenter", + ondrop: move |evt| { + to_owned![files_uploaded]; + async move { + if let Some(file_engine) = &evt.files { + let files = file_engine.files(); + for file_name in &files { + if let Some(file) = file_engine.read_file_to_string(file_name).await{ + files_uploaded.write().push(file); + } + } + } + } + }, + ondragover: move |event: DragEvent| { + event.stop_propagation(); + }, + "Drop files here" + } ul { for file in files_uploaded.read().iter() { diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index d7f1b5dfa..2d8578644 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -87,7 +87,7 @@ impl Config { self } - /// Set a file drop handler + /// Set a file drop handler. If this is enabled, html drag events will be disabled. pub fn with_file_drop_handler( mut self, handler: impl Fn(&Window, FileDropEvent) -> bool + 'static, diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 5d3acf44f..7eb6a4490 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -49,14 +49,12 @@ pub fn build( .with_custom_protocol(String::from("dioxus"), move |r| { protocol::desktop_handler(r, 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() - }) .with_web_context(&mut web_context); + if let Some(handler) = file_handler { + webview = webview.with_file_drop_handler(handler) + } + #[cfg(windows)] { // Windows has a platform specific settings to disable the browser shortcut keys diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 91917ee4b..95700f153 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -41,6 +41,7 @@ features = [ "FocusEvent", "CompositionEvent", "ClipboardEvent", + "DragEvent" ] [dev-dependencies] diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index fa3219631..323bcc87a 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -1,7 +1,9 @@ -use dioxus_core::Event; - +use crate::FileEngine; use crate::MouseData; +use dioxus_core::Event; +use std::fmt::Debug; + pub type DragEvent = Event; /// The DragEvent interface is a DOM event that represents a drag and drop interaction. The user initiates a drag by @@ -9,12 +11,38 @@ 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, Eq)] +#[derive(Clone)] pub struct DragData { /// Inherit mouse data pub mouse: MouseData, + + #[cfg_attr( + feature = "serialize", + serde( + default, + skip_serializing, + deserialize_with = "crate::events::deserialize_file_engine" + ) + )] + pub files: Option>, } +impl Debug for DragData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DragData") + .field("mouse", &self.mouse) + .finish() + } +} + +impl PartialEq for DragData { + fn eq(&self, other: &Self) -> bool { + self.mouse == other.mouse + } +} + +impl Eq for DragData {} + impl_event! { DragData; diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 113c765c5..4fa09d2a6 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -48,7 +48,7 @@ impl FileEngine for SerializedFileEngine { } #[cfg(feature = "serialize")] -fn deserialize_file_engine<'de, D>( +pub(crate) fn deserialize_file_engine<'de, D>( deserializer: D, ) -> Result>, D::Error> where diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 196099444..0204bdd3d 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -123,6 +123,7 @@ impl From<&MouseEvent> for DragData { fn from(value: &MouseEvent) -> Self { Self { mouse: MouseData::from(value), + files: None, } } } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 9b93ec9e3..e35105044 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -351,7 +351,11 @@ class Interpreter { let handler = (event) => { let target = event.target; if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); + const realId = find_real_id(target); + if (realId === null) { + return; + } + let shouldPreventDefault = target.getAttribute( `dioxus-prevent-default` ); @@ -379,22 +383,6 @@ class Interpreter { 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(); @@ -404,41 +392,42 @@ class Interpreter { 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; + serialize_event(event).then((contents) => { + 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; } - } else { - // @ts-ignore - contents.values[name] = - element.value ?? element.textContent; } } } - } - if (realId === null) { - return; - } - window.ipc.postMessage( - serializeIpcMessage("user_event", { - name: edit.name, - element: parseInt(realId), - data: contents, - bubbles, - }) - ); + window.ipc.postMessage( + serializeIpcMessage("user_event", { + name: edit.name, + element: parseInt(realId), + data: contents, + bubbles, + }) + ); + }); } }; this.NewEventListener(edit.name, edit.id, bubbles, handler); @@ -575,7 +564,7 @@ function get_mouse_data(event) { }; } -function serialize_event(event) { +async function serialize_event(event) { switch (event.type) { case "copy": case "cut": @@ -661,7 +650,11 @@ function serialize_event(event) { case "dragover": case "dragstart": case "drop": { - return { mouse: get_mouse_data(event) }; + let files = null; + if (event.dataTransfer && event.dataTransfer.files) { + files = await serializeFileList(event.dataTransfer.files); + } + return { mouse: get_mouse_data(event), files }; } case "click": case "contextmenu": @@ -816,6 +809,20 @@ function serialize_event(event) { } } } +async function serializeFileList(fileList) { + const file_contents = {}; + + for (let i = 0; i < fileList.length; i++) { + const file = fileList[i]; + + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + return { + files: file_contents, + }; +} function serializeIpcMessage(method, params = {}) { return JSON.stringify({ method, params }); } diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index bea21a71b..b35d36ab2 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -45,19 +45,7 @@ static INTERPRETER_JS: Lazy = Lazy::new(|| { const type = target.getAttribute("type"); if (type === "file") { async function read_files() { - const files = target.files; - const file_contents = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - let file_engine = { - files: file_contents, - }; + const file_engine=await serializeFileList( target.files); contents.files = file_engine; if (realId === null) { diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index fcbb996bb..7730bacc8 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -79,7 +79,8 @@ features = [ "console", "FileList", "File", - "FileReader" + "FileReader", + "DataTransfer" ] [features] diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index e1e3b6365..b66918c38 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -244,8 +244,17 @@ pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) - } "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" => { + let mut files = None; + if let Some(event) = event.dyn_ref::() { + if let Some(data) = event.data_transfer() { + if let Some(file_list) = data.files() { + files = WebFileEngine::new(file_list) + .map(|f| Arc::new(f) as Arc); + } + } + } let mouse = MouseData::from(event); - Rc::new(DragData { mouse }) + Rc::new(DragData { mouse, files }) } "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" From 26f6561de56636b186f815b65c884d0a4c409cb4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 11 Jul 2023 17:55:55 -0500 Subject: [PATCH 002/126] fix html cargo.toml --- packages/html/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 23787286c..0ef32fb15 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -39,7 +39,7 @@ features = [ "FocusEvent", "CompositionEvent", "ClipboardEvent", - "DragEvent" + "DragEvent", "Element", "DomRect", "ScrollIntoViewOptions", From 8d38b6c11b0c2d328607a5a50753784b609318b8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 11 Jul 2023 18:03:13 -0500 Subject: [PATCH 003/126] fix interpreter js --- packages/interpreter/src/interpreter.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 03b1f390a..759f52e34 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -380,7 +380,7 @@ class Interpreter { // this handler is only provided on the desktop and liveview implementations since this // method is not used by the web implementation -function handler(event, name, bubbles, config) { +async function handler(event, name, bubbles, config) { let target = event.target; if (target != null) { let preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); @@ -431,7 +431,7 @@ function handler(event, name, bubbles, config) { event.preventDefault(); } - let contents = serialize_event(event); + let contents = await serialize_event(event); /*POST_EVENT_SERIALIZATION*/ @@ -606,6 +606,7 @@ async function serialize_event(event) { if (event.dataTransfer && event.dataTransfer.files) { files = await serializeFileList(event.dataTransfer.files); } + return { mouse: get_mouse_data(event), files }; } case "click": From fe971ce181291bbdb432f5d03422f3506f3fac5e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 20 Jul 2023 13:20:14 -0700 Subject: [PATCH 004/126] fix file upload example --- examples/file_upload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/file_upload.rs b/examples/file_upload.rs index b498c5250..f5deab173 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -3,7 +3,7 @@ use dioxus::prelude::*; use tokio::time::sleep; fn main() { - dioxus_desktop::launch(app); + dioxus_desktop::launch(App); } fn App(cx: Scope) -> Element { From 8b4e0b3e07f831bc7f0576b4c0e9a3cde304abb1 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 8 Aug 2023 15:33:48 -0700 Subject: [PATCH 005/126] Use serde default instead of options in the CLI --- packages/cli/src/builder.rs | 31 ++------- packages/cli/src/cli/build.rs | 9 +-- packages/cli/src/cli/clean.rs | 6 +- packages/cli/src/cli/serve.rs | 11 +-- packages/cli/src/config.rs | 107 ++++++++++++++++++++--------- packages/cli/src/server/mod.rs | 21 +----- packages/cli/src/server/output.rs | 20 ++---- packages/cli/src/server/web/mod.rs | 17 +---- 8 files changed, 100 insertions(+), 122 deletions(-) diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 3fa8e7818..6dca9e779 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -130,7 +130,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { } // check binaryen:wasm-opt tool - let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default(); + let dioxus_tools = dioxus_config.application.tools.clone(); if dioxus_tools.contains_key("binaryen") { let info = dioxus_tools.get("binaryen").unwrap(); let binaryen = crate::tools::Tool::Binaryen; @@ -346,13 +346,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result String { let mut script_list = resouces.script.unwrap_or_default(); if serve { - let mut dev_style = resouces.dev.style.clone().unwrap_or_default(); - let mut dev_script = resouces.dev.script.unwrap_or_default(); + let mut dev_style = resouces.dev.style.clone(); + let mut dev_script = resouces.dev.script; style_list.append(&mut dev_style); script_list.append(&mut dev_script); } @@ -459,13 +453,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { &style.to_str().unwrap(), )) } - if config - .application - .tools - .clone() - .unwrap_or_default() - .contains_key("tailwindcss") - { + if config.application.tools.clone().contains_key("tailwindcss") { style_str.push_str("\n"); } @@ -516,12 +504,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { ); } - let title = config - .web - .app - .title - .clone() - .unwrap_or_else(|| "dioxus | ⛺".into()); + let title = config.web.app.title.clone(); replace_or_insert_before("{app_title}", &title, " Result> { let mut result = vec![]; let dioxus_config = &config.dioxus_config; - let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default(); + let dioxus_tools = dioxus_config.application.tools.clone(); // check sass tool state let sass = Tool::Sass; diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 3053a6e5a..93136aa19 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -54,14 +54,7 @@ impl Build { let mut file = std::fs::File::create( crate_config .crate_dir - .join( - crate_config - .dioxus_config - .application - .out_dir - .clone() - .unwrap_or_else(|| PathBuf::from("dist")), - ) + .join(crate_config.dioxus_config.application.out_dir.clone()) .join("index.html"), )?; file.write_all(temp.as_bytes())?; diff --git a/packages/cli/src/cli/clean.rs b/packages/cli/src/cli/clean.rs index 5fed1e1a6..09fb21469 100644 --- a/packages/cli/src/cli/clean.rs +++ b/packages/cli/src/cli/clean.rs @@ -19,11 +19,7 @@ impl Clean { return custom_error!("Cargo clean failed."); } - let out_dir = crate_config - .dioxus_config - .application - .out_dir - .unwrap_or_else(|| PathBuf::from("dist")); + let out_dir = crate_config.dioxus_config.application.out_dir; if crate_config.crate_dir.join(&out_dir).is_dir() { remove_dir_all(crate_config.crate_dir.join(&out_dir))?; } diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index 433d611b7..2681c4a5f 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -58,14 +58,9 @@ impl Serve { pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> { let serve_html = gen_page(&crate_config.dioxus_config, true); - let dist_path = crate_config.crate_dir.join( - crate_config - .dioxus_config - .application - .out_dir - .clone() - .unwrap_or_else(|| PathBuf::from("dist")), - ); + let dist_path = crate_config + .crate_dir + .join(crate_config.dioxus_config.application.out_dir.clone()); if !dist_path.is_dir() { create_dir_all(&dist_path)?; } diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 2e499451f..2fc9f96ce 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -86,33 +86,33 @@ fn acquire_dioxus_toml(dir: &Path) -> Option { impl Default for DioxusConfig { fn default() -> Self { - let name = "name"; + let name = default_name(); Self { application: ApplicationConfig { - name: name.into(), - default_platform: Platform::Web, - out_dir: Some(PathBuf::from("dist")), - asset_dir: Some(PathBuf::from("public")), + name: name.clone(), + default_platform: default_platform(), + out_dir: out_dir_default(), + asset_dir: asset_dir_default(), - tools: None, + tools: Default::default(), sub_package: None, }, web: WebConfig { app: WebAppConfig { - title: Some("dioxus | ⛺".into()), + title: default_title(), base_path: None, }, - proxy: Some(vec![]), + proxy: vec![], watcher: WebWatcherConfig { - watch_path: Some(vec![PathBuf::from("src")]), - reload_html: Some(false), - index_on_404: Some(true), + watch_path: watch_path_default(), + reload_html: false, + index_on_404: true, }, resource: WebResourceConfig { dev: WebDevResourceConfig { - style: Some(vec![]), - script: Some(vec![]), + style: vec![], + script: vec![], }, style: Some(vec![]), script: Some(vec![]), @@ -136,20 +136,44 @@ impl Default for DioxusConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApplicationConfig { + #[serde(default = "default_name")] pub name: String, + #[serde(default = "default_platform")] pub default_platform: Platform, - pub out_dir: Option, - pub asset_dir: Option, + #[serde(default = "out_dir_default")] + pub out_dir: PathBuf, + #[serde(default = "asset_dir_default")] + pub asset_dir: PathBuf, - pub tools: Option>, + #[serde(default)] + pub tools: HashMap, + #[serde(default)] pub sub_package: Option, } +fn default_name() -> String { + "name".into() +} + +fn default_platform() -> Platform { + Platform::Web +} + +fn asset_dir_default() -> PathBuf { + PathBuf::from("public") +} + +fn out_dir_default() -> PathBuf { + PathBuf::from("dist") +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebConfig { + #[serde(default)] pub app: WebAppConfig, - pub proxy: Option>, + #[serde(default)] + pub proxy: Vec, pub watcher: WebWatcherConfig, pub resource: WebResourceConfig, #[serde(default)] @@ -158,10 +182,24 @@ pub struct WebConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebAppConfig { - pub title: Option, + #[serde(default = "default_title")] + pub title: String, pub base_path: Option, } +impl Default for WebAppConfig { + fn default() -> Self { + Self { + title: default_title(), + base_path: None, + } + } +} + +fn default_title() -> String { + "dioxus | ⛺".into() +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebProxyConfig { pub backend: String, @@ -169,9 +207,16 @@ pub struct WebProxyConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebWatcherConfig { - pub watch_path: Option>, - pub reload_html: Option, - pub index_on_404: Option, + #[serde(default = "watch_path_default")] + pub watch_path: Vec, + #[serde(default)] + pub reload_html: bool, + #[serde(default = "true_bool")] + pub index_on_404: bool, +} + +fn watch_path_default() -> Vec { + vec![PathBuf::from("src")] } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -183,8 +228,10 @@ pub struct WebResourceConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebDevResourceConfig { - pub style: Option>, - pub script: Option>, + #[serde(default)] + pub style: Vec, + #[serde(default)] + pub script: Vec, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -238,17 +285,11 @@ impl CrateConfig { let workspace_dir = meta.workspace_root; let target_dir = meta.target_directory; - let out_dir = match dioxus_config.application.out_dir { - Some(ref v) => crate_dir.join(v), - None => crate_dir.join("dist"), - }; + let out_dir = crate_dir.join(&dioxus_config.application.out_dir); let cargo_def = &crate_dir.join("Cargo.toml"); - let asset_dir = match dioxus_config.application.asset_dir { - Some(ref v) => crate_dir.join(v), - None => crate_dir.join("public"), - }; + let asset_dir = crate_dir.join(&dioxus_config.application.asset_dir); let manifest = cargo_toml::Manifest::from_path(cargo_def).unwrap(); @@ -577,3 +618,7 @@ impl Default for WebviewInstallMode { Self::OfflineInstaller { silent: false } } } + +fn true_bool() -> bool { + true +} diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 19dc44b75..b0f3bc56d 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -5,10 +5,7 @@ use dioxus_core::Template; use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; use notify::{RecommendedWatcher, Watcher}; -use std::{ - path::PathBuf, - sync::{Arc, Mutex}, -}; +use std::sync::{Arc, Mutex}; use tokio::sync::broadcast::Sender; mod output; @@ -25,13 +22,7 @@ async fn setup_file_watcher Result + Send + 'static>( let mut last_update_time = chrono::Local::now().timestamp(); // file watcher: check file change - let allow_watch_path = config - .dioxus_config - .web - .watcher - .watch_path - .clone() - .unwrap_or_else(|| vec![PathBuf::from("src")]); + let allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone(); let watcher_config = config.clone(); let mut watcher = notify::recommended_watcher(move |info: notify::Result| { @@ -87,13 +78,7 @@ async fn setup_file_watcher_hot_reload Result + Send + ' web_info: Option, ) -> Result { // file watcher: check file change - let allow_watch_path = config - .dioxus_config - .web - .watcher - .watch_path - .clone() - .unwrap_or_else(|| vec![PathBuf::from("src")]); + let allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone(); let watcher_config = config.clone(); let mut last_update_time = chrono::Local::now().timestamp(); diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs index ce0f55064..68582f742 100644 --- a/packages/cli/src/server/output.rs +++ b/packages/cli/src/server/output.rs @@ -46,19 +46,13 @@ pub fn print_console_info( } else { "Default" }; - let url_rewrite = if config - .dioxus_config - .web - .watcher - .index_on_404 - .unwrap_or(false) - { + let url_rewrite = if config.dioxus_config.web.watcher.index_on_404 { "True" } else { "False" }; - let proxies = config.dioxus_config.web.proxy.as_ref(); + let proxies = &config.dioxus_config.web.proxy; if options.changed.is_empty() { println!( @@ -106,12 +100,10 @@ pub fn print_console_info( println!(); println!("\t> Profile : {}", profile.green()); println!("\t> Hot Reload : {}", hot_reload.cyan()); - if let Some(proxies) = proxies { - if !proxies.is_empty() { - println!("\t> Proxies :"); - for proxy in proxies { - println!("\t\t- {}", proxy.backend.blue()); - } + if !proxies.is_empty() { + println!("\t> Proxies :"); + for proxy in proxies { + println!("\t\t- {}", proxy.backend.blue()); } } println!("\t> Index Template : {}", custom_html_file.green()); diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index e08d2297a..768236f5d 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -322,12 +322,7 @@ async fn setup_router( .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) .and_then( move |response: Response| async move { - let response = if file_service_config - .dioxus_config - .web - .watcher - .index_on_404 - .unwrap_or(false) + let response = if file_service_config.dioxus_config.web.watcher.index_on_404 && response.status() == StatusCode::NOT_FOUND { let body = Full::from( @@ -359,7 +354,7 @@ async fn setup_router( let mut router = Router::new().route("/_dioxus/ws", get(ws_handler)); // Setup proxy - for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() { + for proxy_config in config.dioxus_config.web.proxy { router = proxy::add_proxy(router, &proxy_config)?; } @@ -476,13 +471,7 @@ fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { let result = builder::build(config, true)?; // change the websocket reload state to true; // the page will auto-reload. - if config - .dioxus_config - .web - .watcher - .reload_html - .unwrap_or(false) - { + if config.dioxus_config.web.watcher.reload_html { let _ = Serve::regen_dev_page(config); } let _ = reload_tx.send(()); From 8d1c17ba7dcb2da08379cbcfcbfdf2d20b906849 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 9 Aug 2023 11:10:41 -0700 Subject: [PATCH 006/126] fix clippy --- packages/cli/src/config.rs | 2 +- packages/core/tests/suspense.rs | 2 +- packages/desktop/src/protocol.rs | 9 +++++++-- packages/html/src/eval.rs | 2 +- packages/native-core/src/real_dom.rs | 1 - packages/rsx/src/lib.rs | 10 ++-------- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 2fc9f96ce..267a7f2a2 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -126,7 +126,7 @@ impl Default for DioxusConfig { }, bundle: BundleConfig { identifier: Some(format!("io.github.{name}")), - publisher: Some(name.into()), + publisher: Some(name), ..Default::default() }, plugin: toml::Value::Table(toml::map::Map::new()), diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 7b2516447..abce7f8a6 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -36,7 +36,7 @@ fn suspended_child(cx: Scope) -> Element { cx.spawn(async move { val += 1; }); - return cx.suspend()?; + cx.suspend()?; } render!("child") diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 89ce50a48..53652f82f 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -158,8 +158,13 @@ fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> { } let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) { - Some(t) if t == "text/plain" => get_mime_by_ext(trimmed), - Some(f) => f, + Some(f) => { + if f == "text/plain" { + get_mime_by_ext(trimmed) + } else { + f + } + } None => get_mime_by_ext(trimmed), }; diff --git a/packages/html/src/eval.rs b/packages/html/src/eval.rs index b39eef805..c90f5125b 100644 --- a/packages/html/src/eval.rs +++ b/packages/html/src/eval.rs @@ -42,7 +42,7 @@ pub fn use_eval(cx: &ScopeState) -> &EvalCreator { Rc::new(move |script: &str| { eval_provider .new_evaluator(script.to_string()) - .map(|evaluator| UseEval::new(evaluator)) + .map(UseEval::new) }) as Rc Result> }) } diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 44c378ddf..b520b2de1 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -446,7 +446,6 @@ impl RealDom { drop(tree); children.reverse(); if let Some(node) = self.get_mut(id) { - let node = node; f(node); stack.extend(children.iter()); } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 1f01fcd72..783dd3322 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -288,10 +288,7 @@ impl DynamicMapping { let idx = self.last_attribute_idx; self.last_attribute_idx += 1; - self.attribute_to_idx - .entry(attr) - .or_insert_with(Vec::new) - .push(idx); + self.attribute_to_idx.entry(attr).or_default().push(idx); idx } @@ -300,10 +297,7 @@ impl DynamicMapping { let idx = self.last_element_idx; self.last_element_idx += 1; - self.node_to_idx - .entry(node) - .or_insert_with(Vec::new) - .push(idx); + self.node_to_idx.entry(node).or_default().push(idx); idx } From d573f5dfd5c25f82b009933968bd70b46d7ef77b Mon Sep 17 00:00:00 2001 From: Koji AGAWA Date: Fri, 11 Aug 2023 11:59:52 +0900 Subject: [PATCH 007/126] feat: props spread --- Cargo.toml | 2 + packages/autofmt/src/element.rs | 1 + packages/core/src/lib.rs | 4 +- packages/core/src/nodes.rs | 31 +++++ packages/dioxus/tests/props_spread.rs | 121 ++++++++++++++++++ packages/html-internal-macro/Cargo.toml | 28 ++++ packages/html-internal-macro/src/lib.rs | 83 ++++++++++++ .../html-internal-macro/tests/01-simple.rs | 1 + .../html-internal-macro/tests/progress.rs | 5 + packages/html/Cargo.toml | 1 + packages/html/src/elements.rs | 12 +- packages/html/src/global_attributes.rs | 7 + packages/rsx-rosetta/src/lib.rs | 1 + packages/rsx/src/element.rs | 8 ++ 14 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 packages/dioxus/tests/props_spread.rs create mode 100644 packages/html-internal-macro/Cargo.toml create mode 100644 packages/html-internal-macro/src/lib.rs create mode 100644 packages/html-internal-macro/tests/01-simple.rs create mode 100644 packages/html-internal-macro/tests/progress.rs diff --git a/Cargo.toml b/Cargo.toml index 5b55f4628..0072a608e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "packages/extension", "packages/router", "packages/html", + "packages/html-internal-macro", "packages/hooks", "packages/web", "packages/ssr", @@ -60,6 +61,7 @@ dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0" } dioxus-router = { path = "packages/router", version = "0.4.1" } dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" } dioxus-html = { path = "packages/html", version = "0.4.0" } +dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.4.0" } dioxus-hooks = { path = "packages/hooks", version = "0.4.0" } dioxus-web = { path = "packages/web", version = "0.4.0" } dioxus-ssr = { path = "packages/ssr", version = "0.4.0" } diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index dbc336401..44f527337 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -49,6 +49,7 @@ impl Writer<'_> { attributes, children, brace, + extra_attributes, } = el; /* diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 62e142f60..b9e514a7c 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -73,8 +73,8 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, - CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, + fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeBox, AttributeValue, BorrowedAttributeValue, + CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, HasAttributesBox, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index a108f3577..0e3a9708b 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -832,3 +832,34 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option { } } } + +pub struct AttributeBox<'a> { + /// The name of the attribute. + pub name: &'a str, + + /// The value of the attribute + pub value: Box + 'static>, + + /// The namespace of the attribute. + /// + /// Doesn’t exist in the html spec. Used in Dioxus to denote “style” tags and other attribute groups. + pub namespace: Option<&'static str>, + + /// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated + pub volatile: bool, +} + +impl<'a> AttributeBox<'a> { + pub fn new(name: &'a str, value: impl IntoAttributeValue<'a> + 'static, namespace: Option<&'static str>, volatile: bool) -> Self { + Self { + name, + value: Box::new(value), + namespace, + volatile, + } + } +} + +pub trait HasAttributesBox<'a, T> { + fn push_attribute(self, attr: AttributeBox<'a>) -> T; +} diff --git a/packages/dioxus/tests/props_spread.rs b/packages/dioxus/tests/props_spread.rs new file mode 100644 index 000000000..a183e11da --- /dev/null +++ b/packages/dioxus/tests/props_spread.rs @@ -0,0 +1,121 @@ +use dioxus::core_macro::render; +use dioxus::prelude::rsx; +use dioxus_core::{AttributeBox, Element, HasAttributesBox, Scope}; +use dioxus_html::{ExtendedAudioMarker, ExtendedGlobalAttributesMarker}; + +#[test] +fn props_spread() { + pub struct FooProps<'a> { + pub open: Option<&'a str>, + attributes: Vec>, + } + + // ----- + impl<'a> FooProps<'a> { + #[doc = "\nCreate a builder for building `FooProps`.\nOn the builder, call `.open(...)`(optional) to set the values of the fields.\nFinally, call `.build()` to create the instance of `FooProps`.\n "] + #[allow(dead_code)] + pub fn builder() -> FooPropsBuilder<'a, ((), ), > { + FooPropsBuilder { fields: ((), ), attributes: Vec::new(), _phantom: core::default::Default::default() } + } + } + #[must_use] + #[doc(hidden)] + #[allow(dead_code, non_camel_case_types, non_snake_case)] + pub struct FooPropsBuilder<'a, TypedBuilderFields, > { + fields: TypedBuilderFields, + attributes: Vec>, + _phantom: ( core::marker::PhantomData<&'a ()> ), + } + //impl<'a, TypedBuilderFields, > Clone for FooPropsBuilder<'a, TypedBuilderFields, > where TypedBuilderFields: Clone { fn clone(&self) -> Self { Self { fields: self.fields.clone(), attributes: self.attributes, _phantom: Default::default() } } } + impl<'a> dioxus::prelude::Properties for FooProps<'a> { + type Builder = FooPropsBuilder<'a, ((), ), >; + const IS_STATIC: bool = false; + fn builder() -> Self::Builder { FooProps::builder() } + unsafe fn memoize(&self, other: &Self) -> bool { false } + } + #[doc(hidden)] + #[allow(dead_code, non_camel_case_types, non_snake_case)] + pub trait FooPropsBuilder_Optional { fn into_value T>(self, default: F) -> T; } + impl FooPropsBuilder_Optional for () { fn into_value T>(self, default: F) -> T { default() } } + impl FooPropsBuilder_Optional for (T, ) { fn into_value T>(self, _: F) -> T { self.0 } } + #[allow(dead_code, non_camel_case_types, missing_docs)] + impl<'a> FooPropsBuilder<'a, ((), )> { + pub fn open(self, open: &'a str) -> FooPropsBuilder<'a, ((Option<&'a str>, + // pub attributes: Vec>, + ), )> { + let open = (Some(open), ); + let (_, ) = self.fields; + FooPropsBuilder { fields: (open, ), attributes: self.attributes, _phantom: self._phantom } + } + } + #[doc(hidden)] + #[allow(dead_code, non_camel_case_types, non_snake_case)] + pub enum FooPropsBuilder_Error_Repeated_field_open {} + #[doc(hidden)] + #[allow(dead_code, non_camel_case_types, missing_docs)] + impl<'a> FooPropsBuilder<'a, ((Option<&'a str>, + // pub attributes: Vec>, + ), )> { + #[deprecated(note = "Repeated field open")] + pub fn open(self, _: FooPropsBuilder_Error_Repeated_field_open) -> FooPropsBuilder<'a, ((Option<&'a str>, + // pub attributes: Vec>, + ), )> { self } + } + #[allow(dead_code, non_camel_case_types, missing_docs)] + impl<'a, __open: FooPropsBuilder_Optional>> FooPropsBuilder<'a, (__open, ), > { + pub fn build(self) -> FooProps<'a> { + let (open, ) = self.fields; + let open = FooPropsBuilder_Optional::into_value(open, || Default::default()); + FooProps { open, attributes: self.attributes } + } + } + // ----- + + impl<'a, A> HasAttributesBox<'a, FooPropsBuilder<'a, (A, )>> for FooPropsBuilder<'a, (A, )> { + fn push_attribute(self, attr: AttributeBox<'a>) -> FooPropsBuilder<'a, (A, )> { + let mut attrs = Vec::from(self.attributes); + attrs.push(attr); + FooPropsBuilder { fields: self.fields, attributes: attrs, _phantom: self._phantom } + } + } + impl ExtendedGlobalAttributesMarker for FooPropsBuilder<'_, (A,)> {} + impl ExtendedAudioMarker for FooPropsBuilder<'_, (A,)> {} + + use dioxus::prelude::*; + use dioxus_html::AudioExtension; + + #[allow(non_snake_case)] + pub fn Foo<'a>(cx: Scope<'a, FooProps<'a>>) -> Element<'a> { + let muted = false; + let attributes = cx.props.attributes; + render! { + // rsx! { + // audio { + // muted: muted, + // } + // } + ::dioxus::core::LazyNodes::new(move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { + static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] }; + let mut attrs = vec![__cx.attr(dioxus_elements::audio::muted.0, muted, dioxus_elements::audio::muted.1, dioxus_elements::audio::muted.2)]; + for attr in attributes { + attrs.push(__cx.attr(attr.name, attr.value.into_value(__cx.bump()), attr.namespace, attr.volatile)); + }; + ::dioxus::core::VNode { + parent: None, + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: Default::default(), + dynamic_nodes: __cx.bump().alloc([]), + dynamic_attrs: __cx.bump().alloc(attrs), + } + }) + } + } + + rsx! { + Foo { + autoplay: true, + controls: true, + } + }; +} \ No newline at end of file diff --git a/packages/html-internal-macro/Cargo.toml b/packages/html-internal-macro/Cargo.toml new file mode 100644 index 000000000..ce8ad54b1 --- /dev/null +++ b/packages/html-internal-macro/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "dioxus-html-internal-macro" +version = { workspace = true } +edition = "2021" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react", "liveview"] +license = "MIT OR Apache-2.0" +description = "HTML function macros for Dioxus" + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = "1.0.66" +syn = { version = "2", features = ["full"] } +quote = "^1.0.26" +convert_case = "^0.6.0" + +[lib] +proc-macro = true + +[[test]] +name = "tests" +path = "tests/progress.rs" + +[dev-dependencies] +trybuild = { version = "1.0.82", features = ["diff"] } \ No newline at end of file diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs new file mode 100644 index 000000000..5794fd073 --- /dev/null +++ b/packages/html-internal-macro/src/lib.rs @@ -0,0 +1,83 @@ +use proc_macro::TokenStream; + +use convert_case::{Case, Casing}; +use quote::{quote, TokenStreamExt, ToTokens}; +use syn::{braced, Ident, parse_macro_input, Token}; +use syn::__private::TokenStream2; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; + +#[proc_macro] +pub fn impl_extension_attributes(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ImplExtensionAttributes); + input.to_token_stream().into() +} + +struct ImplExtensionAttributes { + is_element: bool, + name: Ident, + attrs: Punctuated, +} + +impl Parse for ImplExtensionAttributes { + fn parse(input: ParseStream) -> syn::Result { + let content; + + let element: Ident = input.parse()?; + let name = input.parse()?; + braced!(content in input); + let attrs = content.parse_terminated(Ident::parse, Token![,])?; + + Ok(ImplExtensionAttributes { + is_element: element.to_string() == "ELEMENT", + name, + attrs, + }) + } +} + +impl ToTokens for ImplExtensionAttributes { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let name = &self.name; + let camel_name = name.to_string().to_case(Case::UpperCamel); + let impl_name = Ident::new(format!("{}Impl", &camel_name).as_str(), name.span()); + let extension_name = Ident::new(format!("{}Extension", &camel_name).as_str(), name.span()); + let marker_name = Ident::new(format!("Extended{}Marker", &camel_name).as_str(), name.span()); + + if !self.is_element { + tokens.append_all(quote! { + trait #impl_name {} + impl #name for #impl_name {} + }); + } + + let defs = self.attrs.iter().map(|ident| { + quote! { + fn #ident(self, value: impl IntoAttributeValue<'a> + 'static) -> Self; + } + }); + let impls = self.attrs.iter().map(|ident| { + let d = if self.is_element { + quote! { #name::#ident } + } else { + quote! { #impl_name::#ident } + }; + quote! { + fn #ident(self, value: impl IntoAttributeValue<'a> + 'static) -> Self { + let d = #d; + self.push_attribute(AttributeBox::new(d.0, value, d.1, d.2)) + } + } + }); + tokens.append_all(quote! { + pub trait #marker_name {} + + pub trait #extension_name<'a> { + #(#defs)* + } + impl<'a, T> #extension_name<'a> for T where T: HasAttributesBox<'a, T> + #marker_name { + #(#impls)* + } + }); + } +} diff --git a/packages/html-internal-macro/tests/01-simple.rs b/packages/html-internal-macro/tests/01-simple.rs new file mode 100644 index 000000000..e71fdf554 --- /dev/null +++ b/packages/html-internal-macro/tests/01-simple.rs @@ -0,0 +1 @@ +fn main() {} \ No newline at end of file diff --git a/packages/html-internal-macro/tests/progress.rs b/packages/html-internal-macro/tests/progress.rs new file mode 100644 index 000000000..563805397 --- /dev/null +++ b/packages/html-internal-macro/tests/progress.rs @@ -0,0 +1,5 @@ +#[test] +fn tests() { + let t = trybuild::TestCases::new(); + t.pass("tests/01-simple.rs"); +} diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 86ccb029d..cdf072a7c 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-core = { workspace = true } dioxus-rsx = { workspace = true, features = ["hot_reload"], optional = true } +dioxus-html-internal-macro = { workspace = true } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } wasm-bindgen = { workspace = true, optional = true } diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 7e5bed124..e7e682bf5 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1,9 +1,15 @@ #![allow(non_upper_case_globals)] + +use dioxus_core::AttributeBox; +use dioxus_core::HasAttributesBox; +use dioxus_core::prelude::IntoAttributeValue; +use dioxus_html_internal_macro::impl_extension_attributes; +#[cfg(feature = "hot-reload-context")] +use dioxus_rsx::HotReloadingContext; + #[cfg(feature = "hot-reload-context")] use crate::{map_global_attributes, map_svg_attributes}; use crate::{GlobalAttributes, SvgAttributes}; -#[cfg(feature = "hot-reload-context")] -use dioxus_rsx::HotReloadingContext; pub type AttributeDiscription = (&'static str, Option<&'static str>, bool); @@ -106,6 +112,8 @@ macro_rules! impl_element { } impl GlobalAttributes for $name {} + + impl_extension_attributes![ELEMENT $name { $($fil,)* }]; }; ( diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 3792397ad..2c7718686 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -1,5 +1,10 @@ #![allow(non_upper_case_globals)] +use dioxus_core::HasAttributesBox; +use dioxus_core::AttributeBox; +use dioxus_core::prelude::IntoAttributeValue; +use dioxus_html_internal_macro::impl_extension_attributes; + use crate::AttributeDiscription; #[cfg(feature = "hot-reload-context")] @@ -62,6 +67,8 @@ macro_rules! trait_methods { )* None } + + impl_extension_attributes![GLOBAL $trait { $($name,)* }]; }; // Rename the incoming ident and apply a custom namespace diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 2ceb5f038..97678cf3a 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -73,6 +73,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { attributes, key: None, brace: Default::default(), + extra_attributes: None, })) } diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 8f45d91ff..dd33c825f 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -21,6 +21,7 @@ pub struct Element { pub attributes: Vec, pub children: Vec, pub brace: syn::token::Brace, + pub extra_attributes: Option, } impl Parse for Element { @@ -35,6 +36,7 @@ impl Parse for Element { let mut children: Vec = vec![]; let mut key = None; let mut _el_ref = None; + let mut extra_attributes = None; // parse fields with commas // break when we don't get this pattern anymore @@ -42,6 +44,11 @@ impl Parse for Element { // "def": 456, // abc: 123, loop { + if content.peek(Token![...]) { + content.parse::()?; + extra_attributes = Some(content.parse::()?); + } + // Parse the raw literal fields if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) { let name = content.parse::()?; @@ -160,6 +167,7 @@ impl Parse for Element { attributes, children, brace, + extra_attributes }) } } From 22c659d9952fa79d68d35af0da685aa5679aeffd Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 17 Sep 2023 19:01:00 -0500 Subject: [PATCH 008/126] fix formatting --- packages/fullstack/src/adapters/warp_adapter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fullstack/src/adapters/warp_adapter.rs b/packages/fullstack/src/adapters/warp_adapter.rs index 5afcb5077..316d70e56 100644 --- a/packages/fullstack/src/adapters/warp_adapter.rs +++ b/packages/fullstack/src/adapters/warp_adapter.rs @@ -143,7 +143,7 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R let req = warp::hyper::Request::from_parts(parts, bytes.into()); service.run(req).await.map_err(|err| { tracing::error!("Server function error: {}", err); - + struct WarpServerFnError(String); impl std::fmt::Debug for WarpServerFnError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -152,7 +152,7 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R } impl warp::reject::Reject for WarpServerFnError {} - + warp::reject::custom(WarpServerFnError(err.to_string())) }) } From 4691046e238e850644193ba2ea4232a7a60f9ac9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 17 Sep 2023 19:01:20 -0500 Subject: [PATCH 009/126] fix formatting --- packages/fullstack/src/adapters/warp_adapter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fullstack/src/adapters/warp_adapter.rs b/packages/fullstack/src/adapters/warp_adapter.rs index 5afcb5077..316d70e56 100644 --- a/packages/fullstack/src/adapters/warp_adapter.rs +++ b/packages/fullstack/src/adapters/warp_adapter.rs @@ -143,7 +143,7 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R let req = warp::hyper::Request::from_parts(parts, bytes.into()); service.run(req).await.map_err(|err| { tracing::error!("Server function error: {}", err); - + struct WarpServerFnError(String); impl std::fmt::Debug for WarpServerFnError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -152,7 +152,7 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R } impl warp::reject::Reject for WarpServerFnError {} - + warp::reject::custom(WarpServerFnError(err.to_string())) }) } From d3eefe4a27f85812ba4044c94cc37c8f087be2b3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 Sep 2023 09:16:58 -0500 Subject: [PATCH 010/126] fix clippy --- packages/core/src/scope_context.rs | 2 +- packages/web/src/dom.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 79ed05d75..59bed3b1f 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -128,7 +128,7 @@ impl ScopeContext { parent.name ); if let Some(shared) = parent.shared_contexts.borrow().iter().find_map(|any| { - tracing::trace!("found context {:?}", any.type_id()); + tracing::trace!("found context {:?}", (**any).type_id()); any.downcast_ref::() }) { return Some(shared.clone()); diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index b47f71757..183eb9c14 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,7 +7,6 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> -use crate::file_engine::WebFileEngine; use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; @@ -288,8 +287,10 @@ pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) - let mut files = None; if let Some(event) = event.dyn_ref::() { if let Some(data) = event.data_transfer() { + #[cfg(feature = "file_engine")] + #[allow(clippy::arc_with_non_send_sync)] if let Some(file_list) = data.files() { - files = WebFileEngine::new(file_list) + files = crate::file_engine::WebFileEngine::new(file_list) .map(|f| Arc::new(f) as Arc); } } From dc446b5e5b46bb9a0687d4a1ff5d574abf809e14 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 Sep 2023 16:38:27 -0500 Subject: [PATCH 011/126] fix extending an element --- packages/core-macro/src/props/mod.rs | 2 +- packages/core/src/fragment.rs | 2 +- packages/core/src/lib.rs | 10 +- packages/core/src/nodes.rs | 35 ++---- packages/core/src/properties.rs | 11 +- packages/dioxus/tests/props_spread.rs | 142 +++++++++++++++++------- packages/html-internal-macro/src/lib.rs | 15 ++- packages/html/src/elements.rs | 3 +- packages/html/src/global_attributes.rs | 3 +- packages/rsx/src/component.rs | 4 +- 10 files changed, 138 insertions(+), 89 deletions(-) diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 6a0d24a95..e24b8327a 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -661,7 +661,7 @@ Finally, call `.build()` to create the instance of `{name}`. { type Builder = #builder_name #generics_with_empty; const IS_STATIC: bool = #is_static; - fn builder() -> Self::Builder { + fn builder(_cx: &::dioxus_core::prelude::ScopeState) -> Self::Builder { #name::builder() } unsafe fn memoize(&self, other: &Self) -> bool { diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index a9e919bfa..fa8c7e4bd 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -94,7 +94,7 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> { impl<'a> Properties for FragmentProps<'a> { type Builder = FragmentBuilder<'a, false>; const IS_STATIC: bool = false; - fn builder() -> Self::Builder { + fn builder(_cx: &ScopeState) -> Self::Builder { FragmentBuilder(None) } unsafe fn memoize(&self, _other: &Self) -> bool { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index b9e514a7c..230ae4c34 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -73,11 +73,11 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeBox, AttributeValue, BorrowedAttributeValue, - CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, HasAttributesBox, IntoDynNode, - LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, - TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, - VirtualDom, + fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, + CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, HasAttributesBox, + IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, + ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, + VPlaceholder, VText, VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 0e3a9708b..f3d0ea750 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -833,33 +833,12 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option { } } -pub struct AttributeBox<'a> { - /// The name of the attribute. - pub name: &'a str, - - /// The value of the attribute - pub value: Box + 'static>, - - /// The namespace of the attribute. - /// - /// Doesn’t exist in the html spec. Used in Dioxus to denote “style” tags and other attribute groups. - pub namespace: Option<&'static str>, - - /// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated - pub volatile: bool, -} - -impl<'a> AttributeBox<'a> { - pub fn new(name: &'a str, value: impl IntoAttributeValue<'a> + 'static, namespace: Option<&'static str>, volatile: bool) -> Self { - Self { - name, - value: Box::new(value), - namespace, - volatile, - } - } -} - pub trait HasAttributesBox<'a, T> { - fn push_attribute(self, attr: AttributeBox<'a>) -> T; + fn push_attribute( + self, + name: &str, + ns: Option<&str>, + attr: impl IntoAttributeValue<'a>, + volatile: bool, + ) -> T; } diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index f754f8b68..d3f9f6989 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -41,7 +41,7 @@ pub trait Properties: Sized { const IS_STATIC: bool; /// Create a builder for this component. - fn builder() -> Self::Builder; + fn builder(cx: &ScopeState) -> Self::Builder; /// Memoization can only happen if the props are valid for the 'static lifetime /// @@ -54,7 +54,7 @@ pub trait Properties: Sized { impl Properties for () { type Builder = EmptyBuilder; const IS_STATIC: bool = true; - fn builder() -> Self::Builder { + fn builder(_cx: &ScopeState) -> Self::Builder { EmptyBuilder {} } unsafe fn memoize(&self, _other: &Self) -> bool { @@ -70,8 +70,11 @@ impl EmptyBuilder { /// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern /// to initialize a component's props. -pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder { - T::builder() +pub fn fc_to_builder<'a, T: Properties + 'a>( + cx: &ScopeState, + _: fn(Scope<'a, T>) -> Element<'a>, +) -> T::Builder { + T::builder(cx) } #[cfg(not(miri))] diff --git a/packages/dioxus/tests/props_spread.rs b/packages/dioxus/tests/props_spread.rs index a183e11da..8839e86e8 100644 --- a/packages/dioxus/tests/props_spread.rs +++ b/packages/dioxus/tests/props_spread.rs @@ -1,51 +1,83 @@ use dioxus::core_macro::render; use dioxus::prelude::rsx; -use dioxus_core::{AttributeBox, Element, HasAttributesBox, Scope}; +use dioxus_core::{Attribute, Element, HasAttributesBox, Scope}; use dioxus_html::{ExtendedAudioMarker, ExtendedGlobalAttributesMarker}; #[test] fn props_spread() { pub struct FooProps<'a> { pub open: Option<&'a str>, - attributes: Vec>, + attributes: Vec>, } // ----- impl<'a> FooProps<'a> { #[doc = "\nCreate a builder for building `FooProps`.\nOn the builder, call `.open(...)`(optional) to set the values of the fields.\nFinally, call `.build()` to create the instance of `FooProps`.\n "] #[allow(dead_code)] - pub fn builder() -> FooPropsBuilder<'a, ((), ), > { - FooPropsBuilder { fields: ((), ), attributes: Vec::new(), _phantom: core::default::Default::default() } + pub fn builder(cx: &ScopeState) -> FooPropsBuilder<'a, ((),)> { + FooPropsBuilder { + bump: cx.bump(), + fields: ((),), + attributes: Vec::new(), + _phantom: core::default::Default::default(), + } } } #[must_use] #[doc(hidden)] #[allow(dead_code, non_camel_case_types, non_snake_case)] - pub struct FooPropsBuilder<'a, TypedBuilderFields, > { + pub struct FooPropsBuilder<'a, TypedBuilderFields> { + bump: &'a ::dioxus::core::exports::bumpalo::Bump, fields: TypedBuilderFields, - attributes: Vec>, - _phantom: ( core::marker::PhantomData<&'a ()> ), + attributes: Vec>, + _phantom: (core::marker::PhantomData<&'a ()>), } //impl<'a, TypedBuilderFields, > Clone for FooPropsBuilder<'a, TypedBuilderFields, > where TypedBuilderFields: Clone { fn clone(&self) -> Self { Self { fields: self.fields.clone(), attributes: self.attributes, _phantom: Default::default() } } } impl<'a> dioxus::prelude::Properties for FooProps<'a> { - type Builder = FooPropsBuilder<'a, ((), ), >; + type Builder = FooPropsBuilder<'a, ((),)>; const IS_STATIC: bool = false; - fn builder() -> Self::Builder { FooProps::builder() } - unsafe fn memoize(&self, other: &Self) -> bool { false } + fn builder(cx: &ScopeState) -> Self::Builder { + FooProps::builder(cx) + } + unsafe fn memoize(&self, other: &Self) -> bool { + false + } } #[doc(hidden)] #[allow(dead_code, non_camel_case_types, non_snake_case)] - pub trait FooPropsBuilder_Optional { fn into_value T>(self, default: F) -> T; } - impl FooPropsBuilder_Optional for () { fn into_value T>(self, default: F) -> T { default() } } - impl FooPropsBuilder_Optional for (T, ) { fn into_value T>(self, _: F) -> T { self.0 } } + pub trait FooPropsBuilder_Optional { + fn into_value T>(self, default: F) -> T; + } + impl FooPropsBuilder_Optional for () { + fn into_value T>(self, default: F) -> T { + default() + } + } + impl FooPropsBuilder_Optional for (T,) { + fn into_value T>(self, _: F) -> T { + self.0 + } + } #[allow(dead_code, non_camel_case_types, missing_docs)] - impl<'a> FooPropsBuilder<'a, ((), )> { - pub fn open(self, open: &'a str) -> FooPropsBuilder<'a, ((Option<&'a str>, - // pub attributes: Vec>, - ), )> { - let open = (Some(open), ); - let (_, ) = self.fields; - FooPropsBuilder { fields: (open, ), attributes: self.attributes, _phantom: self._phantom } + impl<'a> FooPropsBuilder<'a, ((),)> { + pub fn open( + self, + open: &'a str, + ) -> FooPropsBuilder< + 'a, + (( + Option<&'a str>, + // pub attributes: Vec>, + ),), + > { + let open = (Some(open),); + let (_,) = self.fields; + FooPropsBuilder { + bump: self.bump, + fields: (open,), + attributes: self.attributes, + _phantom: self._phantom, + } } } #[doc(hidden)] @@ -53,33 +85,67 @@ fn props_spread() { pub enum FooPropsBuilder_Error_Repeated_field_open {} #[doc(hidden)] #[allow(dead_code, non_camel_case_types, missing_docs)] - impl<'a> FooPropsBuilder<'a, ((Option<&'a str>, - // pub attributes: Vec>, - ), )> { + impl<'a> + FooPropsBuilder< + 'a, + (( + Option<&'a str>, + // pub attributes: Vec>, + ),), + > + { #[deprecated(note = "Repeated field open")] - pub fn open(self, _: FooPropsBuilder_Error_Repeated_field_open) -> FooPropsBuilder<'a, ((Option<&'a str>, - // pub attributes: Vec>, - ), )> { self } + pub fn open( + self, + _: FooPropsBuilder_Error_Repeated_field_open, + ) -> FooPropsBuilder< + 'a, + (( + Option<&'a str>, + // pub attributes: Vec>, + ),), + > { + self + } } #[allow(dead_code, non_camel_case_types, missing_docs)] - impl<'a, __open: FooPropsBuilder_Optional>> FooPropsBuilder<'a, (__open, ), > { + impl<'a, __open: FooPropsBuilder_Optional>> FooPropsBuilder<'a, (__open,)> { pub fn build(self) -> FooProps<'a> { - let (open, ) = self.fields; + let (open,) = self.fields; let open = FooPropsBuilder_Optional::into_value(open, || Default::default()); - FooProps { open, attributes: self.attributes } + FooProps { + open, + attributes: self.attributes, + } } } // ----- - impl<'a, A> HasAttributesBox<'a, FooPropsBuilder<'a, (A, )>> for FooPropsBuilder<'a, (A, )> { - fn push_attribute(self, attr: AttributeBox<'a>) -> FooPropsBuilder<'a, (A, )> { + impl<'a, A> HasAttributesBox<'a, FooPropsBuilder<'a, (A,)>> for FooPropsBuilder<'a, (A,)> { + fn push_attribute( + self, + name: &str, + ns: Option<&str>, + attr: impl IntoAttributeValue<'a>, + volatile: bool, + ) -> Self { let mut attrs = Vec::from(self.attributes); - attrs.push(attr); - FooPropsBuilder { fields: self.fields, attributes: attrs, _phantom: self._phantom } + attrs.push(Attribute::new( + name, + attr.into_value(self.bump), + ns, + volatile, + )); + FooPropsBuilder { + bump: self.bump, + fields: self.fields, + attributes: attrs, + _phantom: self._phantom, + } } } - impl ExtendedGlobalAttributesMarker for FooPropsBuilder<'_, (A,)> {} - impl ExtendedAudioMarker for FooPropsBuilder<'_, (A,)> {} + impl ExtendedGlobalAttributesMarker for FooPropsBuilder<'_, (A,)> {} + impl ExtendedAudioMarker for FooPropsBuilder<'_, (A,)> {} use dioxus::prelude::*; use dioxus_html::AudioExtension; @@ -87,7 +153,7 @@ fn props_spread() { #[allow(non_snake_case)] pub fn Foo<'a>(cx: Scope<'a, FooProps<'a>>) -> Element<'a> { let muted = false; - let attributes = cx.props.attributes; + let attributes = &cx.props.attributes; render! { // rsx! { // audio { @@ -99,7 +165,7 @@ fn props_spread() { let mut attrs = vec![__cx.attr(dioxus_elements::audio::muted.0, muted, dioxus_elements::audio::muted.1, dioxus_elements::audio::muted.2)]; for attr in attributes { attrs.push(__cx.attr(attr.name, attr.value.into_value(__cx.bump()), attr.namespace, attr.volatile)); - }; + } ::dioxus::core::VNode { parent: None, key: None, @@ -118,4 +184,4 @@ fn props_spread() { controls: true, } }; -} \ No newline at end of file +} diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs index 5794fd073..1a1d22d08 100644 --- a/packages/html-internal-macro/src/lib.rs +++ b/packages/html-internal-macro/src/lib.rs @@ -1,11 +1,11 @@ use proc_macro::TokenStream; use convert_case::{Case, Casing}; -use quote::{quote, TokenStreamExt, ToTokens}; -use syn::{braced, Ident, parse_macro_input, Token}; +use quote::{quote, ToTokens, TokenStreamExt}; use syn::__private::TokenStream2; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; +use syn::{braced, parse_macro_input, Ident, Token}; #[proc_macro] pub fn impl_extension_attributes(input: TokenStream) -> TokenStream { @@ -42,11 +42,14 @@ impl ToTokens for ImplExtensionAttributes { let camel_name = name.to_string().to_case(Case::UpperCamel); let impl_name = Ident::new(format!("{}Impl", &camel_name).as_str(), name.span()); let extension_name = Ident::new(format!("{}Extension", &camel_name).as_str(), name.span()); - let marker_name = Ident::new(format!("Extended{}Marker", &camel_name).as_str(), name.span()); + let marker_name = Ident::new( + format!("Extended{}Marker", &camel_name).as_str(), + name.span(), + ); if !self.is_element { tokens.append_all(quote! { - trait #impl_name {} + struct #impl_name; impl #name for #impl_name {} }); } @@ -60,12 +63,12 @@ impl ToTokens for ImplExtensionAttributes { let d = if self.is_element { quote! { #name::#ident } } else { - quote! { #impl_name::#ident } + quote! { <#impl_name as #name>::#ident } }; quote! { fn #ident(self, value: impl IntoAttributeValue<'a> + 'static) -> Self { let d = #d; - self.push_attribute(AttributeBox::new(d.0, value, d.1, d.2)) + self.push_attribute(d.0, d.1, value, d.2) } } }); diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index e7e682bf5..e1aaf84f4 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1,8 +1,7 @@ #![allow(non_upper_case_globals)] -use dioxus_core::AttributeBox; -use dioxus_core::HasAttributesBox; use dioxus_core::prelude::IntoAttributeValue; +use dioxus_core::HasAttributesBox; use dioxus_html_internal_macro::impl_extension_attributes; #[cfg(feature = "hot-reload-context")] use dioxus_rsx::HotReloadingContext; diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 2c7718686..97811fe1f 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -1,8 +1,7 @@ #![allow(non_upper_case_globals)] -use dioxus_core::HasAttributesBox; -use dioxus_core::AttributeBox; use dioxus_core::prelude::IntoAttributeValue; +use dioxus_core::HasAttributesBox; use dioxus_html_internal_macro::impl_extension_attributes; use crate::AttributeDiscription; diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index 6487e5ab7..bccef2638 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -151,8 +151,8 @@ impl ToTokens for Component { } None => { let mut toks = match prop_gen_args { - Some(gen_args) => quote! { fc_to_builder(#name #gen_args) }, - None => quote! { fc_to_builder(#name) }, + Some(gen_args) => quote! { fc_to_builder(__cx, #name #gen_args) }, + None => quote! { fc_to_builder(__cx, #name) }, }; for field in &self.fields { match field.name.to_string().as_str() { From fc8c25280a5dc25acf5d9883ffab8ab6f785da0c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 20 Sep 2023 16:02:04 -0500 Subject: [PATCH 012/126] allow many attributes to be attached to one element --- packages/core/src/create.rs | 18 +++- packages/core/src/diff.rs | 84 ++++++++++++++-- packages/core/src/lib.rs | 16 ++-- packages/core/src/nodes.rs | 76 +++++++++++++-- packages/core/src/scopes.rs | 29 +++--- packages/core/src/virtual_dom.rs | 95 +++++++++++++++---- packages/core/tests/fuzzing.rs | 7 +- packages/dioxus/tests/props_spread.rs | 2 +- .../html-internal-macro/tests/01-simple.rs | 2 +- packages/html/src/events.rs | 4 +- packages/rsx/src/element.rs | 10 +- packages/ssr/src/renderer.rs | 42 ++++---- 12 files changed, 289 insertions(+), 96 deletions(-) diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 7fbc8a9c8..226a446f9 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,5 +1,7 @@ use crate::any_props::AnyProps; -use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText}; +use crate::innerlude::{ + AttributeType, BorrowedAttributeValue, MountedAttribute, VComponent, VPlaceholder, VText, +}; use crate::mutations::Mutation; use crate::mutations::Mutation::*; use crate::nodes::VNode; @@ -283,7 +285,7 @@ impl<'b> VirtualDom { let id = self.assign_static_node_as_dynamic(path, root, node, attr_id); loop { - self.write_attribute(&node.dynamic_attrs[attr_id], id); + self.write_attribute_type(&node.dynamic_attrs[attr_id], id); // Only push the dynamic attributes forward if they match the current path (same element) match attrs.next_if(|(_, p)| *p == path) { @@ -294,10 +296,20 @@ impl<'b> VirtualDom { } } - fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) { + fn write_attribute_type(&mut self, attribute: &'b MountedAttribute<'b>, id: ElementId) { // Make sure we set the attribute's associated id attribute.mounted_element.set(id); + match &attribute.ty { + AttributeType::Single(attribute) => self.write_attribute(attribute, id), + AttributeType::Many(attribute) => { + for attribute in *attribute { + self.write_attribute(attribute, id); + } + } + } + } + pub(crate) fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) { // Safety: we promise not to re-alias this text later on after committing it to the mutation let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) }; diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 270c81730..12fb31b1b 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,7 +1,9 @@ use crate::{ any_props::AnyProps, arena::ElementId, - innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText}, + innerlude::{ + AttributeType, BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText, + }, mutations::Mutation, nodes::RenderReturn, nodes::{DynamicNode, VNode}, @@ -103,16 +105,51 @@ impl<'b> VirtualDom { .zip(right_template.dynamic_attrs.iter()) .for_each(|(left_attr, right_attr)| { // Move over the ID from the old to the new - right_attr - .mounted_element - .set(left_attr.mounted_element.get()); + let mounted_id = left_attr.mounted_element.get(); + right_attr.mounted_element.set(mounted_id); // We want to make sure anything that gets pulled is valid self.update_template(left_attr.mounted_element.get(), right_template); - // If the attributes are different (or volatile), we need to update them - if left_attr.value != right_attr.value || left_attr.volatile { - self.update_attribute(right_attr, left_attr); + match (&left_attr.ty, &right_attr.ty) { + (AttributeType::Single(left), AttributeType::Single(right)) => { + self.diff_attribute(left, right, mounted_id) + } + (AttributeType::Many(left), AttributeType::Many(right)) => { + let mut left_iter = left.iter().peekable(); + let mut right_iter = right.iter().peekable(); + + loop { + match (left_iter.peek(), right_iter.peek()) { + (Some(left), Some(right)) => { + // check which name is greater + match left.name.cmp(right.name) { + std::cmp::Ordering::Less => self.remove_attribute( + left.name, + left.namespace, + mounted_id, + ), + std::cmp::Ordering::Greater => { + self.write_attribute(right, mounted_id) + } + std::cmp::Ordering::Equal => { + self.diff_attribute(left, right, mounted_id) + } + } + } + (Some(_), None) => { + let left = left_iter.next().unwrap(); + self.remove_attribute(left.name, left.namespace, mounted_id) + } + (None, Some(_)) => { + let right = right_iter.next().unwrap(); + self.write_attribute(right, mounted_id) + } + (None, None) => break, + } + } + } + _ => unreachable!("The macro should never generate this case"), } }); @@ -138,6 +175,18 @@ impl<'b> VirtualDom { } } + fn diff_attribute( + &mut self, + left_attr: &'b Attribute<'b>, + right_attr: &'b Attribute<'b>, + id: ElementId, + ) { + // If the attributes are different (or volatile), we need to update them + if left_attr.value != right_attr.value || left_attr.volatile { + self.update_attribute(right_attr, left_attr, id); + } + } + fn diff_dynamic_node( &mut self, left_node: &'b DynamicNode<'b>, @@ -155,12 +204,29 @@ impl<'b> VirtualDom { }; } - fn update_attribute(&mut self, right_attr: &'b Attribute<'b>, left_attr: &'b Attribute) { + fn remove_attribute(&mut self, name: &'b str, ns: Option<&'static str>, id: ElementId) { + let name = unsafe { std::mem::transmute(name) }; + let value: BorrowedAttributeValue<'b> = BorrowedAttributeValue::None; + let value = unsafe { std::mem::transmute(value) }; + self.mutations.push(Mutation::SetAttribute { + id, + ns, + name, + value, + }); + } + + fn update_attribute( + &mut self, + right_attr: &'b Attribute<'b>, + left_attr: &'b Attribute<'b>, + id: ElementId, + ) { let name = unsafe { std::mem::transmute(left_attr.name) }; let value: BorrowedAttributeValue<'b> = (&right_attr.value).into(); let value = unsafe { std::mem::transmute(value) }; self.mutations.push(Mutation::SetAttribute { - id: left_attr.mounted_element.get(), + id, ns: right_attr.namespace, name, value, diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 230ae4c34..45fc43451 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -73,11 +73,11 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, - CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, HasAttributesBox, - IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, - ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, - VPlaceholder, VText, VirtualDom, + fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeType, AttributeValue, + BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event, + Fragment, HasAttributesBox, IntoDynNode, LazyNodes, MountedAttribute, Mutation, Mutations, + Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, + TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types @@ -88,9 +88,9 @@ pub mod prelude { consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, provide_context, provide_context_to_scope, provide_root_context, push_future, remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue, - Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes, - Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, - TemplateNode, Throw, VNode, VirtualDom, + AttributeType, Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, + LazyNodes, MountedAttribute, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, + Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index f3d0ea750..e34fb7d42 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -60,7 +60,7 @@ pub struct VNode<'a> { pub dynamic_nodes: &'a [DynamicNode<'a>], /// The dynamic parts of the template - pub dynamic_attrs: &'a [Attribute<'a>], + pub dynamic_attrs: &'a [MountedAttribute<'a>], } impl<'a> VNode<'a> { @@ -414,6 +414,44 @@ pub enum TemplateAttribute<'a> { }, } +#[derive(Debug)] +pub struct MountedAttribute<'a> { + pub(crate) ty: AttributeType<'a>, + + /// The element in the DOM that this attribute belongs to + pub(crate) mounted_element: Cell, +} + +impl<'a> From> for MountedAttribute<'a> { + fn from(attr: Attribute<'a>) -> Self { + Self { + ty: AttributeType::Single(attr), + mounted_element: Default::default(), + } + } +} + +impl<'a> From<&'a [Attribute<'a>]> for MountedAttribute<'a> { + fn from(attr: &'a [Attribute<'a>]) -> Self { + Self { + ty: AttributeType::Many(attr), + mounted_element: Default::default(), + } + } +} + +impl<'a> MountedAttribute<'a> { + /// Get the type of this attribute + pub fn attribute_type(&self) -> &AttributeType<'a> { + &self.ty + } + + /// Get the element that this attribute is mounted to + pub fn mounted_element(&self) -> ElementId { + self.mounted_element.get() + } +} + /// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"` #[derive(Debug)] pub struct Attribute<'a> { @@ -430,9 +468,6 @@ pub struct Attribute<'a> { /// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated pub volatile: bool, - - /// The element in the DOM that this attribute belongs to - pub(crate) mounted_element: Cell, } impl<'a> Attribute<'a> { @@ -448,13 +483,38 @@ impl<'a> Attribute<'a> { value, namespace, volatile, - mounted_element: Cell::new(ElementId::default()), + } + } +} + +#[derive(Debug)] +pub enum AttributeType<'a> { + Single(Attribute<'a>), + /// Many different attributes sorted by name + Many(&'a [Attribute<'a>]), +} + +impl<'a> AttributeType<'a> { + /// Call the given function on each attribute + pub fn for_each<'b, F>(&'b self, mut f: F) + where + F: FnMut(&'b Attribute<'a>), + { + match self { + Self::Single(attr) => f(attr), + Self::Many(attrs) => attrs.iter().for_each(f), } } - /// Get the element that this attribute is mounted to - pub fn mounted_element(&self) -> ElementId { - self.mounted_element.get() + /// Try to call the given function on each attribute + pub fn try_for_each<'b, F, E>(&'b self, mut f: F) -> Result<(), E> + where + F: FnMut(&'b Attribute<'a>) -> Result<(), E>, + { + match self { + Self::Single(attr) => f(attr), + Self::Many(attrs) => attrs.iter().try_for_each(f), + } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 5ada89069..5d279f86a 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -8,7 +8,8 @@ use crate::{ nodes::{IntoAttributeValue, IntoDynNode, RenderReturn}, runtime::Runtime, scope_context::ScopeContext, - AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId, + AnyValue, Attribute, AttributeType, AttributeValue, Element, Event, MountedAttribute, + Properties, TaskId, }; use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ @@ -335,14 +336,10 @@ impl<'src> ScopeState { let mut listeners = self.attributes_to_drop.borrow_mut(); for attr in element.dynamic_attrs { - match attr.value { - AttributeValue::Any(_) | AttributeValue::Listener(_) => { - let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; - listeners.push(unbounded); - } - - _ => (), - } + attr.ty.for_each(|attr| { + let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; + listeners.push(unbounded); + }) } let mut props = self.borrowed_props.borrow_mut(); @@ -393,13 +390,15 @@ impl<'src> ScopeState { value: impl IntoAttributeValue<'src>, namespace: Option<&'static str>, volatile: bool, - ) -> Attribute<'src> { - Attribute { - name, - namespace, - volatile, + ) -> MountedAttribute<'src> { + MountedAttribute { + ty: AttributeType::Single(Attribute { + name, + namespace, + volatile, + value: value.into_value(self.bump()), + }), mounted_element: Default::default(), - value: value.into_value(self.bump()), } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index a1def71d2..bee13b3a2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -5,13 +5,13 @@ use crate::{ any_props::VProps, arena::{ElementId, ElementRef}, - innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg}, + innerlude::{AttributeType, DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg}, mutations::Mutation, nodes::RenderReturn, nodes::{Template, TemplateId}, runtime::{Runtime, RuntimeGuard}, scopes::{ScopeId, ScopeState}, - AttributeValue, Element, Event, Scope, + Attribute, AttributeValue, Element, Event, Scope, }; use futures_util::{pin_mut, StreamExt}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -371,12 +371,30 @@ impl VirtualDom { for (idx, attr) in template.dynamic_attrs.iter().enumerate() { let this_path = node_template.attr_paths[idx]; - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - if attr.name.trim_start_matches("on") == name - && target_path.is_decendant(&this_path) - { - listeners.push(&attr.value); + fn add_listener<'a>( + attribute: &'a Attribute<'a>, + event_name: &str, + listeners: &mut Vec<&'a AttributeValue<'a>>, + ) { + if attribute.name.trim_start_matches("on") == event_name { + listeners.push(&attribute.value); + } + listeners.push(&attribute.value); + } + + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + if target_path.is_decendant(&this_path) { + match &attr.ty { + AttributeType::Single(attribute) => { + add_listener(attribute, name, &mut listeners); + } + AttributeType::Many(attributes) => { + for attribute in *attributes { + add_listener(attribute, name, &mut listeners); + } + } + } // Break if this is the exact target element. // This means we won't call two listeners with the same name on the same element. This should be // documented, or be rejected from the rsx! macro outright @@ -422,20 +440,57 @@ impl VirtualDom { for (idx, attr) in template.dynamic_attrs.iter().enumerate() { let this_path = node_template.attr_paths[idx]; - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - // Only call the listener if this is the exact target element. - if attr.name.trim_start_matches("on") == name && target_path == this_path { - if let AttributeValue::Listener(listener) = &attr.value { - let origin = el_ref.scope; - self.runtime.scope_stack.borrow_mut().push(origin); - self.runtime.rendering.set(false); - if let Some(cb) = listener.borrow_mut().as_deref_mut() { - cb(uievent.clone()); - } - self.runtime.scope_stack.borrow_mut().pop(); - self.runtime.rendering.set(true); + fn call_listener( + attribute: &Attribute, + event_name: &str, + uievent: &Event, + runtime: &Runtime, + origin: ScopeId, + ) -> bool { + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + // Only call the listener if this is the exact target element. + if attribute.name.trim_start_matches("on") == event_name { + if let AttributeValue::Listener(listener) = &attribute.value { + runtime.scope_stack.borrow_mut().push(origin); + runtime.rendering.set(false); + if let Some(cb) = listener.borrow_mut().as_deref_mut() { + cb(uievent.clone()); + } + runtime.scope_stack.borrow_mut().pop(); + runtime.rendering.set(true); - break; + return true; + } + } + false + } + + if target_path == this_path { + match &attr.ty { + AttributeType::Single(attribute) => { + if call_listener( + attribute, + name, + &uievent, + &self.runtime, + el_ref.scope, + ) { + return; + } + } + AttributeType::Many(attributes) => { + for attribute in *attributes { + if call_listener( + attribute, + name, + &uievent, + &self.runtime, + el_ref.scope, + ) { + return; + } + } + } } } } diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index 40bb5dcbf..0de10ec56 100644 --- a/packages/core/tests/fuzzing.rs +++ b/packages/core/tests/fuzzing.rs @@ -1,7 +1,7 @@ #![cfg(not(miri))] use dioxus::prelude::Props; -use dioxus_core::*; +use dioxus_core::{MountedAttribute, *}; use std::{cell::Cell, collections::HashSet}; fn random_ns() -> Option<&'static str> { @@ -205,7 +205,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { } } -fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { +fn create_random_dynamic_attr(cx: &ScopeState) -> MountedAttribute { let value = match rand::random::() % 7 { 0 => AttributeValue::Text(Box::leak( format!("{}", rand::random::()).into_boxed_str(), @@ -217,7 +217,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { 5 => AttributeValue::None, 6 => { let value = cx.listener(|e: Event| println!("{:?}", e)); - return Attribute::new("ondata", value, None, false); + return Attribute::new("ondata", value, None, false).into(); } _ => unreachable!(), }; @@ -227,6 +227,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { random_ns(), rand::random(), ) + .into() } static mut TEMPLATE_COUNT: usize = 0; diff --git a/packages/dioxus/tests/props_spread.rs b/packages/dioxus/tests/props_spread.rs index 8839e86e8..01423ea80 100644 --- a/packages/dioxus/tests/props_spread.rs +++ b/packages/dioxus/tests/props_spread.rs @@ -164,7 +164,7 @@ fn props_spread() { static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] }; let mut attrs = vec![__cx.attr(dioxus_elements::audio::muted.0, muted, dioxus_elements::audio::muted.1, dioxus_elements::audio::muted.2)]; for attr in attributes { - attrs.push(__cx.attr(attr.name, attr.value.into_value(__cx.bump()), attr.namespace, attr.volatile)); + attrs.push(attr); } ::dioxus::core::VNode { parent: None, diff --git a/packages/html-internal-macro/tests/01-simple.rs b/packages/html-internal-macro/tests/01-simple.rs index e71fdf554..f328e4d9d 100644 --- a/packages/html-internal-macro/tests/01-simple.rs +++ b/packages/html-internal-macro/tests/01-simple.rs @@ -1 +1 @@ -fn main() {} \ No newline at end of file +fn main() {} diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index c59149493..dc4e2b815 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -9,7 +9,7 @@ macro_rules! impl_event { $( $( #[$attr] )* #[inline] - pub fn $name<'a, E: crate::EventReturn, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::Attribute<'a> { + pub fn $name<'a, E: crate::EventReturn, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::MountedAttribute<'a> { ::dioxus_core::Attribute::new( stringify!($name), _cx.listener(move |e: ::dioxus_core::Event<$data>| { @@ -17,7 +17,7 @@ macro_rules! impl_event { }), None, false, - ) + ).into() } )* }; diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index dd33c825f..f64ab9809 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -167,7 +167,7 @@ impl Parse for Element { attributes, children, brace, - extra_attributes + extra_attributes, }) } } @@ -397,11 +397,3 @@ impl ToTokens for ElementAttrNamed { tokens.append_all(attribute); } } - -// ::dioxus::core::Attribute { -// name: stringify!(#name), -// namespace: None, -// volatile: false, -// mounted_node: Default::default(), -// value: ::dioxus::core::AttributeValue::Text(#value), -// } diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index f23e654fb..74066ad44 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -79,23 +79,31 @@ impl Renderer { match segment { Segment::Attr(idx) => { let attr = &template.dynamic_attrs[*idx]; - if attr.name == "dangerous_inner_html" { - inner_html = Some(attr); - } else if attr.namespace == Some("style") { - accumulated_dynamic_styles.push(attr); - } else { - match attr.value { - AttributeValue::Text(value) => { - write!(buf, " {}=\"{}\"", attr.name, value)? - } - AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?, - AttributeValue::Int(value) => write!(buf, " {}={}", attr.name, value)?, - AttributeValue::Float(value) => { - write!(buf, " {}={}", attr.name, value)? - } - _ => {} - }; - } + attr.attribute_type().try_for_each(|attr| { + if attr.name == "dangerous_inner_html" { + inner_html = Some(attr); + } else if attr.namespace == Some("style") { + accumulated_dynamic_styles.push(attr); + } else { + match attr.value { + AttributeValue::Text(value) => { + write!(buf, " {}=\"{}\"", attr.name, value)? + } + AttributeValue::Bool(value) => { + write!(buf, " {}={}", attr.name, value)? + } + AttributeValue::Int(value) => { + write!(buf, " {}={}", attr.name, value)? + } + AttributeValue::Float(value) => { + write!(buf, " {}={}", attr.name, value)? + } + _ => {} + }; + } + + Ok(()) + })?; } Segment::Node(idx) => match &template.dynamic_nodes[*idx] { DynamicNode::Component(node) => { From fa494349fee7c25d1a284d9c08f674a5f04cf286 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 20 Sep 2023 16:15:11 -0500 Subject: [PATCH 013/126] add a lifetime to the properties trait and make the manual spread example work --- examples/spread.rs | 209 ++++++++++++++++++++++++++ packages/core-macro/src/props/mod.rs | 4 +- packages/core/src/fragment.rs | 2 +- packages/core/src/nodes.rs | 4 +- packages/core/src/properties.rs | 10 +- packages/core/src/scopes.rs | 2 +- packages/dioxus/tests/props_spread.rs | 187 ----------------------- 7 files changed, 220 insertions(+), 198 deletions(-) create mode 100644 examples/spread.rs delete mode 100644 packages/dioxus/tests/props_spread.rs diff --git a/examples/spread.rs b/examples/spread.rs new file mode 100644 index 000000000..eb09e5dd9 --- /dev/null +++ b/examples/spread.rs @@ -0,0 +1,209 @@ +//! Example: SSR +//! +//! This example shows how we can render the Dioxus Virtualdom using SSR. + +use dioxus::core::{Attribute, HasAttributesBox}; +use dioxus::html::{AudioExtension, ExtendedAudioMarker, ExtendedGlobalAttributesMarker}; +use dioxus::prelude::*; + +fn main() { + // We can render VirtualDoms + let mut vdom = VirtualDom::new(app); + let _ = vdom.rebuild(); + println!("{}", dioxus_ssr::render(&vdom)); + + // Or we can render rsx! calls themselves + println!( + "{}", + dioxus_ssr::render_lazy(rsx! { + div { + h1 { "Hello, world!" } + } + }) + ); + + // We can configure the SSR rendering to add ids for rehydration + println!("{}", dioxus_ssr::pre_render(&vdom)); + + // We can render to a buf directly too + let mut file = String::new(); + let mut renderer = dioxus_ssr::Renderer::default(); + renderer.render_to(&mut file, &vdom).unwrap(); + println!("{file}"); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx!(Foo { + autoplay: true, + controls: true, + })) +} + +pub struct FooProps<'a> { + pub open: Option<&'a str>, + attributes: Vec>, +} + +// ----- +impl<'a> FooProps<'a> { + #[doc = "\nCreate a builder for building `FooProps`.\nOn the builder, call `.open(...)`(optional) to set the values of the fields.\nFinally, call `.build()` to create the instance of `FooProps`.\n "] + #[allow(dead_code)] + pub fn builder(cx: &'a ScopeState) -> FooPropsBuilder<'a, ((),)> { + FooPropsBuilder { + bump: cx.bump(), + fields: ((),), + attributes: Vec::new(), + _phantom: core::default::Default::default(), + } + } +} +#[must_use] +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub struct FooPropsBuilder<'a, TypedBuilderFields> { + bump: &'a ::dioxus::core::exports::bumpalo::Bump, + fields: TypedBuilderFields, + attributes: Vec>, + _phantom: (core::marker::PhantomData<&'a ()>), +} +//impl<'a, TypedBuilderFields, > Clone for FooPropsBuilder<'a, TypedBuilderFields, > where TypedBuilderFields: Clone { fn clone(&self) -> Self { Self { fields: self.fields.clone(), attributes: self.attributes, _phantom: Default::default() } } } +impl<'a> dioxus::prelude::Properties<'a> for FooProps<'a> { + type Builder = FooPropsBuilder<'a, ((),)>; + const IS_STATIC: bool = false; + fn builder(cx: &'a ScopeState) -> Self::Builder { + FooProps::builder(cx) + } + unsafe fn memoize(&self, other: &Self) -> bool { + false + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub trait FooPropsBuilder_Optional { + fn into_value T>(self, default: F) -> T; +} +impl FooPropsBuilder_Optional for () { + fn into_value T>(self, default: F) -> T { + default() + } +} +impl FooPropsBuilder_Optional for (T,) { + fn into_value T>(self, _: F) -> T { + self.0 + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a> FooPropsBuilder<'a, ((),)> { + pub fn open( + self, + open: &'a str, + ) -> FooPropsBuilder< + 'a, + (( + Option<&'a str>, + // pub attributes: Vec>, + ),), + > { + let open = (Some(open),); + let (_,) = self.fields; + FooPropsBuilder { + bump: self.bump, + fields: (open,), + attributes: self.attributes, + _phantom: self._phantom, + } + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub enum FooPropsBuilder_Error_Repeated_field_open {} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a> + FooPropsBuilder< + 'a, + (( + Option<&'a str>, + // pub attributes: Vec>, + ),), + > +{ + #[deprecated(note = "Repeated field open")] + pub fn open( + self, + _: FooPropsBuilder_Error_Repeated_field_open, + ) -> FooPropsBuilder< + 'a, + (( + Option<&'a str>, + // pub attributes: Vec>, + ),), + > { + self + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __open: FooPropsBuilder_Optional>> FooPropsBuilder<'a, (__open,)> { + pub fn build(self) -> FooProps<'a> { + let (open,) = self.fields; + let open = FooPropsBuilder_Optional::into_value(open, || Default::default()); + FooProps { + open, + attributes: self.attributes, + } + } +} +// ----- + +impl<'a, A> HasAttributesBox<'a, FooPropsBuilder<'a, (A,)>> for FooPropsBuilder<'a, (A,)> { + fn push_attribute( + self, + name: &'a str, + ns: Option<&'static str>, + attr: impl IntoAttributeValue<'a>, + volatile: bool, + ) -> Self { + let mut attrs = self.attributes; + // We insert attributes so that the list is binary-searchable + if let Err(index) = attrs.binary_search_by(|probe| probe.name.cmp(name)) { + attrs.insert( + index, + Attribute::new(name, attr.into_value(self.bump), ns, volatile), + ); + } + FooPropsBuilder { + bump: self.bump, + fields: self.fields, + attributes: attrs, + _phantom: self._phantom, + } + } +} +impl ExtendedGlobalAttributesMarker for FooPropsBuilder<'_, (A,)> {} +impl ExtendedAudioMarker for FooPropsBuilder<'_, (A,)> {} + +#[allow(non_snake_case)] +pub fn Foo<'a>(cx: Scope<'a, FooProps<'a>>) -> Element<'a> { + let muted = false; + let attributes = &cx.props.attributes; + render! { + // rsx! { + // audio { + // muted: muted, + // } + // } + ::dioxus::core::LazyNodes::new(move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { + static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "123" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] }; + let mut attrs = vec![__cx.attr(dioxus_elements::audio::muted.0, muted, dioxus_elements::audio::muted.1, dioxus_elements::audio::muted.2)]; + attrs.push((&**attributes).into()); + ::dioxus::core::VNode { + parent: None, + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: Default::default(), + dynamic_nodes: __cx.bump().alloc([]), + dynamic_attrs: __cx.bump().alloc(attrs), + } + }) + } +} diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index e24b8327a..c7824db85 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -656,12 +656,12 @@ Finally, call `.build()` to create the instance of `{name}`. } } - impl #impl_generics dioxus::prelude::Properties for #name #ty_generics + impl #impl_generics ::dioxus::prelude::Properties<'_> for #name #ty_generics #b_generics_where_extras_predicates { type Builder = #builder_name #generics_with_empty; const IS_STATIC: bool = #is_static; - fn builder(_cx: &::dioxus_core::prelude::ScopeState) -> Self::Builder { + fn builder(_cx: &::dioxus::prelude::ScopeState) -> Self::Builder { #name::builder() } unsafe fn memoize(&self, other: &Self) -> bool { diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index fa8c7e4bd..0c282a930 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -91,7 +91,7 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> { /// }) /// } /// ``` -impl<'a> Properties for FragmentProps<'a> { +impl<'a> Properties<'_> for FragmentProps<'a> { type Builder = FragmentBuilder<'a, false>; const IS_STATIC: bool = false; fn builder(_cx: &ScopeState) -> Self::Builder { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index e34fb7d42..2d3f7fcb0 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -896,8 +896,8 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option { pub trait HasAttributesBox<'a, T> { fn push_attribute( self, - name: &str, - ns: Option<&str>, + name: &'a str, + ns: Option<&'static str>, attr: impl IntoAttributeValue<'a>, volatile: bool, ) -> T; diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index d3f9f6989..d05fbf932 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -32,7 +32,7 @@ use crate::innerlude::*; /// data: &'a str /// } /// ``` -pub trait Properties: Sized { +pub trait Properties<'a>: Sized { /// The type of the builder for this component. /// Used to create "in-progress" versions of the props. type Builder; @@ -41,7 +41,7 @@ pub trait Properties: Sized { const IS_STATIC: bool; /// Create a builder for this component. - fn builder(cx: &ScopeState) -> Self::Builder; + fn builder(cx: &'a ScopeState) -> Self::Builder; /// Memoization can only happen if the props are valid for the 'static lifetime /// @@ -51,7 +51,7 @@ pub trait Properties: Sized { unsafe fn memoize(&self, other: &Self) -> bool; } -impl Properties for () { +impl Properties<'_> for () { type Builder = EmptyBuilder; const IS_STATIC: bool = true; fn builder(_cx: &ScopeState) -> Self::Builder { @@ -70,8 +70,8 @@ impl EmptyBuilder { /// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern /// to initialize a component's props. -pub fn fc_to_builder<'a, T: Properties + 'a>( - cx: &ScopeState, +pub fn fc_to_builder<'a, T: Properties<'a> + 'a>( + cx: &'a ScopeState, _: fn(Scope<'a, T>) -> Element<'a>, ) -> T::Builder { T::builder(cx) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 5d279f86a..62d640037 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -423,7 +423,7 @@ impl<'src> ScopeState { fn_name: &'static str, ) -> DynamicNode<'src> where - P: Properties + 'child, + P: Properties<'child> + 'child, 'src: 'child, { let vcomp = VProps::new(component, P::memoize, props); diff --git a/packages/dioxus/tests/props_spread.rs b/packages/dioxus/tests/props_spread.rs deleted file mode 100644 index 01423ea80..000000000 --- a/packages/dioxus/tests/props_spread.rs +++ /dev/null @@ -1,187 +0,0 @@ -use dioxus::core_macro::render; -use dioxus::prelude::rsx; -use dioxus_core::{Attribute, Element, HasAttributesBox, Scope}; -use dioxus_html::{ExtendedAudioMarker, ExtendedGlobalAttributesMarker}; - -#[test] -fn props_spread() { - pub struct FooProps<'a> { - pub open: Option<&'a str>, - attributes: Vec>, - } - - // ----- - impl<'a> FooProps<'a> { - #[doc = "\nCreate a builder for building `FooProps`.\nOn the builder, call `.open(...)`(optional) to set the values of the fields.\nFinally, call `.build()` to create the instance of `FooProps`.\n "] - #[allow(dead_code)] - pub fn builder(cx: &ScopeState) -> FooPropsBuilder<'a, ((),)> { - FooPropsBuilder { - bump: cx.bump(), - fields: ((),), - attributes: Vec::new(), - _phantom: core::default::Default::default(), - } - } - } - #[must_use] - #[doc(hidden)] - #[allow(dead_code, non_camel_case_types, non_snake_case)] - pub struct FooPropsBuilder<'a, TypedBuilderFields> { - bump: &'a ::dioxus::core::exports::bumpalo::Bump, - fields: TypedBuilderFields, - attributes: Vec>, - _phantom: (core::marker::PhantomData<&'a ()>), - } - //impl<'a, TypedBuilderFields, > Clone for FooPropsBuilder<'a, TypedBuilderFields, > where TypedBuilderFields: Clone { fn clone(&self) -> Self { Self { fields: self.fields.clone(), attributes: self.attributes, _phantom: Default::default() } } } - impl<'a> dioxus::prelude::Properties for FooProps<'a> { - type Builder = FooPropsBuilder<'a, ((),)>; - const IS_STATIC: bool = false; - fn builder(cx: &ScopeState) -> Self::Builder { - FooProps::builder(cx) - } - unsafe fn memoize(&self, other: &Self) -> bool { - false - } - } - #[doc(hidden)] - #[allow(dead_code, non_camel_case_types, non_snake_case)] - pub trait FooPropsBuilder_Optional { - fn into_value T>(self, default: F) -> T; - } - impl FooPropsBuilder_Optional for () { - fn into_value T>(self, default: F) -> T { - default() - } - } - impl FooPropsBuilder_Optional for (T,) { - fn into_value T>(self, _: F) -> T { - self.0 - } - } - #[allow(dead_code, non_camel_case_types, missing_docs)] - impl<'a> FooPropsBuilder<'a, ((),)> { - pub fn open( - self, - open: &'a str, - ) -> FooPropsBuilder< - 'a, - (( - Option<&'a str>, - // pub attributes: Vec>, - ),), - > { - let open = (Some(open),); - let (_,) = self.fields; - FooPropsBuilder { - bump: self.bump, - fields: (open,), - attributes: self.attributes, - _phantom: self._phantom, - } - } - } - #[doc(hidden)] - #[allow(dead_code, non_camel_case_types, non_snake_case)] - pub enum FooPropsBuilder_Error_Repeated_field_open {} - #[doc(hidden)] - #[allow(dead_code, non_camel_case_types, missing_docs)] - impl<'a> - FooPropsBuilder< - 'a, - (( - Option<&'a str>, - // pub attributes: Vec>, - ),), - > - { - #[deprecated(note = "Repeated field open")] - pub fn open( - self, - _: FooPropsBuilder_Error_Repeated_field_open, - ) -> FooPropsBuilder< - 'a, - (( - Option<&'a str>, - // pub attributes: Vec>, - ),), - > { - self - } - } - #[allow(dead_code, non_camel_case_types, missing_docs)] - impl<'a, __open: FooPropsBuilder_Optional>> FooPropsBuilder<'a, (__open,)> { - pub fn build(self) -> FooProps<'a> { - let (open,) = self.fields; - let open = FooPropsBuilder_Optional::into_value(open, || Default::default()); - FooProps { - open, - attributes: self.attributes, - } - } - } - // ----- - - impl<'a, A> HasAttributesBox<'a, FooPropsBuilder<'a, (A,)>> for FooPropsBuilder<'a, (A,)> { - fn push_attribute( - self, - name: &str, - ns: Option<&str>, - attr: impl IntoAttributeValue<'a>, - volatile: bool, - ) -> Self { - let mut attrs = Vec::from(self.attributes); - attrs.push(Attribute::new( - name, - attr.into_value(self.bump), - ns, - volatile, - )); - FooPropsBuilder { - bump: self.bump, - fields: self.fields, - attributes: attrs, - _phantom: self._phantom, - } - } - } - impl ExtendedGlobalAttributesMarker for FooPropsBuilder<'_, (A,)> {} - impl ExtendedAudioMarker for FooPropsBuilder<'_, (A,)> {} - - use dioxus::prelude::*; - use dioxus_html::AudioExtension; - - #[allow(non_snake_case)] - pub fn Foo<'a>(cx: Scope<'a, FooProps<'a>>) -> Element<'a> { - let muted = false; - let attributes = &cx.props.attributes; - render! { - // rsx! { - // audio { - // muted: muted, - // } - // } - ::dioxus::core::LazyNodes::new(move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { - static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] }; - let mut attrs = vec![__cx.attr(dioxus_elements::audio::muted.0, muted, dioxus_elements::audio::muted.1, dioxus_elements::audio::muted.2)]; - for attr in attributes { - attrs.push(attr); - } - ::dioxus::core::VNode { - parent: None, - key: None, - template: std::cell::Cell::new(TEMPLATE), - root_ids: Default::default(), - dynamic_nodes: __cx.bump().alloc([]), - dynamic_attrs: __cx.bump().alloc(attrs), - } - }) - } - } - - rsx! { - Foo { - autoplay: true, - controls: true, - } - }; -} From 97035a44820e15c9d4ec3a2dac834387ddd00071 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 20 Sep 2023 16:36:43 -0500 Subject: [PATCH 014/126] simplify spread example --- examples/spread.rs | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/examples/spread.rs b/examples/spread.rs index eb09e5dd9..a59fdf8f9 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -3,6 +3,7 @@ //! This example shows how we can render the Dioxus Virtualdom using SSR. use dioxus::core::{Attribute, HasAttributesBox}; +use dioxus::html::GlobalAttributesExtension; use dioxus::html::{AudioExtension, ExtendedAudioMarker, ExtendedGlobalAttributesMarker}; use dioxus::prelude::*; @@ -11,31 +12,14 @@ fn main() { let mut vdom = VirtualDom::new(app); let _ = vdom.rebuild(); println!("{}", dioxus_ssr::render(&vdom)); - - // Or we can render rsx! calls themselves - println!( - "{}", - dioxus_ssr::render_lazy(rsx! { - div { - h1 { "Hello, world!" } - } - }) - ); - - // We can configure the SSR rendering to add ids for rehydration - println!("{}", dioxus_ssr::pre_render(&vdom)); - - // We can render to a buf directly too - let mut file = String::new(); - let mut renderer = dioxus_ssr::Renderer::default(); - renderer.render_to(&mut file, &vdom).unwrap(); - println!("{file}"); } fn app(cx: Scope) -> Element { cx.render(rsx!(Foo { autoplay: true, controls: true, + src: "https://www.w3schools.com/tags/horse.ogg", + width: "100", })) } @@ -64,7 +48,7 @@ pub struct FooPropsBuilder<'a, TypedBuilderFields> { bump: &'a ::dioxus::core::exports::bumpalo::Bump, fields: TypedBuilderFields, attributes: Vec>, - _phantom: (core::marker::PhantomData<&'a ()>), + _phantom: core::marker::PhantomData<&'a ()>, } //impl<'a, TypedBuilderFields, > Clone for FooPropsBuilder<'a, TypedBuilderFields, > where TypedBuilderFields: Clone { fn clone(&self) -> Self { Self { fields: self.fields.clone(), attributes: self.attributes, _phantom: Default::default() } } } impl<'a> dioxus::prelude::Properties<'a> for FooProps<'a> { @@ -73,7 +57,7 @@ impl<'a> dioxus::prelude::Properties<'a> for FooProps<'a> { fn builder(cx: &'a ScopeState) -> Self::Builder { FooProps::builder(cx) } - unsafe fn memoize(&self, other: &Self) -> bool { + unsafe fn memoize(&self, _other: &Self) -> bool { false } } @@ -184,23 +168,21 @@ impl ExtendedAudioMarker for FooPropsBuilder<'_, (A,)> {} #[allow(non_snake_case)] pub fn Foo<'a>(cx: Scope<'a, FooProps<'a>>) -> Element<'a> { - let muted = false; let attributes = &cx.props.attributes; render! { // rsx! { // audio { - // muted: muted, + // ...attributes, // } // } ::dioxus::core::LazyNodes::new(move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "123" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] }; - let mut attrs = vec![__cx.attr(dioxus_elements::audio::muted.0, muted, dioxus_elements::audio::muted.1, dioxus_elements::audio::muted.2)]; - attrs.push((&**attributes).into()); + let attrs = vec![(&**attributes).into()]; ::dioxus::core::VNode { parent: None, key: None, template: std::cell::Cell::new(TEMPLATE), - root_ids: Default::default(), + root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(__cx.bump()).into(), dynamic_nodes: __cx.bump().alloc([]), dynamic_attrs: __cx.bump().alloc(attrs), } From 0e0819d2846a845d56fbbbf48a786dbae63a3be6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 22 Sep 2023 09:19:54 -0500 Subject: [PATCH 015/126] further simplify spread example --- examples/spread.rs | 223 +++++++++++---------------------------------- 1 file changed, 54 insertions(+), 169 deletions(-) diff --git a/examples/spread.rs b/examples/spread.rs index a59fdf8f9..76b324f28 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -1,191 +1,76 @@ -//! Example: SSR +//! Example: README.md showcase //! -//! This example shows how we can render the Dioxus Virtualdom using SSR. +//! The example from the README.md. -use dioxus::core::{Attribute, HasAttributesBox}; -use dioxus::html::GlobalAttributesExtension; -use dioxus::html::{AudioExtension, ExtendedAudioMarker, ExtendedGlobalAttributesMarker}; -use dioxus::prelude::*; +use dioxus::{ + core::{exports::bumpalo::Bump, Attribute, HasAttributesBox}, + html::{ExtendedGlobalAttributesMarker, GlobalAttributesExtension}, + prelude::*, +}; fn main() { - // We can render VirtualDoms - let mut vdom = VirtualDom::new(app); - let _ = vdom.rebuild(); - println!("{}", dioxus_ssr::render(&vdom)); + dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - cx.render(rsx!(Foo { - autoplay: true, - controls: true, - src: "https://www.w3schools.com/tags/horse.ogg", - width: "100", - })) + cx.render(::dioxus::core::LazyNodes::new( + move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { + static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { + name: "src/main.rs:15:15:289", + roots: &[::dioxus::core::TemplateNode::Dynamic { id: 0usize }], + node_paths: &[&[0u8]], + attr_paths: &[], + }; + ::dioxus::core::VNode { + parent: None, + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in( + 1usize, + __cx.bump(), + ) + .into(), + dynamic_nodes: __cx.bump().alloc([__cx.component( + Component, + Props { + bump: __cx.bump(), + attributes: Vec::new(), + } + .width("hello"), + "Component", + )]), + dynamic_attrs: __cx.bump().alloc([]), + } + }, + )) } -pub struct FooProps<'a> { - pub open: Option<&'a str>, +#[derive(Props)] +struct Props<'a> { + bump: &'a Bump, attributes: Vec>, } -// ----- -impl<'a> FooProps<'a> { - #[doc = "\nCreate a builder for building `FooProps`.\nOn the builder, call `.open(...)`(optional) to set the values of the fields.\nFinally, call `.build()` to create the instance of `FooProps`.\n "] - #[allow(dead_code)] - pub fn builder(cx: &'a ScopeState) -> FooPropsBuilder<'a, ((),)> { - FooPropsBuilder { - bump: cx.bump(), - fields: ((),), - attributes: Vec::new(), - _phantom: core::default::Default::default(), - } - } -} -#[must_use] -#[doc(hidden)] -#[allow(dead_code, non_camel_case_types, non_snake_case)] -pub struct FooPropsBuilder<'a, TypedBuilderFields> { - bump: &'a ::dioxus::core::exports::bumpalo::Bump, - fields: TypedBuilderFields, - attributes: Vec>, - _phantom: core::marker::PhantomData<&'a ()>, -} -//impl<'a, TypedBuilderFields, > Clone for FooPropsBuilder<'a, TypedBuilderFields, > where TypedBuilderFields: Clone { fn clone(&self) -> Self { Self { fields: self.fields.clone(), attributes: self.attributes, _phantom: Default::default() } } } -impl<'a> dioxus::prelude::Properties<'a> for FooProps<'a> { - type Builder = FooPropsBuilder<'a, ((),)>; - const IS_STATIC: bool = false; - fn builder(cx: &'a ScopeState) -> Self::Builder { - FooProps::builder(cx) - } - unsafe fn memoize(&self, _other: &Self) -> bool { - false - } -} -#[doc(hidden)] -#[allow(dead_code, non_camel_case_types, non_snake_case)] -pub trait FooPropsBuilder_Optional { - fn into_value T>(self, default: F) -> T; -} -impl FooPropsBuilder_Optional for () { - fn into_value T>(self, default: F) -> T { - default() - } -} -impl FooPropsBuilder_Optional for (T,) { - fn into_value T>(self, _: F) -> T { - self.0 - } -} -#[allow(dead_code, non_camel_case_types, missing_docs)] -impl<'a> FooPropsBuilder<'a, ((),)> { - pub fn open( - self, - open: &'a str, - ) -> FooPropsBuilder< - 'a, - (( - Option<&'a str>, - // pub attributes: Vec>, - ),), - > { - let open = (Some(open),); - let (_,) = self.fields; - FooPropsBuilder { - bump: self.bump, - fields: (open,), - attributes: self.attributes, - _phantom: self._phantom, - } - } -} -#[doc(hidden)] -#[allow(dead_code, non_camel_case_types, non_snake_case)] -pub enum FooPropsBuilder_Error_Repeated_field_open {} -#[doc(hidden)] -#[allow(dead_code, non_camel_case_types, missing_docs)] -impl<'a> - FooPropsBuilder< - 'a, - (( - Option<&'a str>, - // pub attributes: Vec>, - ),), - > -{ - #[deprecated(note = "Repeated field open")] - pub fn open( - self, - _: FooPropsBuilder_Error_Repeated_field_open, - ) -> FooPropsBuilder< - 'a, - (( - Option<&'a str>, - // pub attributes: Vec>, - ),), - > { - self - } -} -#[allow(dead_code, non_camel_case_types, missing_docs)] -impl<'a, __open: FooPropsBuilder_Optional>> FooPropsBuilder<'a, (__open,)> { - pub fn build(self) -> FooProps<'a> { - let (open,) = self.fields; - let open = FooPropsBuilder_Optional::into_value(open, || Default::default()); - FooProps { - open, - attributes: self.attributes, - } - } -} -// ----- - -impl<'a, A> HasAttributesBox<'a, FooPropsBuilder<'a, (A,)>> for FooPropsBuilder<'a, (A,)> { +impl<'a> HasAttributesBox<'a, Props<'a>> for Props<'a> { fn push_attribute( - self, + mut self, name: &'a str, ns: Option<&'static str>, attr: impl IntoAttributeValue<'a>, volatile: bool, ) -> Self { - let mut attrs = self.attributes; - // We insert attributes so that the list is binary-searchable - if let Err(index) = attrs.binary_search_by(|probe| probe.name.cmp(name)) { - attrs.insert( - index, - Attribute::new(name, attr.into_value(self.bump), ns, volatile), - ); - } - FooPropsBuilder { - bump: self.bump, - fields: self.fields, - attributes: attrs, - _phantom: self._phantom, - } + self.attributes.push(Attribute { + name, + namespace: ns, + value: attr.into_value(self.bump), + volatile, + }); + self } } -impl ExtendedGlobalAttributesMarker for FooPropsBuilder<'_, (A,)> {} -impl ExtendedAudioMarker for FooPropsBuilder<'_, (A,)> {} -#[allow(non_snake_case)] -pub fn Foo<'a>(cx: Scope<'a, FooProps<'a>>) -> Element<'a> { - let attributes = &cx.props.attributes; - render! { - // rsx! { - // audio { - // ...attributes, - // } - // } - ::dioxus::core::LazyNodes::new(move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { - static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "123" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] }; - let attrs = vec![(&**attributes).into()]; - ::dioxus::core::VNode { - parent: None, - key: None, - template: std::cell::Cell::new(TEMPLATE), - root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(__cx.bump()).into(), - dynamic_nodes: __cx.bump().alloc([]), - dynamic_attrs: __cx.bump().alloc(attrs), - } - }) - } +impl ExtendedGlobalAttributesMarker for Props<'_> {} + +fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { + todo!() } From 5ed116b060c317e4ed657e2f592fb927b514a54c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 22 Sep 2023 09:24:00 -0500 Subject: [PATCH 016/126] fix bounds on generated element extensions --- examples/spread.rs | 30 ++++++++++++++++++++++--- packages/html-internal-macro/src/lib.rs | 4 ++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/examples/spread.rs b/examples/spread.rs index 76b324f28..0458ef08d 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -9,7 +9,11 @@ use dioxus::{ }; fn main() { - dioxus_desktop::launch(app); + let mut dom = VirtualDom::new(app); + dom.rebuild(); + let html = dioxus_ssr::render(&dom); + + println!("{}", html); } fn app(cx: Scope) -> Element { @@ -36,7 +40,8 @@ fn app(cx: Scope) -> Element { bump: __cx.bump(), attributes: Vec::new(), } - .width("hello"), + .width(10) + .height("100px"), "Component", )]), dynamic_attrs: __cx.bump().alloc([]), @@ -72,5 +77,24 @@ impl<'a> HasAttributesBox<'a, Props<'a>> for Props<'a> { impl ExtendedGlobalAttributesMarker for Props<'_> {} fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { - todo!() + let attributes = &cx.props.attributes; + render! { + // rsx! { + // audio { + // ...attributes, + // } + // } + ::dioxus::core::LazyNodes::new(move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { + static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "123" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] }; + let attrs = vec![(&**attributes).into()]; + ::dioxus::core::VNode { + parent: None, + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(__cx.bump()).into(), + dynamic_nodes: __cx.bump().alloc([]), + dynamic_attrs: __cx.bump().alloc(attrs), + } + }) + } } diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs index 1a1d22d08..4b7d23ad5 100644 --- a/packages/html-internal-macro/src/lib.rs +++ b/packages/html-internal-macro/src/lib.rs @@ -56,7 +56,7 @@ impl ToTokens for ImplExtensionAttributes { let defs = self.attrs.iter().map(|ident| { quote! { - fn #ident(self, value: impl IntoAttributeValue<'a> + 'static) -> Self; + fn #ident(self, value: impl IntoAttributeValue<'a>) -> Self; } }); let impls = self.attrs.iter().map(|ident| { @@ -66,7 +66,7 @@ impl ToTokens for ImplExtensionAttributes { quote! { <#impl_name as #name>::#ident } }; quote! { - fn #ident(self, value: impl IntoAttributeValue<'a> + 'static) -> Self { + fn #ident(self, value: impl IntoAttributeValue<'a>) -> Self { let d = #d; self.push_attribute(d.0, d.1, value, d.2) } From 31cd15d61fefdaff1765a973cc8abe303ec53741 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 22 Sep 2023 09:31:13 -0500 Subject: [PATCH 017/126] fix clippy --- examples/spread.rs | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/examples/spread.rs b/examples/spread.rs index 0458ef08d..bab1c426f 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -1,7 +1,3 @@ -//! Example: README.md showcase -//! -//! The example from the README.md. - use dioxus::{ core::{exports::bumpalo::Bump, Attribute, HasAttributesBox}, html::{ExtendedGlobalAttributesMarker, GlobalAttributesExtension}, @@ -10,7 +6,7 @@ use dioxus::{ fn main() { let mut dom = VirtualDom::new(app); - dom.rebuild(); + let _ = dom.rebuild(); let html = dioxus_ssr::render(&dom); println!("{}", html); @@ -79,22 +75,8 @@ impl ExtendedGlobalAttributesMarker for Props<'_> {} fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { let attributes = &cx.props.attributes; render! { - // rsx! { - // audio { - // ...attributes, - // } - // } - ::dioxus::core::LazyNodes::new(move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { - static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "123" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] }; - let attrs = vec![(&**attributes).into()]; - ::dioxus::core::VNode { - parent: None, - key: None, - template: std::cell::Cell::new(TEMPLATE), - root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(__cx.bump()).into(), - dynamic_nodes: __cx.bump().alloc([]), - dynamic_attrs: __cx.bump().alloc(attrs), - } - }) + audio { + ..attributes, + } } } From 5b65c4cfb4e4b19df113f799437371a7f1327ad5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 22 Sep 2023 10:04:34 -0500 Subject: [PATCH 018/126] implement spreading props in the rsx macro --- examples/spread.rs | 2 +- packages/autofmt/src/element.rs | 21 ++++-- packages/autofmt/src/writer.rs | 30 +++++---- packages/rsx-rosetta/src/lib.rs | 16 ++--- packages/rsx/src/attribute.rs | 33 +++++++++- packages/rsx/src/element.rs | 96 ++++++++++++++++++--------- packages/rsx/src/lib.rs | 112 ++++++++++++++++++-------------- 7 files changed, 208 insertions(+), 102 deletions(-) diff --git a/examples/spread.rs b/examples/spread.rs index bab1c426f..75c026aff 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -73,7 +73,7 @@ impl<'a> HasAttributesBox<'a, Props<'a>> for Props<'a> { impl ExtendedGlobalAttributesMarker for Props<'_> {} fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { - let attributes = &cx.props.attributes; + let attributes = &*cx.props.attributes; render! { audio { ..attributes, diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 4f47331c1..ba4f51451 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -49,7 +49,6 @@ impl Writer<'_> { attributes, children, brace, - extra_attributes, .. } = el; @@ -167,7 +166,7 @@ impl Writer<'_> { fn write_attributes( &mut self, - attributes: &[ElementAttrNamed], + attributes: &[AttributeType], key: &Option, sameline: bool, ) -> Result { @@ -189,7 +188,7 @@ impl Writer<'_> { while let Some(attr) = attr_iter.next() { self.out.indent += 1; if !sameline { - self.write_comments(attr.attr.start())?; + self.write_comments(attr.start())?; } self.out.indent -= 1; @@ -290,7 +289,14 @@ impl Writer<'_> { Ok(()) } - fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result { + fn write_attribute(&mut self, attr: &AttributeType) -> Result { + match attr { + AttributeType::Named(attr) => self.write_named_attribute(attr), + AttributeType::Spread(attr) => self.write_spread_attribute(attr), + } + } + + fn write_named_attribute(&mut self, attr: &ElementAttrNamed) -> Result { self.write_attribute_name(&attr.attr.name)?; write!(self.out, ": ")?; self.write_attribute_value(&attr.attr.value)?; @@ -298,6 +304,13 @@ impl Writer<'_> { Ok(()) } + fn write_spread_attribute(&mut self, attr: &Expr) -> Result { + write!(self.out, "..")?; + write!(self.out, "{}", prettyplease::unparse_expr(attr))?; + + Ok(()) + } + // make sure the comments are actually relevant to this element. // test by making sure this element is the primary element on this line pub fn current_span_is_primary(&self, location: Span) -> bool { diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index 788007319..165267714 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -1,4 +1,4 @@ -use dioxus_rsx::{BodyNode, ElementAttrNamed, ElementAttrValue, ForLoop}; +use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop}; use proc_macro2::{LineColumn, Span}; use quote::ToTokens; use std::{ @@ -165,12 +165,12 @@ impl<'a> Writer<'a> { } } - pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize { + pub(crate) fn is_short_attrs(&mut self, attributes: &[AttributeType]) -> usize { let mut total = 0; for attr in attributes { - if self.current_span_is_primary(attr.attr.start()) { - 'line: for line in self.src[..attr.attr.start().start().line - 1].iter().rev() { + if self.current_span_is_primary(attr.start()) { + 'line: for line in self.src[..attr.start().start().line - 1].iter().rev() { match (line.trim().starts_with("//"), line.is_empty()) { (true, _) => return 100000, (_, true) => continue 'line, @@ -179,16 +179,24 @@ impl<'a> Writer<'a> { } } - total += match &attr.attr.name { - dioxus_rsx::ElementAttrName::BuiltIn(name) => { - let name = name.to_string(); - name.len() + match attr { + AttributeType::Named(attr) => { + let name_len = match &attr.attr.name { + dioxus_rsx::ElementAttrName::BuiltIn(name) => { + let name = name.to_string(); + name.len() + } + dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2, + }; + total += name_len; + total += self.attr_value_len(&attr.attr.value); + } + AttributeType::Spread(expr) => { + let expr_len = self.retrieve_formatted_expr(expr).len(); + total += expr_len + 3; } - dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2, }; - total += self.attr_value_len(&attr.attr.value); - total += 6; } diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 22c1c52a9..2497e4411 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -1,6 +1,7 @@ use convert_case::{Case, Casing}; use dioxus_rsx::{ - BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput, + AttributeType, BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, + ElementName, IfmtInput, }; pub use html_parser::{Dom, Node}; use proc_macro2::{Ident, Span}; @@ -34,7 +35,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { Ident::new(new_name.as_str(), Span::call_site()) }; - ElementAttrNamed { + AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text( @@ -42,13 +43,13 @@ pub fn rsx_node_from_html(node: &Node) -> Option { )), name: dioxus_rsx::ElementAttrName::BuiltIn(ident), }, - } + }) }) .collect(); let class = el.classes.join(" "); if !class.is_empty() { - attributes.push(ElementAttrNamed { + attributes.push(AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new( @@ -57,11 +58,11 @@ pub fn rsx_node_from_html(node: &Node) -> Option { )), value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)), }, - }); + })); } if let Some(id) = &el.id { - attributes.push(ElementAttrNamed { + attributes.push(AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new( @@ -70,7 +71,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { )), value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)), }, - }); + })); } let children = el.children.iter().filter_map(rsx_node_from_html).collect(); @@ -82,7 +83,6 @@ pub fn rsx_node_from_html(node: &Node) -> Option { merged_attributes: Default::default(), key: None, brace: Default::default(), - extra_attributes: None, })) } diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index 38cb7c853..6f5d0a096 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -4,7 +4,38 @@ use super::*; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{parse_quote, Expr, ExprIf, Ident, LitStr}; +use syn::{parse_quote, spanned::Spanned, Expr, ExprIf, Ident, LitStr}; + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum AttributeType { + Named(ElementAttrNamed), + Spread(Expr), +} + +impl AttributeType { + pub fn start(&self) -> Span { + match self { + AttributeType::Named(n) => n.attr.start(), + AttributeType::Spread(e) => e.span(), + } + } + + pub(crate) fn try_combine(&self, other: &Self) -> Option { + match (self, other) { + (Self::Named(a), Self::Named(b)) => a.try_combine(b).map(Self::Named), + _ => None, + } + } +} + +impl ToTokens for AttributeType { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + AttributeType::Named(n) => tokens.append_all(quote! { #n }), + AttributeType::Spread(e) => tokens.append_all(quote! { #e.into() }), + } + } +} #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub struct ElementAttrNamed { diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 66ee422c2..e62fc6d6e 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -8,7 +8,7 @@ use syn::{ parse::{Parse, ParseBuffer, ParseStream}, punctuated::Punctuated, spanned::Spanned, - Ident, LitStr, Result, Token, + Expr, Ident, LitStr, Result, Token, }; // ======================================= @@ -18,11 +18,10 @@ use syn::{ pub struct Element { pub name: ElementName, pub key: Option, - pub attributes: Vec, - pub merged_attributes: Vec, + pub attributes: Vec, + pub merged_attributes: Vec, pub children: Vec, pub brace: syn::token::Brace, - pub extra_attributes: Option, } impl Parse for Element { @@ -33,7 +32,7 @@ impl Parse for Element { let content: ParseBuffer; let brace = syn::braced!(content in stream); - let mut attributes: Vec = vec![]; + let mut attributes: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; @@ -43,9 +42,20 @@ impl Parse for Element { // "def": 456, // abc: 123, loop { - if content.peek(Token![...]) { - content.parse::()?; - extra_attributes = Some(content.parse::()?); + if content.peek(Token![..]) { + content.parse::()?; + let expr = content.parse::()?; + let span = expr.span(); + attributes.push(attribute::AttributeType::Spread(expr)); + + if content.is_empty() { + break; + } + + if content.parse::().is_err() { + missing_trailing_comma!(span); + } + continue; } // Parse the raw literal fields @@ -56,13 +66,13 @@ impl Parse for Element { content.parse::()?; let value = content.parse::()?; - attributes.push(ElementAttrNamed { + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { name: ElementAttrName::Custom(name), value, }, - }); + })); if content.is_empty() { break; @@ -85,13 +95,13 @@ impl Parse for Element { let span = content.span(); if name_str.starts_with("on") { - attributes.push(ElementAttrNamed { + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { name: ElementAttrName::BuiltIn(name), value: ElementAttrValue::EventTokens(content.parse()?), }, - }); + })); } else { match name_str.as_str() { "key" => { @@ -99,13 +109,13 @@ impl Parse for Element { } _ => { let value = content.parse::()?; - attributes.push(ElementAttrNamed { + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr { name: ElementAttrName::BuiltIn(name), value, }, - }); + })); } } } @@ -114,7 +124,6 @@ impl Parse for Element { break; } - // todo: add a message saying you need to include commas between fields if content.parse::().is_err() { missing_trailing_comma!(span); } @@ -126,12 +135,26 @@ impl Parse for Element { // Deduplicate any attributes that can be combined // For example, if there are two `class` attributes, combine them into one - let mut merged_attributes: Vec = Vec::new(); + let mut merged_attributes: Vec = Vec::new(); for attr in &attributes { - if let Some(old_attr_index) = merged_attributes - .iter() - .position(|a| a.attr.name == attr.attr.name) - { + if let Some(old_attr_index) = merged_attributes.iter().position(|a| { + matches!((a, attr), ( + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + name: ElementAttrName::BuiltIn(old_name), + .. + }, + .. + }), + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + name: ElementAttrName::BuiltIn(new_name), + .. + }, + .. + }), + ) if old_name == new_name) + }) { let old_attr = &mut merged_attributes[old_attr_index]; if let Some(combined) = old_attr.try_combine(attr) { *old_attr = combined; @@ -165,7 +188,6 @@ impl Parse for Element { merged_attributes, children, brace, - extra_attributes, }) } } @@ -180,15 +202,31 @@ impl ToTokens for Element { None => quote! { None }, }; - let listeners = self - .merged_attributes - .iter() - .filter(|f| matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); + let listeners = self.merged_attributes.iter().filter(|f| { + matches!( + f, + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + value: ElementAttrValue::EventTokens { .. }, + .. + }, + .. + }) + ) + }); - let attr = self - .merged_attributes - .iter() - .filter(|f| !matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); + let attr = self.merged_attributes.iter().filter(|f| { + !matches!( + f, + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + value: ElementAttrValue::EventTokens { .. }, + .. + }, + .. + }) + ) + }); tokens.append_all(quote! { __cx.element( diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 356379d09..24bb8b533 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -261,7 +261,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { #[cfg(feature = "hot_reload")] #[derive(Default, Debug)] struct DynamicMapping { - attribute_to_idx: std::collections::HashMap>, + attribute_to_idx: std::collections::HashMap>, last_attribute_idx: usize, node_to_idx: std::collections::HashMap>, last_element_idx: usize, @@ -277,7 +277,7 @@ impl DynamicMapping { new } - fn get_attribute_idx(&mut self, attr: &ElementAttr) -> Option { + fn get_attribute_idx(&mut self, attr: &AttributeType) -> Option { self.attribute_to_idx .get_mut(attr) .and_then(|idxs| idxs.pop()) @@ -287,7 +287,7 @@ impl DynamicMapping { self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop()) } - fn insert_attribute(&mut self, attr: ElementAttr) -> usize { + fn insert_attribute(&mut self, attr: AttributeType) -> usize { let idx = self.last_attribute_idx; self.last_attribute_idx += 1; @@ -309,10 +309,17 @@ impl DynamicMapping { match node { BodyNode::Element(el) => { for attr in el.merged_attributes { - match &attr.attr.value { - ElementAttrValue::AttrLiteral(input) if input.is_static() => {} + match &attr { + AttributeType::Named(ElementAttrNamed { + attr: + ElementAttr { + value: ElementAttrValue::AttrLiteral(input), + .. + }, + .. + }) if input.is_static() => {} _ => { - self.insert_attribute(attr.attr); + self.insert_attribute(attr); } } } @@ -340,7 +347,7 @@ impl DynamicMapping { #[derive(Default, Debug)] pub struct DynamicContext<'a> { dynamic_nodes: Vec<&'a BodyNode>, - dynamic_attributes: Vec<&'a ElementAttrNamed>, + dynamic_attributes: Vec<&'a AttributeType>, current_path: Vec, node_paths: Vec>, @@ -360,10 +367,16 @@ impl<'a> DynamicContext<'a> { let mut static_attrs = Vec::new(); for attr in &el.merged_attributes { - match &attr.attr.value { - ElementAttrValue::AttrLiteral(value) if value.is_static() => { + match &attr { + AttributeType::Named(ElementAttrNamed { + attr: + ElementAttr { + value: ElementAttrValue::AttrLiteral(value), + name, + }, + .. + }) if value.is_static() => { let value = value.source.as_ref().unwrap(); - let name = &attr.attr.name; let attribute_name_rust = name.to_string(); let (name, namespace) = Ctx::map_attribute(&element_name_rust, &attribute_name_rust) @@ -377,7 +390,7 @@ impl<'a> DynamicContext<'a> { _ => { let idx = match mapping { - Some(mapping) => mapping.get_attribute_idx(&attr.attr)?, + Some(mapping) => mapping.get_attribute_idx(&attr)?, None => self.dynamic_attributes.len(), }; self.dynamic_attributes.push(attr); @@ -447,47 +460,50 @@ impl<'a> DynamicContext<'a> { ElementName::Ident(i) => quote! { dioxus_elements::#i::#name }, ElementName::Custom(_) => quote! { None }, }; - let static_attrs = el - .merged_attributes - .iter() - .map(|attr| match &attr.attr.value { - ElementAttrValue::AttrLiteral(value) if value.is_static() => { - let value = value.to_static().unwrap(); - let ns = { - match &attr.attr.name { - ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)), - ElementAttrName::Custom(_) => quote!(None), - } - }; - let name = &attr.attr.name; - let name = match (el_name, name) { - (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => { - quote! { #el_name::#name.0 } - } - _ => { - let as_string = name.to_string(); - quote! { #as_string } - } - }; - quote! { - ::dioxus::core::TemplateAttribute::Static { - name: #name, - namespace: #ns, - value: #value, + let static_attrs = el.merged_attributes.iter().map(|attr| match attr { + AttributeType::Named(ElementAttrNamed { + attr: + ElementAttr { + value: ElementAttrValue::AttrLiteral(value), + name, + }, + .. + }) if value.is_static() => { + let value = value.to_static().unwrap(); + let ns = { + match name { + ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)), + ElementAttrName::Custom(_) => quote!(None), + } + }; + let name = match (el_name, name) { + (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => { + quote! { #el_name::#name.0 } + } + _ => { + let as_string = name.to_string(); + quote! { #as_string } + } + }; + quote! { + ::dioxus::core::TemplateAttribute::Static { + name: #name, + namespace: #ns, + value: #value, - // todo: we don't diff these so we never apply the volatile flag - // volatile: dioxus_elements::#el_name::#name.2, - } + // todo: we don't diff these so we never apply the volatile flag + // volatile: dioxus_elements::#el_name::#name.2, } } + } - _ => { - let ct = self.dynamic_attributes.len(); - self.dynamic_attributes.push(attr); - self.attr_paths.push(self.current_path.clone()); - quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } } - } - }); + _ => { + let ct = self.dynamic_attributes.len(); + self.dynamic_attributes.push(attr); + self.attr_paths.push(self.current_path.clone()); + quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } } + } + }); let attrs = quote! { #(#static_attrs),*}; From 56b1a0aefc5e35a33e5ae0c3d74df311c1d34be4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 25 Sep 2023 15:42:37 -0500 Subject: [PATCH 019/126] add bump to props --- packages/core-macro/src/props/mod.rs | 103 +++++++++++++++++++++------ packages/core-macro/tests/rsx.rs | 2 + 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 67e64b0f0..f5ad99ce8 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -540,15 +540,35 @@ mod struct_info { // Therefore, we will generate code that shortcircuits the "comparison" in memoization let are_there_generics = !self.generics.params.is_empty(); - let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); + let generics = self.generics.clone(); + let (_, ty_generics, where_clause) = generics.split_for_impl(); + let impl_generics = self.modify_generics(|g| { + // Add a bump lifetime to the generics + g.params.insert( + 0, + syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( + "'__bump", + proc_macro2::Span::call_site(), + ))), + ); + }); + let (impl_generics, b_initial_generics, _) = impl_generics.split_for_impl(); let all_fields_param = syn::GenericParam::Type( syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(), ); let b_generics = self.modify_generics(|g| { + // Add a bump lifetime to the generics + g.params.insert( + 0, + syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( + "'__bump", + proc_macro2::Span::call_site(), + ))), + ); g.params.insert(0, all_fields_param.clone()); }); let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type())); - let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| { + let generics_with_empty = modify_types_generics_hack(&b_initial_generics, |args| { args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into())); }); let phantom_generics = self.generics.params.iter().map(|param| match param { @@ -634,8 +654,9 @@ Finally, call `.build()` to create the instance of `{name}`. impl #impl_generics #name #ty_generics #where_clause { #[doc = #builder_method_doc] #[allow(dead_code)] - #vis fn builder() -> #builder_name #generics_with_empty { + #vis fn builder(_cx: &'__bump ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty { #builder_name { + bump: _cx.bump(), fields: #empties_tuple, _phantom: ::core::default::Default::default(), } @@ -646,6 +667,7 @@ Finally, call `.build()` to create the instance of `{name}`. #builder_type_doc #[allow(dead_code, non_camel_case_types, non_snake_case)] #vis struct #builder_name #b_generics { + bump: &'__bump ::dioxus::core::exports::bumpalo::Bump, fields: #all_fields_param, _phantom: (#( #phantom_generics ),*), } @@ -653,19 +675,20 @@ Finally, call `.build()` to create the instance of `{name}`. impl #b_generics_impl Clone for #builder_name #b_generics_ty #b_generics_where { fn clone(&self) -> Self { Self { + bump: self.bump, fields: self.fields.clone(), _phantom: ::core::default::Default::default(), } } } - impl #impl_generics ::dioxus::prelude::Properties<'_> for #name #ty_generics + impl #impl_generics ::dioxus::prelude::Properties<'__bump> for #name #ty_generics #b_generics_where_extras_predicates { type Builder = #builder_name #generics_with_empty; const IS_STATIC: bool = #is_static; - fn builder(_cx: &::dioxus::prelude::ScopeState) -> Self::Builder { - #name::builder() + fn builder(_cx: &'__bump ::dioxus::prelude::ScopeState) -> Self::Builder { + #name::builder(_cx) } unsafe fn memoize(&self, other: &Self) -> bool { #can_memoize @@ -720,11 +743,13 @@ Finally, call `.build()` to create the instance of `{name}`. ty: field_type, .. } = field; - let mut ty_generics: Vec = self - .generics - .params - .iter() - .map(|generic_param| match generic_param { + // Add the bump lifetime to the generics + let mut ty_generics: Vec = vec![syn::GenericArgument::Lifetime( + syn::Lifetime::new("'__bump", proc_macro2::Span::call_site()), + )]; + + ty_generics.extend(self.generics.params.iter().map( + |generic_param| match generic_param { syn::GenericParam::Type(type_param) => { let ident = type_param.ident.clone(); syn::parse(quote!(#ident).into()).unwrap() @@ -736,11 +761,19 @@ Finally, call `.build()` to create the instance of `{name}`. let ident = const_param.ident.clone(); syn::parse(quote!(#ident).into()).unwrap() } - }) - .collect(); + }, + )); let mut target_generics_tuple = empty_type_tuple(); let mut ty_generics_tuple = empty_type_tuple(); let generics = self.modify_generics(|g| { + // Add a bump lifetime to the generics + g.params.insert( + 0, + syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( + "'__bump", + proc_macro2::Span::call_site(), + ))), + ); let index_after_lifetime_in_generics = g .params .iter() @@ -826,6 +859,7 @@ Finally, call `.build()` to create the instance of `{name}`. let #field_name = (#arg_expr,); let ( #(#descructuring,)* ) = self.fields; #builder_name { + bump: self.bump, fields: ( #(#reconstructing,)* ), _phantom: self._phantom, } @@ -858,11 +892,14 @@ Finally, call `.build()` to create the instance of `{name}`. name: ref field_name, .. } = field; - let mut builder_generics: Vec = self - .generics - .params - .iter() - .map(|generic_param| match generic_param { + // Add a bump lifetime to the generics + let mut builder_generics: Vec = + vec![syn::GenericArgument::Lifetime(syn::Lifetime::new( + "'__bump", + proc_macro2::Span::call_site(), + ))]; + builder_generics.extend(self.generics.params.iter().map(|generic_param| { + match generic_param { syn::GenericParam::Type(type_param) => { let ident = &type_param.ident; syn::parse(quote!(#ident).into()).unwrap() @@ -874,10 +911,18 @@ Finally, call `.build()` to create the instance of `{name}`. let ident = &const_param.ident; syn::parse(quote!(#ident).into()).unwrap() } - }) - .collect(); + } + })); let mut builder_generics_tuple = empty_type_tuple(); let generics = self.modify_generics(|g| { + // Add a bump lifetime to the generics + g.params.insert( + 0, + syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( + "'__bump", + proc_macro2::Span::call_site(), + ))), + ); let index_after_lifetime_in_generics = g .params .iter() @@ -962,6 +1007,15 @@ Finally, call `.build()` to create the instance of `{name}`. } = *self; let generics = self.modify_generics(|g| { + // Add a bump lifetime to the generics + g.params.insert( + 0, + syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( + "'__bump", + proc_macro2::Span::call_site(), + ))), + ); + let index_after_lifetime_in_generics = g .params .iter() @@ -1000,6 +1054,15 @@ Finally, call `.build()` to create the instance of `{name}`. let (_, ty_generics, where_clause) = self.generics.split_for_impl(); let modified_ty_generics = modify_types_generics_hack(&ty_generics, |args| { + // Add a bump lifetime to the generics + args.insert( + 0, + syn::GenericArgument::Lifetime(syn::Lifetime::new( + "'__bump", + proc_macro2::Span::call_site(), + )), + ); + args.insert( 0, syn::GenericArgument::Type( diff --git a/packages/core-macro/tests/rsx.rs b/packages/core-macro/tests/rsx.rs index f4037f928..ff01681fb 100644 --- a/packages/core-macro/tests/rsx.rs +++ b/packages/core-macro/tests/rsx.rs @@ -1,3 +1,5 @@ +use dioxus::prelude::*; + #[test] fn rsx() { let t = trybuild::TestCases::new(); From 7b51bb806018453a5a3931dd8ae92582e63e2342 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 26 Sep 2023 19:23:00 -0500 Subject: [PATCH 020/126] WIP: add extends to the props macro --- examples/spread.rs | 69 +---- packages/core-macro/Cargo.toml | 1 + packages/core-macro/src/props/mod.rs | 338 +++++++++++++++++------- packages/core/src/lib.rs | 7 +- packages/core/src/nodes.rs | 4 +- packages/html-internal-macro/src/lib.rs | 2 +- 6 files changed, 270 insertions(+), 151 deletions(-) diff --git a/examples/spread.rs b/examples/spread.rs index 75c026aff..f3be4d2b7 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -1,3 +1,4 @@ +use crate::dioxus_elements::ExtendedDivMarker; use dioxus::{ core::{exports::bumpalo::Bump, Attribute, HasAttributesBox}, html::{ExtendedGlobalAttributesMarker, GlobalAttributesExtension}, @@ -13,65 +14,15 @@ fn main() { } fn app(cx: Scope) -> Element { - cx.render(::dioxus::core::LazyNodes::new( - move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { - static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { - name: "src/main.rs:15:15:289", - roots: &[::dioxus::core::TemplateNode::Dynamic { id: 0usize }], - node_paths: &[&[0u8]], - attr_paths: &[], - }; - ::dioxus::core::VNode { - parent: None, - key: None, - template: std::cell::Cell::new(TEMPLATE), - root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in( - 1usize, - __cx.bump(), - ) - .into(), - dynamic_nodes: __cx.bump().alloc([__cx.component( - Component, - Props { - bump: __cx.bump(), - attributes: Vec::new(), - } - .width(10) - .height("100px"), - "Component", - )]), - dynamic_attrs: __cx.bump().alloc([]), - } - }, - )) -} - -#[derive(Props)] -struct Props<'a> { - bump: &'a Bump, - attributes: Vec>, -} - -impl<'a> HasAttributesBox<'a, Props<'a>> for Props<'a> { - fn push_attribute( - mut self, - name: &'a str, - ns: Option<&'static str>, - attr: impl IntoAttributeValue<'a>, - volatile: bool, - ) -> Self { - self.attributes.push(Attribute { - name, - namespace: ns, - value: attr.into_value(self.bump), - volatile, - }); - self + render! { + Component { + width: "10px", + height: "10px", + left: 1, + } } } -impl ExtendedGlobalAttributesMarker for Props<'_> {} - fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { let attributes = &*cx.props.attributes; render! { @@ -80,3 +31,9 @@ fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { } } } + +#[derive(Props)] +struct Props<'a> { + #[props(extends = GlobalAttributes)] + attributes: Vec>, +} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index d928d5fec..c30f85289 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -19,6 +19,7 @@ syn = { version = "2.0", features = ["full", "extra-traits"] } dioxus-rsx = { workspace = true } dioxus-core = { workspace = true } constcat = "0.3.0" +convert_case = "^0.6.0" # testing [dev-dependencies] diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index f5ad99ce8..6285643c5 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -24,6 +24,10 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { .included_fields() .map(|f| struct_info.field_impl(f)) .collect::, _>>()?; + let extends = struct_info + .extend_fields() + .map(|f| struct_info.extends_impl(f)) + .collect::, _>>()?; let fields = quote!(#(#fields)*).into_iter(); let required_fields = struct_info .included_fields() @@ -36,6 +40,7 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { #builder_creation #conversion_helper #( #fields )* + #( #extends )* #( #required_fields )* #build_method } @@ -167,8 +172,8 @@ mod field_info { use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; - use syn::Expr; use syn::{parse::Error, punctuated::Punctuated}; + use syn::{Expr, Path}; use super::util::{ expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix, @@ -199,6 +204,13 @@ mod field_info { ); } + // extended field is automatically empty + if !builder_attr.extends.is_empty() { + builder_attr.default = Some( + syn::parse(quote!(::core::default::Default::default()).into()).unwrap(), + ); + } + // auto detect optional let strip_option_auto = builder_attr.strip_option || !builder_attr.ignore_option @@ -257,6 +269,7 @@ mod field_info { pub auto_into: bool, pub strip_option: bool, pub ignore_option: bool, + pub extends: Vec, } impl FieldBuilderAttr { @@ -309,6 +322,17 @@ mod field_info { let name = expr_to_single_string(&assign.left) .ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?; match name.as_str() { + "extends" => { + if let syn::Expr::Path(path) = *assign.right { + self.extends.push(path.path); + Ok(()) + } else { + Err(Error::new_spanned( + assign.right, + "Expected simple identifier", + )) + } + } "default" => { self.default = Some(*assign.right); Ok(()) @@ -363,6 +387,11 @@ mod field_info { Ok(()) } + "extend" => { + self.extends.push(path.path); + Ok(()) + } + _ => { macro_rules! handle_fields { ( $( $flag:expr, $field:ident, $already:expr; )* ) => { @@ -466,11 +495,14 @@ fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&s } mod struct_info { + use convert_case::{Case, Casing}; use proc_macro2::TokenStream; use quote::quote; use syn::parse::Error; use syn::punctuated::Punctuated; - use syn::Expr; + use syn::spanned::Spanned; + use syn::visit::Visit; + use syn::{Expr, Ident}; use super::field_info::{FieldBuilderAttr, FieldInfo}; use super::util::{ @@ -493,7 +525,46 @@ mod struct_info { impl<'a> StructInfo<'a> { pub fn included_fields(&self) -> impl Iterator> { - self.fields.iter().filter(|f| !f.builder_attr.skip) + self.fields + .iter() + .filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty()) + } + + pub fn extend_fields(&self) -> impl Iterator> { + self.fields + .iter() + .filter(|f| !f.builder_attr.extends.is_empty()) + } + + fn extend_lifetime(&self) -> syn::Result> { + let first_extend = self.extend_fields().next(); + + match first_extend { + Some(f) => { + struct VisitFirstLifetime(Option); + + impl Visit<'_> for VisitFirstLifetime { + fn visit_lifetime(&mut self, lifetime: &'_ syn::Lifetime) { + if self.0.is_none() { + self.0 = Some(lifetime.clone()); + } + } + } + + let name = f.name; + let mut visitor = VisitFirstLifetime(None); + + visitor.visit_type(&f.ty); + + visitor.0.ok_or_else(|| { + syn::Error::new_spanned( + name, + "Unable to find lifetime for extended field. Please specify it manually", + ) + }).map(Some) + } + None => Ok(None), + } } pub fn new( @@ -542,29 +613,12 @@ mod struct_info { let generics = self.generics.clone(); let (_, ty_generics, where_clause) = generics.split_for_impl(); - let impl_generics = self.modify_generics(|g| { - // Add a bump lifetime to the generics - g.params.insert( - 0, - syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))), - ); - }); + let impl_generics = self.generics.clone(); let (impl_generics, b_initial_generics, _) = impl_generics.split_for_impl(); let all_fields_param = syn::GenericParam::Type( syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(), ); let b_generics = self.modify_generics(|g| { - // Add a bump lifetime to the generics - g.params.insert( - 0, - syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))), - ); g.params.insert(0, all_fields_param.clone()); }); let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type())); @@ -629,8 +683,7 @@ Finally, call `.build()` to create the instance of `{name}`. quote!(#[doc(hidden)]) }; - let (b_generics_impl, b_generics_ty, b_generics_where_extras_predicates) = - b_generics.split_for_impl(); + let (_, _, b_generics_where_extras_predicates) = b_generics.split_for_impl(); let mut b_generics_where: syn::WhereClause = syn::parse2(quote! { where TypedBuilderFields: Clone })?; @@ -650,12 +703,26 @@ Finally, call `.build()` to create the instance of `{name}`. false => quote! { true }, }; + let extend_fields = self.extend_fields().map(|f| { + let name = f.name; + let ty = f.ty; + quote!(#name: #ty) + }); + let extend_fields_value = self.extend_fields().map(|f| { + let name = f.name; + quote!(#name: Vec::new()) + }); + let extend_lifetime = self + .extend_lifetime()? + .unwrap_or(syn::Lifetime::new("'_", proc_macro2::Span::call_site())); + Ok(quote! { impl #impl_generics #name #ty_generics #where_clause { #[doc = #builder_method_doc] #[allow(dead_code)] - #vis fn builder(_cx: &'__bump ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty { + #vis fn builder(_cx: & #extend_lifetime ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty { #builder_name { + #(#extend_fields_value,)* bump: _cx.bump(), fields: #empties_tuple, _phantom: ::core::default::Default::default(), @@ -667,27 +734,18 @@ Finally, call `.build()` to create the instance of `{name}`. #builder_type_doc #[allow(dead_code, non_camel_case_types, non_snake_case)] #vis struct #builder_name #b_generics { - bump: &'__bump ::dioxus::core::exports::bumpalo::Bump, + #(#extend_fields,)* + bump: & #extend_lifetime ::dioxus::core::exports::bumpalo::Bump, fields: #all_fields_param, _phantom: (#( #phantom_generics ),*), } - impl #b_generics_impl Clone for #builder_name #b_generics_ty #b_generics_where { - fn clone(&self) -> Self { - Self { - bump: self.bump, - fields: self.fields.clone(), - _phantom: ::core::default::Default::default(), - } - } - } - - impl #impl_generics ::dioxus::prelude::Properties<'__bump> for #name #ty_generics + impl #impl_generics ::dioxus::prelude::Properties<#extend_lifetime> for #name #ty_generics #b_generics_where_extras_predicates { type Builder = #builder_name #generics_with_empty; const IS_STATIC: bool = #is_static; - fn builder(_cx: &'__bump ::dioxus::prelude::ScopeState) -> Self::Builder { + fn builder(_cx: &#extend_lifetime ::dioxus::prelude::ScopeState) -> Self::Builder { #name::builder(_cx) } unsafe fn memoize(&self, other: &Self) -> bool { @@ -723,6 +781,137 @@ Finally, call `.build()` to create the instance of `{name}`. }) } + pub fn extends_impl(&self, field: &FieldInfo) -> Result { + let StructInfo { + ref builder_name, .. + } = *self; + + let field_name = field.name; + + let descructuring = self.included_fields().map(|f| { + if f.ordinal == field.ordinal { + quote!(_) + } else { + let name = f.name; + quote!(#name) + } + }); + let reconstructing = self.included_fields().map(|f| f.name); + + // Add the bump lifetime to the generics + let mut ty_generics: Vec = self + .generics + .params + .iter() + .map(|generic_param| match generic_param { + syn::GenericParam::Type(type_param) => { + let ident = type_param.ident.clone(); + syn::parse(quote!(#ident).into()).unwrap() + } + syn::GenericParam::Lifetime(lifetime_def) => { + syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone()) + } + syn::GenericParam::Const(const_param) => { + let ident = const_param.ident.clone(); + syn::parse(quote!(#ident).into()).unwrap() + } + }) + .collect(); + let mut target_generics_tuple = empty_type_tuple(); + let mut ty_generics_tuple = empty_type_tuple(); + let generics = self.modify_generics(|g| { + let index_after_lifetime_in_generics = g + .params + .iter() + .filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_))) + .count(); + for f in self.included_fields() { + if f.ordinal == field.ordinal { + ty_generics_tuple.elems.push_value(empty_type()); + target_generics_tuple + .elems + .push_value(f.tuplized_type_ty_param()); + } else { + g.params + .insert(index_after_lifetime_in_generics, f.generic_ty_param()); + let generic_argument: syn::Type = f.type_ident(); + ty_generics_tuple.elems.push_value(generic_argument.clone()); + target_generics_tuple.elems.push_value(generic_argument); + } + ty_generics_tuple.elems.push_punct(Default::default()); + target_generics_tuple.elems.push_punct(Default::default()); + } + }); + let mut target_generics = ty_generics.clone(); + let index_after_lifetime_in_generics = target_generics + .iter() + .filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_))) + .count(); + target_generics.insert( + index_after_lifetime_in_generics, + syn::GenericArgument::Type(target_generics_tuple.into()), + ); + ty_generics.insert( + index_after_lifetime_in_generics, + syn::GenericArgument::Type(ty_generics_tuple.into()), + ); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + + let forward_extended_fields = self.extend_fields().map(|f| { + let name = f.name; + quote!(#name: self.#name) + }); + + let extends_impl = field.builder_attr.extends.iter().map(|path| { + let name_str = path_to_single_string(path).unwrap(); + let camel_name = name_str.to_case(Case::UpperCamel); + let marker_name = Ident::new( + format!("Extended{}Marker", &camel_name).as_str(), + path.span(), + ); + quote! { + impl #impl_generics #marker_name for #builder_name < #( #ty_generics ),* > #where_clause {} + } + }); + let extend_lifetime = self.extend_lifetime()?.ok_or(Error::new_spanned( + field_name, + "Unable to find lifetime for extended field. Please specify it manually", + ))?; + + Ok(quote! { + impl #impl_generics ::dioxus::prelude::HasAttributesBox<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause { + fn push_attribute( + mut self, + name: &#extend_lifetime str, + ns: Option<&'static str>, + attr: impl ::dioxus::prelude::IntoAttributeValue<#extend_lifetime>, + volatile: bool + ) -> Self { + let ( #(#descructuring,)* ) = self.fields; + self.#field_name.push( + ::dioxus::core::Attribute::new( + name, + { + use ::dioxus::prelude::IntoAttributeValue; + attr.into_value(self.bump) + }, + ns, + volatile, + ) + ); + #builder_name { + #(#forward_extended_fields,)* + bump: self.bump, + fields: ( #(#reconstructing,)* ), + _phantom: self._phantom, + } + } + } + + #(#extends_impl)* + }) + } + pub fn field_impl(&self, field: &FieldInfo) -> Result { let StructInfo { ref builder_name, .. @@ -744,12 +933,11 @@ Finally, call `.build()` to create the instance of `{name}`. .. } = field; // Add the bump lifetime to the generics - let mut ty_generics: Vec = vec![syn::GenericArgument::Lifetime( - syn::Lifetime::new("'__bump", proc_macro2::Span::call_site()), - )]; - - ty_generics.extend(self.generics.params.iter().map( - |generic_param| match generic_param { + let mut ty_generics: Vec = self + .generics + .params + .iter() + .map(|generic_param| match generic_param { syn::GenericParam::Type(type_param) => { let ident = type_param.ident.clone(); syn::parse(quote!(#ident).into()).unwrap() @@ -761,19 +949,11 @@ Finally, call `.build()` to create the instance of `{name}`. let ident = const_param.ident.clone(); syn::parse(quote!(#ident).into()).unwrap() } - }, - )); + }) + .collect(); let mut target_generics_tuple = empty_type_tuple(); let mut ty_generics_tuple = empty_type_tuple(); let generics = self.modify_generics(|g| { - // Add a bump lifetime to the generics - g.params.insert( - 0, - syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))), - ); let index_after_lifetime_in_generics = g .params .iter() @@ -851,6 +1031,11 @@ Finally, call `.build()` to create the instance of `{name}`. ); let repeated_fields_error_message = format!("Repeated field {field_name}"); + let forward_extended_fields = self.extend_fields().map(|f| { + let name = f.name; + quote!(#name: self.#name) + }); + Ok(quote! { #[allow(dead_code, non_camel_case_types, missing_docs)] impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause { @@ -859,6 +1044,7 @@ Finally, call `.build()` to create the instance of `{name}`. let #field_name = (#arg_expr,); let ( #(#descructuring,)* ) = self.fields; #builder_name { + #(#forward_extended_fields,)* bump: self.bump, fields: ( #(#reconstructing,)* ), _phantom: self._phantom, @@ -893,13 +1079,11 @@ Finally, call `.build()` to create the instance of `{name}`. .. } = field; // Add a bump lifetime to the generics - let mut builder_generics: Vec = - vec![syn::GenericArgument::Lifetime(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))]; - builder_generics.extend(self.generics.params.iter().map(|generic_param| { - match generic_param { + let mut builder_generics: Vec = self + .generics + .params + .iter() + .map(|generic_param| match generic_param { syn::GenericParam::Type(type_param) => { let ident = &type_param.ident; syn::parse(quote!(#ident).into()).unwrap() @@ -911,18 +1095,10 @@ Finally, call `.build()` to create the instance of `{name}`. let ident = &const_param.ident; syn::parse(quote!(#ident).into()).unwrap() } - } - })); + }) + .collect(); let mut builder_generics_tuple = empty_type_tuple(); let generics = self.modify_generics(|g| { - // Add a bump lifetime to the generics - g.params.insert( - 0, - syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))), - ); let index_after_lifetime_in_generics = g .params .iter() @@ -1007,15 +1183,6 @@ Finally, call `.build()` to create the instance of `{name}`. } = *self; let generics = self.modify_generics(|g| { - // Add a bump lifetime to the generics - g.params.insert( - 0, - syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))), - ); - let index_after_lifetime_in_generics = g .params .iter() @@ -1054,15 +1221,6 @@ Finally, call `.build()` to create the instance of `{name}`. let (_, ty_generics, where_clause) = self.generics.split_for_impl(); let modified_ty_generics = modify_types_generics_hack(&ty_generics, |args| { - // Add a bump lifetime to the generics - args.insert( - 0, - syn::GenericArgument::Lifetime(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - )), - ); - args.insert( 0, syn::GenericArgument::Type( @@ -1088,7 +1246,9 @@ Finally, call `.build()` to create the instance of `{name}`. // reordering based on that, but for now this much simpler thing is a reasonable approach. let assignments = self.fields.iter().map(|field| { let name = &field.name; - if let Some(ref default) = field.builder_attr.default { + if !field.builder_attr.extends.is_empty() { + quote!(let #name = self.#name;) + } else if let Some(ref default) = field.builder_attr.default { if field.builder_attr.skip { quote!(let #name = #default;) } else { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index dbebd33e0..5723e9418 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -89,9 +89,10 @@ pub mod prelude { consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, provide_context, provide_context_to_scope, provide_root_context, push_future, remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue, - AttributeType, Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, - LazyNodes, MountedAttribute, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, - Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom, + AttributeType, Component, Element, Event, EventHandler, Fragment, HasAttributesBox, + IntoAttributeValue, LazyNodes, MountedAttribute, Properties, Runtime, RuntimeGuard, Scope, + ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw, + VNode, VirtualDom, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index a31481195..d0d10062a 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -899,12 +899,12 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option { } } -pub trait HasAttributesBox<'a, T> { +pub trait HasAttributesBox<'a> { fn push_attribute( self, name: &'a str, ns: Option<&'static str>, attr: impl IntoAttributeValue<'a>, volatile: bool, - ) -> T; + ) -> Self; } diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs index 4b7d23ad5..99c3db0d1 100644 --- a/packages/html-internal-macro/src/lib.rs +++ b/packages/html-internal-macro/src/lib.rs @@ -78,7 +78,7 @@ impl ToTokens for ImplExtensionAttributes { pub trait #extension_name<'a> { #(#defs)* } - impl<'a, T> #extension_name<'a> for T where T: HasAttributesBox<'a, T> + #marker_name { + impl<'a, T> #extension_name<'a> for T where T: HasAttributesBox<'a> + #marker_name { #(#impls)* } }); From b60671e0feaa8cd61efe6965194bb3b11a822b7c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 27 Sep 2023 10:02:49 -0500 Subject: [PATCH 021/126] fix non extending attributes --- examples/spread.rs | 7 +-- packages/core-macro/src/props/mod.rs | 65 +++++++++++++++++-------- packages/core/src/lib.rs | 8 +-- packages/html-internal-macro/src/lib.rs | 22 +++------ packages/html/src/elements.rs | 9 +++- packages/html/src/lib.rs | 7 +++ 6 files changed, 70 insertions(+), 48 deletions(-) diff --git a/examples/spread.rs b/examples/spread.rs index f3be4d2b7..70d677201 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -1,9 +1,4 @@ -use crate::dioxus_elements::ExtendedDivMarker; -use dioxus::{ - core::{exports::bumpalo::Bump, Attribute, HasAttributesBox}, - html::{ExtendedGlobalAttributesMarker, GlobalAttributesExtension}, - prelude::*, -}; +use dioxus::prelude::*; fn main() { let mut dom = VirtualDom::new(app); diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 6285643c5..6b5ad30f3 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -502,7 +502,7 @@ mod struct_info { use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::visit::Visit; - use syn::{Expr, Ident}; + use syn::{parse_quote, Expr, Ident}; use super::field_info::{FieldBuilderAttr, FieldInfo}; use super::util::{ @@ -611,10 +611,17 @@ mod struct_info { // Therefore, we will generate code that shortcircuits the "comparison" in memoization let are_there_generics = !self.generics.params.is_empty(); + let extend_lifetime = self.extend_lifetime()?; + let generics = self.generics.clone(); let (_, ty_generics, where_clause) = generics.split_for_impl(); - let impl_generics = self.generics.clone(); - let (impl_generics, b_initial_generics, _) = impl_generics.split_for_impl(); + let impl_generics = self.modify_generics(|g| { + if extend_lifetime.is_none() { + g.params.insert(0, parse_quote!('__bump)); + } + }); + let (impl_generics, _, _) = impl_generics.split_for_impl(); + let (_, b_initial_generics, _) = self.generics.split_for_impl(); let all_fields_param = syn::GenericParam::Type( syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(), ); @@ -712,9 +719,21 @@ Finally, call `.build()` to create the instance of `{name}`. let name = f.name; quote!(#name: Vec::new()) }); - let extend_lifetime = self - .extend_lifetime()? - .unwrap_or(syn::Lifetime::new("'_", proc_macro2::Span::call_site())); + let has_extend_fields = self.extend_fields().next().is_some(); + let take_bump = if has_extend_fields { + quote!(bump: _cx.bump(),) + } else { + quote!() + }; + let bump_field = if has_extend_fields { + quote!(bump: & #extend_lifetime ::dioxus::core::exports::bumpalo::Bump,) + } else { + quote!() + }; + let extend_lifetime = extend_lifetime.unwrap_or(syn::Lifetime::new( + "'__bump", + proc_macro2::Span::call_site(), + )); Ok(quote! { impl #impl_generics #name #ty_generics #where_clause { @@ -723,7 +742,7 @@ Finally, call `.build()` to create the instance of `{name}`. #vis fn builder(_cx: & #extend_lifetime ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty { #builder_name { #(#extend_fields_value,)* - bump: _cx.bump(), + #take_bump fields: #empties_tuple, _phantom: ::core::default::Default::default(), } @@ -735,7 +754,7 @@ Finally, call `.build()` to create the instance of `{name}`. #[allow(dead_code, non_camel_case_types, non_snake_case)] #vis struct #builder_name #b_generics { #(#extend_fields,)* - bump: & #extend_lifetime ::dioxus::core::exports::bumpalo::Bump, + #bump_field fields: #all_fields_param, _phantom: (#( #phantom_generics ),*), } @@ -862,22 +881,23 @@ Finally, call `.build()` to create the instance of `{name}`. quote!(#name: self.#name) }); - let extends_impl = field.builder_attr.extends.iter().map(|path| { - let name_str = path_to_single_string(path).unwrap(); - let camel_name = name_str.to_case(Case::UpperCamel); - let marker_name = Ident::new( - format!("Extended{}Marker", &camel_name).as_str(), - path.span(), - ); - quote! { - impl #impl_generics #marker_name for #builder_name < #( #ty_generics ),* > #where_clause {} - } - }); let extend_lifetime = self.extend_lifetime()?.ok_or(Error::new_spanned( field_name, "Unable to find lifetime for extended field. Please specify it manually", ))?; + let extends_impl = field.builder_attr.extends.iter().map(|path| { + let name_str = path_to_single_string(path).unwrap(); + let camel_name = name_str.to_case(Case::UpperCamel); + let marker_name = Ident::new( + format!("{}Extension", &camel_name).as_str(), + path.span(), + ); + quote! { + impl #impl_generics dioxus_elements::extensions::#marker_name < #extend_lifetime > for #builder_name < #( #ty_generics ),* > #where_clause {} + } + }); + Ok(quote! { impl #impl_generics ::dioxus::prelude::HasAttributesBox<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause { fn push_attribute( @@ -1035,6 +1055,11 @@ Finally, call `.build()` to create the instance of `{name}`. let name = f.name; quote!(#name: self.#name) }); + let forward_bump = if self.extend_fields().next().is_some() { + quote!(bump: self.bump,) + } else { + quote!() + }; Ok(quote! { #[allow(dead_code, non_camel_case_types, missing_docs)] @@ -1045,7 +1070,7 @@ Finally, call `.build()` to create the instance of `{name}`. let ( #(#descructuring,)* ) = self.fields; #builder_name { #(#forward_extended_fields,)* - bump: self.bump, + #forward_bump fields: ( #(#reconstructing,)* ), _phantom: self._phantom, } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 5723e9418..5fb5d7ba3 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -89,10 +89,10 @@ pub mod prelude { consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, provide_context, provide_context_to_scope, provide_root_context, push_future, remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue, - AttributeType, Component, Element, Event, EventHandler, Fragment, HasAttributesBox, - IntoAttributeValue, LazyNodes, MountedAttribute, Properties, Runtime, RuntimeGuard, Scope, - ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw, - VNode, VirtualDom, + Attribute, AttributeType, Component, Element, Event, EventHandler, Fragment, + HasAttributesBox, IntoAttributeValue, LazyNodes, MountedAttribute, Properties, Runtime, + RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, + TemplateNode, Throw, VNode, VirtualDom, }; } diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs index 99c3db0d1..3de23f6d6 100644 --- a/packages/html-internal-macro/src/lib.rs +++ b/packages/html-internal-macro/src/lib.rs @@ -39,13 +39,13 @@ impl Parse for ImplExtensionAttributes { impl ToTokens for ImplExtensionAttributes { fn to_tokens(&self, tokens: &mut TokenStream2) { let name = &self.name; - let camel_name = name.to_string().to_case(Case::UpperCamel); + let name_string = name.to_string(); + let camel_name = name_string + .strip_prefix("r#") + .unwrap_or(&name_string) + .to_case(Case::UpperCamel); let impl_name = Ident::new(format!("{}Impl", &camel_name).as_str(), name.span()); let extension_name = Ident::new(format!("{}Extension", &camel_name).as_str(), name.span()); - let marker_name = Ident::new( - format!("Extended{}Marker", &camel_name).as_str(), - name.span(), - ); if !self.is_element { tokens.append_all(quote! { @@ -54,11 +54,6 @@ impl ToTokens for ImplExtensionAttributes { }); } - let defs = self.attrs.iter().map(|ident| { - quote! { - fn #ident(self, value: impl IntoAttributeValue<'a>) -> Self; - } - }); let impls = self.attrs.iter().map(|ident| { let d = if self.is_element { quote! { #name::#ident } @@ -73,12 +68,7 @@ impl ToTokens for ImplExtensionAttributes { } }); tokens.append_all(quote! { - pub trait #marker_name {} - - pub trait #extension_name<'a> { - #(#defs)* - } - impl<'a, T> #extension_name<'a> for T where T: HasAttributesBox<'a> + #marker_name { + pub trait #extension_name<'a>: HasAttributesBox<'a> + Sized { #(#impls)* } }); diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index ab9b6ea32..a004f65a2 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -111,8 +111,6 @@ macro_rules! impl_element { } impl GlobalAttributes for $name {} - - impl_extension_attributes![ELEMENT $name { $($fil,)* }]; }; ( @@ -289,6 +287,13 @@ macro_rules! builder_constructors { } ); )* + + pub(crate) mod extensions { + use super::*; + $( + impl_extension_attributes![ELEMENT $name { $($fil,)* }]; + )* + } }; } diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 2fc1baeac..b3fccb4c9 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -39,7 +39,14 @@ pub use render_template::*; mod eval; +pub mod extensions { + pub use crate::elements::extensions::*; + pub use crate::global_attributes::{GlobalAttributesExtension, SvgAttributesExtension}; +} + pub mod prelude { + pub use crate::elements::extensions::*; pub use crate::eval::*; pub use crate::events::*; + pub use crate::global_attributes::{GlobalAttributesExtension, SvgAttributesExtension}; } From 76a050314a359c80545e1fd9dd80d44b2e712159 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 27 Sep 2023 10:05:02 -0500 Subject: [PATCH 022/126] make spread example more interesting --- examples/spread.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/spread.rs b/examples/spread.rs index 70d677201..698713d51 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -12,6 +12,8 @@ fn app(cx: Scope) -> Element { render! { Component { width: "10px", + extra_data: "hello{1}", + extra_data2: "hello{2}", height: "10px", left: 1, } @@ -23,6 +25,7 @@ fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { render! { audio { ..attributes, + "1: {cx.props.extra_data}\n2: {cx.props.extra_data2}" } } } @@ -31,4 +34,6 @@ fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { struct Props<'a> { #[props(extends = GlobalAttributes)] attributes: Vec>, + extra_data: &'a str, + extra_data2: &'a str, } From ee25c03e74a9809d19d6cb7a3f7e90f3d5fc031c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 27 Sep 2023 10:13:45 -0500 Subject: [PATCH 023/126] fix some clippy lints --- examples/README.md | 1 - examples/spread.rs | 3 +-- packages/core-macro/src/props/mod.rs | 4 +++- packages/core/src/lib.rs | 10 +++++----- packages/core/src/nodes.rs | 13 ++++++++++++- packages/html-internal-macro/src/lib.rs | 2 +- packages/html/src/elements.rs | 2 +- packages/html/src/global_attributes.rs | 2 +- packages/rsx/src/attribute.rs | 2 +- 9 files changed, 25 insertions(+), 14 deletions(-) diff --git a/examples/README.md b/examples/README.md index 93530ba30..a20ffe60e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -139,7 +139,6 @@ Missing Features Missing examples - Shared state - Root-less element groups -- Spread props - Custom elements - Component Children: Pass children into child components - Render To string: Render a mounted virtualdom to a string diff --git a/examples/spread.rs b/examples/spread.rs index 698713d51..dc6b7b01d 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -21,10 +21,9 @@ fn app(cx: Scope) -> Element { } fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { - let attributes = &*cx.props.attributes; render! { audio { - ..attributes, + ..cx.props.attributes, "1: {cx.props.extra_data}\n2: {cx.props.extra_data2}" } } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 6b5ad30f3..a0bbdd4e9 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -894,12 +894,14 @@ Finally, call `.build()` to create the instance of `{name}`. path.span(), ); quote! { + #[allow(dead_code, non_camel_case_types, missing_docs)] impl #impl_generics dioxus_elements::extensions::#marker_name < #extend_lifetime > for #builder_name < #( #ty_generics ),* > #where_clause {} } }); Ok(quote! { - impl #impl_generics ::dioxus::prelude::HasAttributesBox<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause { + #[allow(dead_code, non_camel_case_types, missing_docs)] + impl #impl_generics ::dioxus::prelude::HasAttributes<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause { fn push_attribute( mut self, name: &#extend_lifetime str, diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 5fb5d7ba3..e2f8e96c1 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -76,7 +76,7 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeType, AttributeValue, BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event, - Fragment, HasAttributesBox, IntoDynNode, LazyNodes, MountedAttribute, Mutation, Mutations, + Fragment, HasAttributes, IntoDynNode, LazyNodes, MountedAttribute, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom, }; @@ -89,10 +89,10 @@ pub mod prelude { consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, provide_context, provide_context_to_scope, provide_root_context, push_future, remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue, - Attribute, AttributeType, Component, Element, Event, EventHandler, Fragment, - HasAttributesBox, IntoAttributeValue, LazyNodes, MountedAttribute, Properties, Runtime, - RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, - TemplateNode, Throw, VNode, VirtualDom, + Attribute, AttributeType, Component, Element, Event, EventHandler, Fragment, HasAttributes, + IntoAttributeValue, LazyNodes, MountedAttribute, Properties, Runtime, RuntimeGuard, Scope, + ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw, + VNode, VirtualDom, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index d0d10062a..2665db5a3 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -414,6 +414,7 @@ pub enum TemplateAttribute<'a> { }, } +/// An attribute with information about its position in the DOM and the element it was mounted to #[derive(Debug)] pub struct MountedAttribute<'a> { pub(crate) ty: AttributeType<'a>, @@ -440,6 +441,12 @@ impl<'a> From<&'a [Attribute<'a>]> for MountedAttribute<'a> { } } +impl<'a> From<&'a Vec>> for MountedAttribute<'a> { + fn from(attr: &'a Vec>) -> Self { + attr.as_slice().into() + } +} + impl<'a> MountedAttribute<'a> { /// Get the type of this attribute pub fn attribute_type(&self) -> &AttributeType<'a> { @@ -487,8 +494,10 @@ impl<'a> Attribute<'a> { } } +/// The type of an attribute #[derive(Debug)] pub enum AttributeType<'a> { + /// A single attribute Single(Attribute<'a>), /// Many different attributes sorted by name Many(&'a [Attribute<'a>]), @@ -899,7 +908,9 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option { } } -pub trait HasAttributesBox<'a> { +/// A trait for anything that has a dynamic list of attributes +pub trait HasAttributes<'a> { + /// Push an attribute onto the list of attributes fn push_attribute( self, name: &'a str, diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs index 3de23f6d6..049b36469 100644 --- a/packages/html-internal-macro/src/lib.rs +++ b/packages/html-internal-macro/src/lib.rs @@ -68,7 +68,7 @@ impl ToTokens for ImplExtensionAttributes { } }); tokens.append_all(quote! { - pub trait #extension_name<'a>: HasAttributesBox<'a> + Sized { + pub trait #extension_name<'a>: HasAttributes<'a> + Sized { #(#impls)* } }); diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index a004f65a2..fe7447790 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1,7 +1,7 @@ #![allow(non_upper_case_globals)] use dioxus_core::prelude::IntoAttributeValue; -use dioxus_core::HasAttributesBox; +use dioxus_core::HasAttributes; use dioxus_html_internal_macro::impl_extension_attributes; #[cfg(feature = "hot-reload-context")] use dioxus_rsx::HotReloadingContext; diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 97811fe1f..41a57ab84 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -1,7 +1,7 @@ #![allow(non_upper_case_globals)] use dioxus_core::prelude::IntoAttributeValue; -use dioxus_core::HasAttributesBox; +use dioxus_core::HasAttributes; use dioxus_html_internal_macro::impl_extension_attributes; use crate::AttributeDiscription; diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index 6f5d0a096..76acc43bf 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -32,7 +32,7 @@ impl ToTokens for AttributeType { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { AttributeType::Named(n) => tokens.append_all(quote! { #n }), - AttributeType::Spread(e) => tokens.append_all(quote! { #e.into() }), + AttributeType::Spread(e) => tokens.append_all(quote! { (&#e).into() }), } } } From 3e55bb8885f041a4c9a901983dc4590d569d415a Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 27 Sep 2023 10:21:33 -0500 Subject: [PATCH 024/126] fix some clippy lints --- packages/core-macro/src/props/mod.rs | 2 +- packages/html-internal-macro/src/lib.rs | 2 +- packages/rsx/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index a0bbdd4e9..3289d891b 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -554,7 +554,7 @@ mod struct_info { let name = f.name; let mut visitor = VisitFirstLifetime(None); - visitor.visit_type(&f.ty); + visitor.visit_type(f.ty); visitor.0.ok_or_else(|| { syn::Error::new_spanned( diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs index 049b36469..a404a9b16 100644 --- a/packages/html-internal-macro/src/lib.rs +++ b/packages/html-internal-macro/src/lib.rs @@ -29,7 +29,7 @@ impl Parse for ImplExtensionAttributes { let attrs = content.parse_terminated(Ident::parse, Token![,])?; Ok(ImplExtensionAttributes { - is_element: element.to_string() == "ELEMENT", + is_element: element == "ELEMENT", name, attrs, }) diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 24bb8b533..193abe85d 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -390,7 +390,7 @@ impl<'a> DynamicContext<'a> { _ => { let idx = match mapping { - Some(mapping) => mapping.get_attribute_idx(&attr)?, + Some(mapping) => mapping.get_attribute_idx(attr)?, None => self.dynamic_attributes.len(), }; self.dynamic_attributes.push(attr); From c13735709466ec0ec14c4bc8fee505aace4b5f7b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 27 Sep 2023 10:22:49 -0500 Subject: [PATCH 025/126] fix hydration --- packages/core/src/scope_context.rs | 2 +- packages/web/src/rehydrate.rs | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 79ed05d75..59bed3b1f 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -128,7 +128,7 @@ impl ScopeContext { parent.name ); if let Some(shared) = parent.shared_contexts.borrow().iter().find_map(|any| { - tracing::trace!("found context {:?}", any.type_id()); + tracing::trace!("found context {:?}", (**any).type_id()); any.downcast_ref::() }) { return Some(shared.clone()); diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 4415ceb37..4ee437963 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -128,18 +128,20 @@ impl WebsysDom { for attr in *attrs { if let dioxus_core::TemplateAttribute::Dynamic { id } = attr { let attribute = &vnode.dynamic_attrs[*id]; - let value = &attribute.value; - let id = attribute.mounted_element(); - mounted_id = Some(id); - let name = attribute.name; - if let AttributeValue::Listener(_) = value { - let event_name = &name[2..]; - self.interpreter.new_event_listener( - event_name, - id.0 as u32, - event_bubbles(event_name) as u8, - ); - } + &attribute.attribute_type().for_each(|attribute| { + let value = &attribute.value; + let id = attribute.mounted_element(); + mounted_id = Some(id); + let name = attribute.name; + if let AttributeValue::Listener(_) = value { + let event_name = &name[2..]; + self.interpreter.new_event_listener( + event_name, + id.0 as u32, + event_bubbles(event_name) as u8, + ); + } + }); } } if let Some(id) = mounted_id { From eb15014b9d3678ec2bfd15a73a8e04b93facffca Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 3 Oct 2023 16:42:42 -0500 Subject: [PATCH 026/126] fix hydration mounted element --- packages/web/src/rehydrate.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 4ee437963..1553b26dc 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -128,9 +128,9 @@ impl WebsysDom { for attr in *attrs { if let dioxus_core::TemplateAttribute::Dynamic { id } = attr { let attribute = &vnode.dynamic_attrs[*id]; - &attribute.attribute_type().for_each(|attribute| { + let id = attribute.mounted_element(); + attribute.attribute_type().for_each(|attribute| { let value = &attribute.value; - let id = attribute.mounted_element(); mounted_id = Some(id); let name = attribute.name; if let AttributeValue::Listener(_) = value { From d5b7a6d15e348fa101cd02844601682ae51a6798 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 4 Oct 2023 08:46:24 -0500 Subject: [PATCH 027/126] fix fuzzing test --- examples/spread.rs | 1 + packages/native-core/tests/fuzzing.rs | 28 +++++++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/examples/spread.rs b/examples/spread.rs index dc6b7b01d..b13124a5f 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -20,6 +20,7 @@ fn app(cx: Scope) -> Element { } } +#[component] fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { render! { audio { diff --git a/packages/native-core/tests/fuzzing.rs b/packages/native-core/tests/fuzzing.rs index 4f0a34b70..63376f86e 100644 --- a/packages/native-core/tests/fuzzing.rs +++ b/packages/native-core/tests/fuzzing.rs @@ -204,6 +204,20 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { } } +fn create_random_dynamic_mounted_attr(cx: &ScopeState) -> MountedAttribute { + match rand::random::() % 2 { + 0 => MountedAttribute::from( + &*cx.bump().alloc( + (0..(rand::random::() % 3) as usize) + .map(|_| create_random_dynamic_attr(cx)) + .collect::>(), + ), + ), + 1 => MountedAttribute::from(create_random_dynamic_attr(cx)), + _ => unreachable!(), + } +} + fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { let value = match rand::random::() % 6 { 0 => AttributeValue::Text(Box::leak( @@ -214,7 +228,6 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { 3 => AttributeValue::Bool(rand::random()), 4 => cx.any_value(rand::random::()), 5 => AttributeValue::None, - // Listener(RefCell>>), _ => unreachable!(), }; Attribute::new( @@ -273,11 +286,14 @@ fn create_random_element(cx: Scope) -> Element { .collect(); cx.bump().alloc(dynamic_nodes) }, - dynamic_attrs: cx.bump().alloc( - (0..template.attr_paths.len()) - .map(|_| create_random_dynamic_attr(cx)) - .collect::>(), - ), + dynamic_attrs: cx + .bump() + .alloc( + (0..template.attr_paths.len()) + .map(|_| create_random_dynamic_mounted_attr(cx)) + .collect::>(), + ) + .as_slice(), }; Some(node) } From ed95ac932dc260b6a5e3feb0b14a860ba2a67562 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 4 Oct 2023 08:58:28 -0500 Subject: [PATCH 028/126] fix double triggering events --- packages/core/src/virtual_dom.rs | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 7bd5ae0b9..f94b35678 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -375,30 +375,14 @@ impl VirtualDom { for (idx, attr) in template.dynamic_attrs.iter().enumerate() { let this_path = node_template.attr_paths[idx]; - fn add_listener<'a>( - attribute: &'a Attribute<'a>, - event_name: &str, - listeners: &mut Vec<&'a AttributeValue<'a>>, - ) { - if attribute.name.trim_start_matches("on") == event_name { - listeners.push(&attribute.value); - } - - listeners.push(&attribute.value); - } - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one if target_path.is_decendant(&this_path) { - match &attr.ty { - AttributeType::Single(attribute) => { - add_listener(attribute, name, &mut listeners); + attr.ty.for_each(|attribute| { + if attribute.name.trim_start_matches("on") == name { + listeners.push(&attribute.value); } - AttributeType::Many(attributes) => { - for attribute in *attributes { - add_listener(attribute, name, &mut listeners); - } - } - } + }); + // Break if this is the exact target element. // This means we won't call two listeners with the same name on the same element. This should be // documented, or be rejected from the rsx! macro outright From 7f0fa07757c62930f60577fa762035e6bcdd4cd0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 4 Oct 2023 09:11:21 -0500 Subject: [PATCH 029/126] fix hot reloading --- packages/core-macro/tests/rsx.rs | 2 -- packages/fullstack/src/hooks/server_cached.rs | 1 + packages/rsx/src/attribute.rs | 16 +++++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/core-macro/tests/rsx.rs b/packages/core-macro/tests/rsx.rs index ff01681fb..f4037f928 100644 --- a/packages/core-macro/tests/rsx.rs +++ b/packages/core-macro/tests/rsx.rs @@ -1,5 +1,3 @@ -use dioxus::prelude::*; - #[test] fn rsx() { let t = trybuild::TestCases::new(); diff --git a/packages/fullstack/src/hooks/server_cached.rs b/packages/fullstack/src/hooks/server_cached.rs index 519f70dae..fdccd070a 100644 --- a/packages/fullstack/src/hooks/server_cached.rs +++ b/packages/fullstack/src/hooks/server_cached.rs @@ -15,6 +15,7 @@ use serde::{de::DeserializeOwned, Serialize}; /// let state1 = use_state(cx, || from_server(|| { /// 1234 /// })); +/// todo!() /// } /// ``` pub fn server_cached(server_fn: impl Fn() -> O) -> O { diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index 76acc43bf..4c0db5a33 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -37,12 +37,26 @@ impl ToTokens for AttributeType { } } -#[derive(PartialEq, Eq, Clone, Debug, Hash)] +#[derive(Clone, Debug)] pub struct ElementAttrNamed { pub el_name: ElementName, pub attr: ElementAttr, } +impl Hash for ElementAttrNamed { + fn hash(&self, state: &mut H) { + self.attr.name.hash(state); + } +} + +impl PartialEq for ElementAttrNamed { + fn eq(&self, other: &Self) -> bool { + self.attr == other.attr + } +} + +impl Eq for ElementAttrNamed {} + impl ElementAttrNamed { pub(crate) fn try_combine(&self, other: &Self) -> Option { if self.el_name == other.el_name && self.attr.name == other.attr.name { From 9b7b2e7877137bce4bd879aa716b497878fd3268 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 17 Oct 2023 17:51:44 -0700 Subject: [PATCH 030/126] Try making the cache faster using mozilla's cache --- .github/workflows/main.yml | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 037525557..6c8471874 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,9 +36,14 @@ jobs: if: github.event.pull_request.draft == false name: Check runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" steps: - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.3 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - uses: actions/checkout@v4 @@ -48,9 +53,14 @@ jobs: if: github.event.pull_request.draft == false name: Test Suite runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" steps: - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.3 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - uses: davidB/rust-cargo-make@v1 @@ -63,9 +73,14 @@ jobs: if: github.event.pull_request.draft == false name: Rustfmt runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" steps: - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.3 - run: rustup component add rustfmt - uses: actions/checkout@v4 - run: cargo fmt --all -- --check @@ -74,9 +89,14 @@ jobs: if: github.event.pull_request.draft == false name: Clippy runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" steps: - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.3 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - run: rustup component add clippy @@ -86,6 +106,10 @@ jobs: matrix_test: runs-on: ${{ matrix.platform.os }} env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }} strategy: matrix: @@ -136,7 +160,7 @@ jobs: if: ${{ matrix.platform.cross == true }} uses: taiki-e/install-action@cross - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.3 with: workspaces: core -> ../target save-if: ${{ matrix.features.key == 'all' }} From 0f0d90ee193d17310545ba245b26e22fbe7ce2f1 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Thu, 12 Oct 2023 15:29:02 -0400 Subject: [PATCH 031/126] Add a simple nix dev shell --- flake.lock | 247 +++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 63 ++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..e5c50616d --- /dev/null +++ b/flake.lock @@ -0,0 +1,247 @@ +{ + "nodes": { + "crane": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ], + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1696384830, + "narHash": "sha256-j8ZsVqzmj5sOm5MW9cqwQJUZELFFwOislDmqDDEMl6k=", + "owner": "ipetkov", + "repo": "crane", + "rev": "f2143cd27f8bd09ee4f0121336c65015a2a0a19c", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696267196, + "narHash": "sha256-AAQ/2sD+0D18bb8hKuEEVpHUYD1GmO2Uh/taFamn6XQ=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "4f910c9827911b1ec2bf26b5a062cd09f8d89f85", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1696343447, + "narHash": "sha256-B2xAZKLkkeRFG5XcHHSXXcP7To9Xzr59KXeZiRf4vdQ=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "c9afaba3dfa4085dbd2ccb38dfade5141e33d9d4", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1697009197, + "narHash": "sha256-viVRhBTFT8fPJTb1N3brQIpFZnttmwo3JVKNuWRVc3s=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "01441e14af5e29c9d27ace398e6dd0b293e25a54", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1696019113, + "narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1681358109, + "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay_2", + "systems": "systems_3" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "crane", + "flake-utils" + ], + "nixpkgs": [ + "crane", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1696299134, + "narHash": "sha256-RS77cAa0N+Sfj5EmKbm5IdncNXaBCE1BSSQvUE8exvo=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "611ccdceed92b4d94ae75328148d84ee4a5b462d", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1697076655, + "narHash": "sha256-NcCtVUOd0X81srZkrdP8qoA1BMsPdO2tGtlZpsGijeU=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "aa7584f5bbf5947716ad8ec14eccc0334f0d28f0", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..7f151a660 --- /dev/null +++ b/flake.nix @@ -0,0 +1,63 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + systems.url = "github:nix-systems/default"; + + rust-overlay.url = "github:oxalica/rust-overlay"; + crane.url = "github:ipetkov/crane"; + crane.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = import inputs.systems; + + perSystem = { config, self', pkgs, lib, system, ... }: + let + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + ]; + }; + rustBuildInputs = [ + pkgs.openssl + pkgs.libiconv + pkgs.pkg-config + ] ++ lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; [ + IOKit + Carbon + WebKit + Security + Cocoa + ]); + + # This is useful when building crates as packages + # Note that it does require a `Cargo.lock` which this repo does not have + # craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; + in + { + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ + inputs.rust-overlay.overlays.default + ]; + }; + + devShells.default = pkgs.mkShell { + name = "dioxus-dev"; + buildInputs = rustBuildInputs; + nativeBuildInputs = [ + # Add shell dependencies here + rustToolchain + ]; + shellHook = '' + # For rust-analyzer 'hover' tooltips to work. + export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; + ''; + }; + }; + }; +} From 46fec1ae482e38cfee26e925ea22b88544dcf18a Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 2 Nov 2023 13:46:57 -0500 Subject: [PATCH 032/126] fix clippy --- packages/core-macro/src/props/mod.rs | 4 +--- packages/core/src/diff.rs | 2 +- packages/rsx/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 2ef213762..59a5c0cd6 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -936,9 +936,7 @@ Finally, call `.build()` to create the instance of `{name}`. pub fn field_impl(&self, field: &FieldInfo) -> Result { let FieldInfo { - name: field_name, - ty: field_type, - .. + name: field_name, .. } = field; if *field_name == "key" { return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys")); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 64d16b1ce..5e7bf7573 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -626,7 +626,7 @@ impl<'b> VirtualDom { // If none of the old keys are reused by the new children, then we remove all the remaining old children and // create the new children afresh. if shared_keys.is_empty() { - if old.get(0).is_some() { + if !old.is_empty() { self.remove_nodes(&old[1..]); self.replace(&old[0], new); } else { diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index d4170ffe9..7a705ba6b 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -195,7 +195,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { fn to_tokens(&self, out_tokens: &mut TokenStream2) { let mut context = DynamicContext::default(); - let key = match self.roots.get(0) { + let key = match self.roots.first() { Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key.clone(), Some(BodyNode::Component(comp)) if self.roots.len() == 1 => comp.key().cloned(), _ => None, From f372144fd7b903896636781d469568127a0789c3 Mon Sep 17 00:00:00 2001 From: divinerapier Date: Fri, 3 Nov 2023 10:59:04 +0800 Subject: [PATCH 033/126] Bump tauri bundler for using proxy to fetch resources Signed-off-by: divinerapier --- packages/cli/Cargo.toml | 4 ++-- packages/cli/src/config.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index adb0239f2..a11ed3545 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -72,8 +72,8 @@ cargo-generate = "0.18" toml_edit = "0.19.11" # bundling -tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] } -tauri-utils = "=1.4.*" +tauri-bundler = { version = "=1.4.*", features = ["native-tls-vendored"] } +tauri-utils = "=1.5.*" dioxus-autofmt = { workspace = true } dioxus-check = { workspace = true } diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 3abf47f0c..037b5f167 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -525,6 +525,7 @@ impl From for tauri_bundler::NsisSettings { display_language_selector: val.display_language_selector, custom_language_files: None, template: None, + compression: None, } } } From 02e91d8b9aee3b1815c3ad86c506c0f2cc27741a Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Sat, 4 Nov 2023 21:50:11 +0530 Subject: [PATCH 034/126] add get_parsed_values function --- packages/rink/src/hooks.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index f8c4500d2..a78194753 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -74,6 +74,29 @@ impl FormData { files: None, } } + + // ***** function to parse the 'values' to make it ready to use******* + // e.g - self.values = { username: ["rust"], password: ["dioxus"]} + // what we need it to be: { username: "rust", password: "dioxus"} + pub fn get_parsed_values(&self) -> Option> { + if (self.values.is_empty()) { + return None; + } + + let raw_values = self.values.clone(); + + let mut parsed_values: HashMap = HashMap::new(); + + for (fieldname, values) in raw_values.into_iter() { + // convert array of string value to string + // eg - ["rust", "javascript"] => "rust, javascript" + let prepared_value = values.join(", "); + + parsed_values.insert(fieldname, prepared_value); + } + + Some(parsed_values) + } } #[derive(Clone, Debug, PartialEq)] From d54ec57192b191cabb6f8ca138c369788fccaeb2 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Sun, 5 Nov 2023 08:16:09 +0530 Subject: [PATCH 035/126] 1. migrated code to form.rs 2. made get_parsed_values as private fn. 3. handled multi-valued data while parsing --- packages/html/src/events/form.rs | 55 ++++++++++++++++++++++++++++++-- packages/rink/src/hooks.rs | 23 ------------- 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index fab13482b..e3b35b098 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -1,9 +1,16 @@ use std::{any::Any, collections::HashMap, fmt::Debug}; use dioxus_core::Event; +use serde::{Deserialize, Serialize}; pub type FormEvent = Event; +#[derive(Serialize, Deserialize)] +enum ValueType { + Text(String), + VecText(Vec), +} + /* DOMEvent: Send + SyncTarget relatedTarget */ #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone)] @@ -23,6 +30,52 @@ pub struct FormData { pub files: Option>, } +impl FormData { + // ***** function to parse the 'values' to make it ready to use******* + // e.g - self.values = { username: ["rust"], password: ["dioxus"]} + // what we need it to be: { username: "rust", password: "dioxus"} + fn get_parsed_values(&self) -> Option> { + if self.values.is_empty() { + return None; + } + + let raw_values = self.values.clone(); + + let mut parsed_values: HashMap = HashMap::new(); + + for (fieldname, values) in raw_values.into_iter() { + /* + case 1 - multiple values, example { driving_types: ["manual", "automatic"] } + In this case we want to return the values as it is, NO point in making + driving_types: "manual, automatic" + + case 2 - single value, example { favourite_language: ["rust"] } + In this case we would want to deserialize the value as follows + favourite_language: "rust" + */ + if values.len() > 1 { + // handling multiple values - case 1 + parsed_values.insert(fieldname, ValueType::VecText(values.clone())); + } else { + // handle single value - case 2 + let prepared_value = values.into_iter().next()?; + parsed_values.insert(fieldname, ValueType::Text(prepared_value)); + } + } + + Some(parsed_values) + } + + pub fn parse_json(&self) -> Result + where + T: serde::de::DeserializeOwned, + { + let formatted = self.get_parsed_values(); + let as_string = serde_json::to_string(&formatted)?; + serde_json::from_str(&as_string) + } +} + #[cfg(feature = "serialize")] #[derive(serde::Serialize, serde::Deserialize)] struct SerializedFileEngine { @@ -60,8 +113,6 @@ fn deserialize_file_engine<'de, D>( where D: serde::Deserializer<'de>, { - use serde::Deserialize; - let Ok(file_engine) = SerializedFileEngine::deserialize(deserializer) else { return Ok(None); }; diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index a78194753..f8c4500d2 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -74,29 +74,6 @@ impl FormData { files: None, } } - - // ***** function to parse the 'values' to make it ready to use******* - // e.g - self.values = { username: ["rust"], password: ["dioxus"]} - // what we need it to be: { username: "rust", password: "dioxus"} - pub fn get_parsed_values(&self) -> Option> { - if (self.values.is_empty()) { - return None; - } - - let raw_values = self.values.clone(); - - let mut parsed_values: HashMap = HashMap::new(); - - for (fieldname, values) in raw_values.into_iter() { - // convert array of string value to string - // eg - ["rust", "javascript"] => "rust, javascript" - let prepared_value = values.join(", "); - - parsed_values.insert(fieldname, prepared_value); - } - - Some(parsed_values) - } } #[derive(Clone, Debug, PartialEq)] From 8a2d170d9622cb466f034f46ba3123e92fe7464c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 8 Nov 2023 12:48:25 -0600 Subject: [PATCH 036/126] pull out the CLI config data into a separate library --- Cargo.toml | 2 + packages/cli-config/.gitignore | 4 + packages/cli-config/Cargo.toml | 102 ++++ packages/cli-config/README.md | 5 + packages/cli-config/src/assets/dioxus.toml | 74 +++ packages/{cli => cli-config}/src/cargo.rs | 43 +- packages/cli-config/src/config.rs | 658 +++++++++++++++++++++ packages/cli-config/src/lib.rs | 9 + packages/cli/Cargo.toml | 1 + packages/cli/src/builder.rs | 3 +- packages/cli/src/cli/build.rs | 2 +- packages/cli/src/cli/cfg.rs | 13 +- packages/cli/src/cli/config.rs | 4 +- packages/cli/src/cli/serve.rs | 6 +- packages/cli/src/config.rs | 9 +- packages/cli/src/error.rs | 14 + packages/cli/src/lib.rs | 3 - packages/cli/src/server/output.rs | 3 +- 18 files changed, 919 insertions(+), 36 deletions(-) create mode 100644 packages/cli-config/.gitignore create mode 100644 packages/cli-config/Cargo.toml create mode 100644 packages/cli-config/README.md create mode 100644 packages/cli-config/src/assets/dioxus.toml rename packages/{cli => cli-config}/src/cargo.rs (69%) create mode 100644 packages/cli-config/src/config.rs create mode 100644 packages/cli-config/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index d687d74d0..c615e2e42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "packages/dioxus", "packages/core", "packages/cli", + "packages/cli-config", "packages/core-macro", "packages/router-macro", "packages/extension", @@ -78,6 +79,7 @@ dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4 rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" } dioxus-signals = { path = "packages/signals" } generational-box = { path = "packages/generational-box" } +dioxus-cli-config = { path = "packages/cli-config", version = "0.4.1" } dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" } dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" } dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" } diff --git a/packages/cli-config/.gitignore b/packages/cli-config/.gitignore new file mode 100644 index 000000000..6700b1332 --- /dev/null +++ b/packages/cli-config/.gitignore @@ -0,0 +1,4 @@ +/target +Cargo.lock +.DS_Store +.idea/ diff --git a/packages/cli-config/Cargo.toml b/packages/cli-config/Cargo.toml new file mode 100644 index 000000000..d89d411fd --- /dev/null +++ b/packages/cli-config/Cargo.toml @@ -0,0 +1,102 @@ +[package] +name = "dioxus-cli-config" +version = "0.4.1" +authors = ["Jonathan Kelley"] +edition = "2021" +description = "Configuration for the Dioxus CLI" +repository = "https://github.com/DioxusLabs/dioxus/" +license = "MIT OR Apache-2.0" +keywords = ["react", "gui", "cli", "dioxus", "wasm"] + +[dependencies] +# cli core +clap = { version = "4.2", features = ["derive"] } +thiserror = { workspace = true } +wasm-bindgen-cli-support = "0.2" +colored = "2.0.0" + +# features +log = "0.4.14" +fern = { version = "0.6.0", features = ["colored"] } +serde = { version = "1.0.136", features = ["derive"] } +serde_json = "1.0.79" +toml = "0.5.8" +fs_extra = "1.2.0" +cargo_toml = "0.16.0" +futures = "0.3.21" +notify = { version = "5.0.0-pre.16", features = ["serde"] } +html_parser = { workspace = true } +cargo_metadata = "0.15.0" +tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] } +atty = "0.2.14" +chrono = "0.4.19" +anyhow = "1.0.53" +hyper = "0.14.17" +hyper-rustls = "0.23.2" +indicatif = "0.17.5" +subprocess = "0.2.9" + +axum = { version = "0.5.1", features = ["ws", "headers"] } +axum-server = { version = "0.5.1", features = ["tls-rustls"] } +tower-http = { version = "0.2.2", features = ["full"] } +headers = "0.3.7" + +walkdir = "2" + +# tools download +dirs = "4.0.0" +reqwest = { version = "0.11", features = [ + "rustls-tls", + "stream", + "trust-dns", + "blocking", +] } +flate2 = "1.0.22" +tar = "0.4.38" +zip = "0.6.2" +tower = "0.4.12" +syn = { version = "2.0", features = ["full", "extra-traits"] } +lazy_static = "1.4.0" + +# plugin packages +mlua = { version = "0.8.1", features = [ + "lua54", + "vendored", + "async", + "send", + "macros", +], optional = true } +ctrlc = "3.2.3" +open = "4.1.0" +cargo-generate = "0.18" +toml_edit = "0.19.11" + +# bundling +tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] } +tauri-utils = "=1.4.*" + +dioxus-autofmt = { workspace = true } +dioxus-check = { workspace = true } +rsx-rosetta = { workspace = true } +dioxus-rsx = { workspace = true } +dioxus-html = { workspace = true, features = ["hot-reload-context"] } +dioxus-core = { workspace = true, features = ["serialize"] } +dioxus-hot-reload = { workspace = true } +interprocess-docfix = { version = "1.2.2" } + +[features] +default = [] +plugin = ["mlua"] + +[[bin]] +path = "src/main.rs" +name = "dx" + +[dev-dependencies] +tempfile = "3.3" + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" diff --git a/packages/cli-config/README.md b/packages/cli-config/README.md new file mode 100644 index 000000000..ef2435713 --- /dev/null +++ b/packages/cli-config/README.md @@ -0,0 +1,5 @@ +
+

📦✨ Dioxus CLI Configuration

+
+ +The **dioxus-cli-config** contains the configuration for the **dioxus-cli**. diff --git a/packages/cli-config/src/assets/dioxus.toml b/packages/cli-config/src/assets/dioxus.toml new file mode 100644 index 000000000..892a6cdf8 --- /dev/null +++ b/packages/cli-config/src/assets/dioxus.toml @@ -0,0 +1,74 @@ +[application] + +# dioxus project name +name = "{{project-name}}" + +# default platfrom +# you can also use `dx serve/build --platform XXX` to use other platform +# value: web | desktop +default_platform = "{{default-platform}}" + +# Web `build` & `serve` dist path +out_dir = "dist" + +# resource (static) file folder +asset_dir = "public" + +[web.app] + +# HTML title tag content +title = "Dioxus | An elegant GUI library for Rust" + +[web.watcher] + +index_on_404 = true + +watch_path = ["src", "examples"] + +# include `assets` in web platform +[web.resource] + +# CSS style file +style = [] + +# Javascript code file +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] + +[application.plugins] + +available = true + +required = [] + +[bundler] +# Bundle identifier +identifier = "io.github.{{project-name}}" + +# Bundle publisher +publisher = "{{project-name}}" + +# Bundle icon +icon = ["icons/icon.png"] + +# Bundle resources +resources = ["public/*"] + +# Bundle copyright +copyright = "" + +# Bundle category +category = "Utility" + +# Bundle short description +short_description = "An amazing dioxus application." + +# Bundle long description +long_description = """ +An amazing dioxus application. +""" \ No newline at end of file diff --git a/packages/cli/src/cargo.rs b/packages/cli-config/src/cargo.rs similarity index 69% rename from packages/cli/src/cargo.rs rename to packages/cli-config/src/cargo.rs index b52fa4b09..97e4040db 100644 --- a/packages/cli/src/cargo.rs +++ b/packages/cli-config/src/cargo.rs @@ -1,12 +1,33 @@ //! Utilities for working with cargo and rust files -use crate::error::{Error, Result}; +use std::error::Error; use std::{ - env, fs, + env, + fmt::{Display, Formatter}, + fs, path::{Path, PathBuf}, process::Command, str, }; +#[derive(Debug, Clone)] +pub struct CargoError { + msg: String, +} + +impl CargoError { + pub fn new(msg: String) -> Self { + Self { msg } + } +} + +impl Display for CargoError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "CargoError: {}", self.msg) + } +} + +impl Error for CargoError {} + /// How many parent folders are searched for a `Cargo.toml` const MAX_ANCESTORS: u32 = 10; @@ -19,7 +40,7 @@ pub struct Metadata { /// Returns the root of the crate that the command is run from /// /// If the command is run from the workspace root, this will return the top-level Cargo.toml -pub fn crate_root() -> Result { +pub fn crate_root() -> Result { // From the current directory we work our way up, looking for `Cargo.toml` env::current_dir() .ok() @@ -35,7 +56,7 @@ pub fn crate_root() -> Result { None }) .ok_or_else(|| { - Error::CargoError("Failed to find directory containing Cargo.toml".to_string()) + CargoError::new("Failed to find directory containing Cargo.toml".to_string()) }) } @@ -53,11 +74,11 @@ fn contains_manifest(path: &Path) -> bool { impl Metadata { /// Returns the struct filled from `cargo metadata` output /// TODO @Jon, find a different way that doesn't rely on the cargo metadata command (it's slow) - pub fn get() -> Result { + pub fn get() -> Result { let output = Command::new("cargo") .args(["metadata"]) .output() - .map_err(|_| Error::CargoError("Manifset".to_string()))?; + .map_err(|_| CargoError::new("Manifset".to_string()))?; if !output.status.success() { let mut msg = str::from_utf8(&output.stderr).unwrap().trim(); @@ -65,22 +86,22 @@ impl Metadata { msg = &msg[7..]; } - return Err(Error::CargoError(msg.to_string())); + return Err(CargoError::new(msg.to_string())); } let stdout = str::from_utf8(&output.stdout).unwrap(); if let Some(line) = stdout.lines().next() { let meta: serde_json::Value = serde_json::from_str(line) - .map_err(|_| Error::CargoError("InvalidOutput".to_string()))?; + .map_err(|_| CargoError::new("InvalidOutput".to_string()))?; let workspace_root = meta["workspace_root"] .as_str() - .ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))? + .ok_or_else(|| CargoError::new("InvalidOutput".to_string()))? .into(); let target_directory = meta["target_directory"] .as_str() - .ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))? + .ok_or_else(|| CargoError::new("InvalidOutput".to_string()))? .into(); return Ok(Self { @@ -89,6 +110,6 @@ impl Metadata { }); } - Err(Error::CargoError("InvalidOutput".to_string())) + Err(CargoError::new("InvalidOutput".to_string())) } } diff --git a/packages/cli-config/src/config.rs b/packages/cli-config/src/config.rs new file mode 100644 index 000000000..f7ab89124 --- /dev/null +++ b/packages/cli-config/src/config.rs @@ -0,0 +1,658 @@ +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fmt::{Display, Formatter}, + path::{Path, PathBuf}, +}; + +use crate::CargoError; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] +pub enum Platform { + #[clap(name = "web")] + #[serde(rename = "web")] + Web, + #[clap(name = "desktop")] + #[serde(rename = "desktop")] + Desktop, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DioxusConfig { + pub application: ApplicationConfig, + + pub web: WebConfig, + + #[serde(default)] + pub bundle: BundleConfig, + + #[serde(default = "default_plugin")] + pub plugin: toml::Value, +} + +fn default_plugin() -> toml::Value { + toml::Value::Boolean(true) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoadDioxusConfigError { + location: String, + error: String, +} + +impl std::fmt::Display for LoadDioxusConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {}", self.location, self.error) + } +} + +impl std::error::Error for LoadDioxusConfigError {} + +impl DioxusConfig { + /// Load the dioxus config from a path + pub fn load(bin: Option) -> Result, CrateConfigError> { + let crate_dir = crate::cargo::crate_root(); + + let crate_dir = match crate_dir { + Ok(dir) => { + if let Some(bin) = bin { + dir.join(bin) + } else { + dir + } + } + Err(_) => return Ok(None), + }; + let crate_dir = crate_dir.as_path(); + + let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else { + return Ok(None); + }; + + let dioxus_conf_file = dioxus_conf_file.as_path(); + let cfg = toml::from_str::(&std::fs::read_to_string(dioxus_conf_file)?) + .map_err(|err| { + let error_location = dioxus_conf_file + .strip_prefix(crate_dir) + .unwrap_or(dioxus_conf_file) + .display(); + CrateConfigError::LoadDioxusConfig(LoadDioxusConfigError { + location: error_location.to_string(), + error: err.to_string(), + }) + }) + .map(Some); + match cfg { + Ok(Some(mut cfg)) => { + let name = cfg.application.name.clone(); + if cfg.bundle.identifier.is_none() { + cfg.bundle.identifier = Some(format!("io.github.{name}")); + } + if cfg.bundle.publisher.is_none() { + cfg.bundle.publisher = Some(name); + } + Ok(Some(cfg)) + } + cfg => cfg, + } + } +} + +fn acquire_dioxus_toml(dir: &Path) -> Option { + // prefer uppercase + let uppercase_conf = dir.join("Dioxus.toml"); + if uppercase_conf.is_file() { + return Some(uppercase_conf); + } + + // lowercase is fine too + let lowercase_conf = dir.join("dioxus.toml"); + if lowercase_conf.is_file() { + return Some(lowercase_conf); + } + + None +} + +impl Default for DioxusConfig { + fn default() -> Self { + let name = "name"; + Self { + application: ApplicationConfig { + name: name.into(), + default_platform: Platform::Web, + out_dir: Some(PathBuf::from("dist")), + asset_dir: Some(PathBuf::from("public")), + + tools: None, + + sub_package: None, + }, + web: WebConfig { + app: WebAppConfig { + title: Some("dioxus | ⛺".into()), + base_path: None, + }, + proxy: Some(vec![]), + watcher: WebWatcherConfig { + watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]), + reload_html: Some(false), + index_on_404: Some(true), + }, + resource: WebResourceConfig { + dev: WebDevResourceConfig { + style: Some(vec![]), + script: Some(vec![]), + }, + style: Some(vec![]), + script: Some(vec![]), + }, + https: WebHttpsConfig { + enabled: None, + mkcert: None, + key_path: None, + cert_path: None, + }, + }, + bundle: BundleConfig { + identifier: Some(format!("io.github.{name}")), + publisher: Some(name.into()), + ..Default::default() + }, + plugin: toml::Value::Table(toml::map::Map::new()), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApplicationConfig { + pub name: String, + pub default_platform: Platform, + pub out_dir: Option, + pub asset_dir: Option, + + pub tools: Option>, + + pub sub_package: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebConfig { + pub app: WebAppConfig, + pub proxy: Option>, + pub watcher: WebWatcherConfig, + pub resource: WebResourceConfig, + #[serde(default)] + pub https: WebHttpsConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebAppConfig { + pub title: Option, + pub base_path: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebProxyConfig { + pub backend: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebWatcherConfig { + pub watch_path: Option>, + pub reload_html: Option, + pub index_on_404: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebResourceConfig { + pub dev: WebDevResourceConfig, + pub style: Option>, + pub script: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebDevResourceConfig { + pub style: Option>, + pub script: Option>, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct WebHttpsConfig { + pub enabled: Option, + pub mkcert: Option, + pub key_path: Option, + pub cert_path: Option, +} + +#[derive(Debug, Clone)] +pub struct CrateConfig { + pub out_dir: PathBuf, + pub crate_dir: PathBuf, + pub workspace_dir: PathBuf, + pub target_dir: PathBuf, + pub asset_dir: PathBuf, + pub manifest: cargo_toml::Manifest, + pub executable: ExecutableType, + pub dioxus_config: DioxusConfig, + pub release: bool, + pub hot_reload: bool, + pub cross_origin_policy: bool, + pub verbose: bool, + pub custom_profile: Option, + pub features: Option>, +} + +#[derive(Debug)] +pub enum CrateConfigError { + Cargo(CargoError), + Io(std::io::Error), + Toml(toml::de::Error), + LoadDioxusConfig(LoadDioxusConfigError), +} + +impl From for CrateConfigError { + fn from(err: CargoError) -> Self { + Self::Cargo(err) + } +} + +impl From for CrateConfigError { + fn from(err: std::io::Error) -> Self { + Self::Io(err) + } +} + +impl From for CrateConfigError { + fn from(err: toml::de::Error) -> Self { + Self::Toml(err) + } +} + +impl From for CrateConfigError { + fn from(err: LoadDioxusConfigError) -> Self { + Self::LoadDioxusConfig(err) + } +} + +impl Display for CrateConfigError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Cargo(err) => write!(f, "{}", err), + Self::Io(err) => write!(f, "{}", err), + Self::Toml(err) => write!(f, "{}", err), + Self::LoadDioxusConfig(err) => write!(f, "{}", err), + } + } +} + +impl std::error::Error for CrateConfigError {} + +#[derive(Debug, Clone)] +pub enum ExecutableType { + Binary(String), + Lib(String), + Example(String), +} + +impl CrateConfig { + pub fn new(bin: Option) -> Result { + let dioxus_config = DioxusConfig::load(bin.clone())?.unwrap_or_default(); + + let crate_root = crate::cargo::crate_root()?; + + let crate_dir = if let Some(package) = &dioxus_config.application.sub_package { + crate_root.join(package) + } else if let Some(bin) = bin { + crate_root.join(bin) + } else { + crate_root + }; + + let meta = crate::cargo::Metadata::get()?; + let workspace_dir = meta.workspace_root; + let target_dir = meta.target_directory; + + let out_dir = match dioxus_config.application.out_dir { + Some(ref v) => crate_dir.join(v), + None => crate_dir.join("dist"), + }; + + let cargo_def = &crate_dir.join("Cargo.toml"); + + let asset_dir = match dioxus_config.application.asset_dir { + Some(ref v) => crate_dir.join(v), + None => crate_dir.join("public"), + }; + + let manifest = cargo_toml::Manifest::from_path(cargo_def).unwrap(); + + let mut output_filename = String::from("dioxus_app"); + if let Some(package) = &manifest.package.as_ref() { + output_filename = match &package.default_run { + Some(default_run_target) => default_run_target.to_owned(), + None => manifest + .bin + .iter() + .find(|b| b.name == manifest.package.as_ref().map(|pkg| pkg.name.clone())) + .or(manifest + .bin + .iter() + .find(|b| b.path == Some("src/main.rs".to_owned()))) + .or(manifest.bin.first()) + .or(manifest.lib.as_ref()) + .and_then(|prod| prod.name.clone()) + .unwrap_or(String::from("dioxus_app")), + }; + } + + let executable = ExecutableType::Binary(output_filename); + + let release = false; + let hot_reload = false; + let verbose = false; + let custom_profile = None; + let features = None; + + Ok(Self { + out_dir, + crate_dir, + workspace_dir, + target_dir, + asset_dir, + manifest, + executable, + release, + dioxus_config, + hot_reload, + cross_origin_policy: false, + custom_profile, + features, + verbose, + }) + } + + pub fn as_example(&mut self, example_name: String) -> &mut Self { + self.executable = ExecutableType::Example(example_name); + self + } + + pub fn with_release(&mut self, release: bool) -> &mut Self { + self.release = release; + self + } + + pub fn with_hot_reload(&mut self, hot_reload: bool) -> &mut Self { + self.hot_reload = hot_reload; + self + } + + pub fn with_cross_origin_policy(&mut self, cross_origin_policy: bool) -> &mut Self { + self.cross_origin_policy = cross_origin_policy; + self + } + + pub fn with_verbose(&mut self, verbose: bool) -> &mut Self { + self.verbose = verbose; + self + } + + pub fn set_profile(&mut self, profile: String) -> &mut Self { + self.custom_profile = Some(profile); + self + } + + pub fn set_features(&mut self, features: Vec) -> &mut Self { + self.features = Some(features); + self + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct BundleConfig { + pub identifier: Option, + pub publisher: Option, + pub icon: Option>, + pub resources: Option>, + pub copyright: Option, + pub category: Option, + pub short_description: Option, + pub long_description: Option, + pub external_bin: Option>, + pub deb: Option, + pub macos: Option, + pub windows: Option, +} + +impl From for tauri_bundler::BundleSettings { + fn from(val: BundleConfig) -> Self { + tauri_bundler::BundleSettings { + identifier: val.identifier, + publisher: val.publisher, + icon: val.icon, + resources: val.resources, + copyright: val.copyright, + category: val.category.and_then(|c| c.parse().ok()), + short_description: val.short_description, + long_description: val.long_description, + external_bin: val.external_bin, + deb: val.deb.map(Into::into).unwrap_or_default(), + macos: val.macos.map(Into::into).unwrap_or_default(), + windows: val.windows.map(Into::into).unwrap_or_default(), + ..Default::default() + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DebianSettings { + pub depends: Option>, + pub files: HashMap, + pub nsis: Option, +} + +impl From for tauri_bundler::DebianSettings { + fn from(val: DebianSettings) -> Self { + tauri_bundler::DebianSettings { + depends: val.depends, + files: val.files, + desktop_template: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct WixSettings { + pub language: Vec<(String, Option)>, + pub template: Option, + pub fragment_paths: Vec, + pub component_group_refs: Vec, + pub component_refs: Vec, + pub feature_group_refs: Vec, + pub feature_refs: Vec, + pub merge_refs: Vec, + pub skip_webview_install: bool, + pub license: Option, + pub enable_elevated_update_task: bool, + pub banner_path: Option, + pub dialog_image_path: Option, + pub fips_compliant: bool, +} + +impl From for tauri_bundler::WixSettings { + fn from(val: WixSettings) -> Self { + tauri_bundler::WixSettings { + language: tauri_bundler::bundle::WixLanguage({ + let mut languages: Vec<_> = val + .language + .iter() + .map(|l| { + ( + l.0.clone(), + tauri_bundler::bundle::WixLanguageConfig { + locale_path: l.1.clone(), + }, + ) + }) + .collect(); + if languages.is_empty() { + languages.push(("en-US".into(), Default::default())); + } + languages + }), + template: val.template, + fragment_paths: val.fragment_paths, + component_group_refs: val.component_group_refs, + component_refs: val.component_refs, + feature_group_refs: val.feature_group_refs, + feature_refs: val.feature_refs, + merge_refs: val.merge_refs, + skip_webview_install: val.skip_webview_install, + license: val.license, + enable_elevated_update_task: val.enable_elevated_update_task, + banner_path: val.banner_path, + dialog_image_path: val.dialog_image_path, + fips_compliant: val.fips_compliant, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MacOsSettings { + pub frameworks: Option>, + pub minimum_system_version: Option, + pub license: Option, + pub exception_domain: Option, + pub signing_identity: Option, + pub provider_short_name: Option, + pub entitlements: Option, + pub info_plist_path: Option, +} + +impl From for tauri_bundler::MacOsSettings { + fn from(val: MacOsSettings) -> Self { + tauri_bundler::MacOsSettings { + frameworks: val.frameworks, + minimum_system_version: val.minimum_system_version, + license: val.license, + exception_domain: val.exception_domain, + signing_identity: val.signing_identity, + provider_short_name: val.provider_short_name, + entitlements: val.entitlements, + info_plist_path: val.info_plist_path, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WindowsSettings { + pub digest_algorithm: Option, + pub certificate_thumbprint: Option, + pub timestamp_url: Option, + pub tsp: bool, + pub wix: Option, + pub icon_path: Option, + pub webview_install_mode: WebviewInstallMode, + pub webview_fixed_runtime_path: Option, + pub allow_downgrades: bool, + pub nsis: Option, +} + +impl From for tauri_bundler::WindowsSettings { + fn from(val: WindowsSettings) -> Self { + tauri_bundler::WindowsSettings { + digest_algorithm: val.digest_algorithm, + certificate_thumbprint: val.certificate_thumbprint, + timestamp_url: val.timestamp_url, + tsp: val.tsp, + wix: val.wix.map(Into::into), + icon_path: val.icon_path.unwrap_or("icons/icon.ico".into()), + webview_install_mode: val.webview_install_mode.into(), + webview_fixed_runtime_path: val.webview_fixed_runtime_path, + allow_downgrades: val.allow_downgrades, + nsis: val.nsis.map(Into::into), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NsisSettings { + pub template: Option, + pub license: Option, + pub header_image: Option, + pub sidebar_image: Option, + pub installer_icon: Option, + pub install_mode: NSISInstallerMode, + pub languages: Option>, + pub custom_language_files: Option>, + pub display_language_selector: bool, +} + +impl From for tauri_bundler::NsisSettings { + fn from(val: NsisSettings) -> Self { + tauri_bundler::NsisSettings { + license: val.license, + header_image: val.header_image, + sidebar_image: val.sidebar_image, + installer_icon: val.installer_icon, + install_mode: val.install_mode.into(), + languages: val.languages, + display_language_selector: val.display_language_selector, + custom_language_files: None, + template: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NSISInstallerMode { + CurrentUser, + PerMachine, + Both, +} + +impl From for tauri_utils::config::NSISInstallerMode { + fn from(val: NSISInstallerMode) -> Self { + match val { + NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser, + NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine, + NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum WebviewInstallMode { + Skip, + DownloadBootstrapper { silent: bool }, + EmbedBootstrapper { silent: bool }, + OfflineInstaller { silent: bool }, + FixedRuntime { path: PathBuf }, +} + +impl WebviewInstallMode { + fn into(self) -> tauri_utils::config::WebviewInstallMode { + match self { + Self::Skip => tauri_utils::config::WebviewInstallMode::Skip, + Self::DownloadBootstrapper { silent } => { + tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent } + } + Self::EmbedBootstrapper { silent } => { + tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent } + } + Self::OfflineInstaller { silent } => { + tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent } + } + Self::FixedRuntime { path } => { + tauri_utils::config::WebviewInstallMode::FixedRuntime { path } + } + } + } +} + +impl Default for WebviewInstallMode { + fn default() -> Self { + Self::OfflineInstaller { silent: false } + } +} diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs new file mode 100644 index 000000000..78531cf62 --- /dev/null +++ b/packages/cli-config/src/lib.rs @@ -0,0 +1,9 @@ +#![doc = include_str!("../README.md")] +#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] +#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] + +mod config; +pub use config::*; + +mod cargo; +pub use cargo::*; diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index adb0239f2..4eb9db09f 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -14,6 +14,7 @@ clap = { version = "4.2", features = ["derive"] } thiserror = { workspace = true } wasm-bindgen-cli-support = "0.2" colored = "2.0.0" +dioxus-cli-config = { workspace = true } # features log = "0.4.14" diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index c85cdc484..c38514513 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -5,6 +5,7 @@ use crate::{ DioxusConfig, }; use cargo_metadata::{diagnostic::Diagnostic, Message}; +use dioxus_cli_config::crate_root; use indicatif::{ProgressBar, ProgressStyle}; use serde::Serialize; use std::{ @@ -429,7 +430,7 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { } pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { - let crate_root = crate::cargo::crate_root().unwrap(); + let crate_root = crate_root().unwrap(); let custom_html_file = crate_root.join("index.html"); let mut html = if custom_html_file.is_file() { let mut buf = String::new(); diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 7fe29f8ce..7ecc286ba 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -1,6 +1,6 @@ -use crate::cfg::Platform; #[cfg(feature = "plugin")] use crate::plugin::PluginManager; +use dioxus_cli_config::Platform; use super::*; diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs index 2a441a1c8..53c64b6e2 100644 --- a/packages/cli/src/cli/cfg.rs +++ b/packages/cli/src/cli/cfg.rs @@ -1,5 +1,4 @@ -use clap::ValueEnum; -use serde::Serialize; +use dioxus_cli_config::Platform; use super::*; @@ -91,16 +90,6 @@ pub struct ConfigOptsServe { pub features: Option>, } -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] -pub enum Platform { - #[clap(name = "web")] - #[serde(rename = "web")] - Web, - #[clap(name = "desktop")] - #[serde(rename = "desktop")] - Desktop, -} - /// Config options for the bundling system. #[derive(Clone, Debug, Default, Deserialize, Parser)] pub struct ConfigOptsBundle { diff --git a/packages/cli/src/cli/config.rs b/packages/cli/src/cli/config.rs index 45fcc2f23..ab29f95b2 100644 --- a/packages/cli/src/cli/config.rs +++ b/packages/cli/src/cli/config.rs @@ -1,3 +1,5 @@ +use dioxus_cli_config::crate_root; + use super::*; /// Dioxus config file controls @@ -26,7 +28,7 @@ pub enum Config { impl Config { pub fn config(self) -> Result<()> { - let crate_root = crate::cargo::crate_root()?; + let crate_root = crate_root()?; match self { Config::Init { name, diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index 433d611b7..af22a17ca 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -1,3 +1,5 @@ +use dioxus_cli_config::Platform; + use super::*; use std::{fs::create_dir_all, io::Write, path::PathBuf}; @@ -40,7 +42,7 @@ impl Serve { .unwrap_or(crate_config.dioxus_config.application.default_platform); match platform { - cfg::Platform::Web => { + Platform::Web => { // generate dev-index page Serve::regen_dev_page(&crate_config)?; @@ -48,7 +50,7 @@ impl Serve { server::web::startup(self.serve.port, crate_config.clone(), self.serve.open) .await?; } - cfg::Platform::Desktop => { + Platform::Desktop => { server::desktop::startup(crate_config.clone()).await?; } } diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 3abf47f0c..842d3bad6 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -1,4 +1,5 @@ -use crate::{cfg::Platform, error::Result}; +use crate::error::Result; +use dioxus_cli_config::{crate_root, Metadata, Platform}; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, @@ -24,7 +25,7 @@ fn default_plugin() -> toml::Value { impl DioxusConfig { pub fn load(bin: Option) -> crate::error::Result> { - let crate_dir = crate::cargo::crate_root(); + let crate_dir = crate_root(); let crate_dir = match crate_dir { Ok(dir) => { @@ -224,7 +225,7 @@ impl CrateConfig { pub fn new(bin: Option) -> Result { let dioxus_config = DioxusConfig::load(bin.clone())?.unwrap_or_default(); - let crate_root = crate::cargo::crate_root()?; + let crate_root = crate_root()?; let crate_dir = if let Some(package) = &dioxus_config.application.sub_package { crate_root.join(package) @@ -234,7 +235,7 @@ impl CrateConfig { crate_root }; - let meta = crate::cargo::Metadata::get()?; + let meta = Metadata::get()?; let workspace_dir = meta.workspace_root; let target_dir = meta.target_directory; diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index 84b9d4b71..e8d6c91e4 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -69,6 +69,20 @@ impl From for Error { } } +impl From for Error { + fn from(e: dioxus_cli_config::LoadDioxusConfigError) -> Self { + Self::RuntimeError(e.to_string()) + } +} + +impl From for Error { + fn from(e: dioxus_cli_config::CargoError) -> Self { + Self::CargoError(e.to_string()) + } +} + + + #[macro_export] macro_rules! custom_error { ($msg:literal $(,)?) => { diff --git a/packages/cli/src/lib.rs b/packages/cli/src/lib.rs index b27cf55a9..9a87a2dfa 100644 --- a/packages/cli/src/lib.rs +++ b/packages/cli/src/lib.rs @@ -10,9 +10,6 @@ pub mod tools; pub use builder::*; -pub mod cargo; -pub use cargo::*; - pub mod cli; pub use cli::*; diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs index 71323dd29..a221e2f9b 100644 --- a/packages/cli/src/server/output.rs +++ b/packages/cli/src/server/output.rs @@ -1,6 +1,7 @@ use crate::server::Diagnostic; use crate::CrateConfig; use colored::Colorize; +use dioxus_cli_config::crate_root; use std::path::PathBuf; use std::process::Command; @@ -40,7 +41,7 @@ pub fn print_console_info( profile = config.custom_profile.as_ref().unwrap().to_string(); } let hot_reload = if config.hot_reload { "RSX" } else { "Normal" }; - let crate_root = crate::cargo::crate_root().unwrap(); + let crate_root = crate_root().unwrap(); let custom_html_file = if crate_root.join("index.html").is_file() { "Custom [index.html]" } else { From f1533781a4e0caed102f8e74ae836f37239bbb22 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Thu, 9 Nov 2023 00:38:22 +0530 Subject: [PATCH 037/126] add fn to convert hashmap to json --- packages/html/src/events/form.rs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index e3b35b098..6346a99e6 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -1,11 +1,12 @@ use std::{any::Any, collections::HashMap, fmt::Debug}; use dioxus_core::Event; -use serde::{Deserialize, Serialize}; +use serde::{de::Error, Deserialize, Serialize}; pub type FormEvent = Event; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] // this will serialize Text(String) -> String and VecText(Vec) to Vec enum ValueType { Text(String), VecText(Vec), @@ -30,13 +31,21 @@ pub struct FormData { pub files: Option>, } +fn convert_hashmap_to_json(hashmap: &HashMap) -> serde_json::Result +where + K: Serialize + std::hash::Hash + Eq, + V: Serialize, +{ + serde_json::to_string(hashmap) +} + impl FormData { // ***** function to parse the 'values' to make it ready to use******* // e.g - self.values = { username: ["rust"], password: ["dioxus"]} // what we need it to be: { username: "rust", password: "dioxus"} - fn get_parsed_values(&self) -> Option> { + fn get_parsed_values(&self) -> Result { if self.values.is_empty() { - return None; + return Err(serde_json::Error::custom("Values is empty")); } let raw_values = self.values.clone(); @@ -58,21 +67,24 @@ impl FormData { parsed_values.insert(fieldname, ValueType::VecText(values.clone())); } else { // handle single value - case 2 - let prepared_value = values.into_iter().next()?; + let prepared_value = values.into_iter().next().unwrap(); parsed_values.insert(fieldname, ValueType::Text(prepared_value)); } } - Some(parsed_values) + // convert HashMap to JSON string + convert_hashmap_to_json(&parsed_values) } pub fn parse_json(&self) -> Result where T: serde::de::DeserializeOwned, { - let formatted = self.get_parsed_values(); - let as_string = serde_json::to_string(&formatted)?; - serde_json::from_str(&as_string) + let parsed_json = self + .get_parsed_values() + .expect("Failed to parse values to JSON"); + + serde_json::from_str(&parsed_json) } } From 1a9d1eda19827c53da48a966d7418ad1f241466b Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Thu, 9 Nov 2023 00:41:50 +0530 Subject: [PATCH 038/126] optimize hashmap insertion --- packages/html/src/events/form.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 6346a99e6..1303dc209 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -62,14 +62,21 @@ impl FormData { In this case we would want to deserialize the value as follows favourite_language: "rust" */ - if values.len() > 1 { - // handling multiple values - case 1 - parsed_values.insert(fieldname, ValueType::VecText(values.clone())); - } else { - // handle single value - case 2 - let prepared_value = values.into_iter().next().unwrap(); - parsed_values.insert(fieldname, ValueType::Text(prepared_value)); - } + parsed_values.insert( + fieldname, + if values.len() > 1 { + // handling multiple values - case 1 + ValueType::VecText(values) + } else { + // handle single value - case 2 + ValueType::Text( + values + .into_iter() + .next() + .ok_or_else(|| serde_json::Error::custom("Values array is empty"))?, + ) + }, + ); } // convert HashMap to JSON string From 2cbbdc23c6bda33609cd10d85e0d3cc6450b7186 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Thu, 9 Nov 2023 11:04:47 +0530 Subject: [PATCH 039/126] add impl fn for FormData to get input types --- packages/html/src/events/form.rs | 15 ++++++++++++++- packages/rink/src/hooks.rs | 3 +++ packages/rink/src/widgets/button.rs | 1 + packages/rink/src/widgets/checkbox.rs | 1 + packages/rink/src/widgets/slider.rs | 1 + packages/rink/src/widgets/text_like.rs | 1 + packages/web/src/dom.rs | 18 +++++++++++++++++- 7 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 1303dc209..2e2fc5934 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -20,6 +20,8 @@ pub struct FormData { pub values: HashMap>, + pub value_types: HashMap, + #[cfg_attr( feature = "serialize", serde( @@ -53,6 +55,17 @@ impl FormData { let mut parsed_values: HashMap = HashMap::new(); for (fieldname, values) in raw_values.into_iter() { + // check if the fieldname can hold multiple values based on its types + let field_type = self + .value_types + .get(&fieldname) + .expect("Provided invalid field"); + + let is_multi_valued_input = match field_type.as_str() { + "select" | "checkbox" => true, + _ => false, + }; + /* case 1 - multiple values, example { driving_types: ["manual", "automatic"] } In this case we want to return the values as it is, NO point in making @@ -64,7 +77,7 @@ impl FormData { */ parsed_values.insert( fieldname, - if values.len() > 1 { + if is_multi_valued_input { // handling multiple values - case 1 ValueType::VecText(values) } else { diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index f8c4500d2..47efc9e7e 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -63,6 +63,8 @@ pub struct FormData { pub values: HashMap>, + pub value_types: HashMap, + pub files: Option, } @@ -71,6 +73,7 @@ impl FormData { dioxus_html::FormData { value: self.value, values: self.values, + value_types: self.value_types, files: None, } } diff --git a/packages/rink/src/widgets/button.rs b/packages/rink/src/widgets/button.rs index 523b2392a..41be40da5 100644 --- a/packages/rink/src/widgets/button.rs +++ b/packages/rink/src/widgets/button.rs @@ -93,6 +93,7 @@ impl Button { let data = FormData { value: self.value.to_string(), values: HashMap::new(), + value_types: HashMap::new(), files: None, }; ctx.send(crate::Event { diff --git a/packages/rink/src/widgets/checkbox.rs b/packages/rink/src/widgets/checkbox.rs index fab809ed2..ea5a0ba53 100644 --- a/packages/rink/src/widgets/checkbox.rs +++ b/packages/rink/src/widgets/checkbox.rs @@ -129,6 +129,7 @@ impl CheckBox { .then(|| self.value.to_string()) .unwrap_or_default(), values: HashMap::new(), + value_types: HashMap::new(), files: None, }; { diff --git a/packages/rink/src/widgets/slider.rs b/packages/rink/src/widgets/slider.rs index 7080b6c12..dd1b50a7b 100644 --- a/packages/rink/src/widgets/slider.rs +++ b/packages/rink/src/widgets/slider.rs @@ -198,6 +198,7 @@ impl Slider { let data = FormData { value: self.value.to_string(), values: HashMap::new(), + value_types: HashMap::new(), files: None, }; ctx.send(Event { diff --git a/packages/rink/src/widgets/text_like.rs b/packages/rink/src/widgets/text_like.rs index 6521073d0..c9dacb74e 100644 --- a/packages/rink/src/widgets/text_like.rs +++ b/packages/rink/src/widgets/text_like.rs @@ -171,6 +171,7 @@ impl TextLike { let data: FormData = FormData { value: self.text.clone(), values: HashMap::new(), + value_types: HashMap::new(), files: None, }; let ctx: UniqueView = world.borrow().expect("expected widget context"); diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index f45d6558a..556d9ab9a 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -15,7 +15,7 @@ use dioxus_interpreter_js::{get_node, minimal_bindings, save_template, Channel}; use futures_channel::mpsc; use js_sys::Array; use rustc_hash::FxHashMap; -use std::{any::Any, rc::Rc}; +use std::{any::Any, collections::HashMap, rc::Rc}; use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast, JsValue}; use web_sys::{Document, Element, Event}; @@ -365,6 +365,21 @@ fn read_input_to_data(target: Element) -> Rc { }) .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + let mut value_types = HashMap::new(); + + // to get the input_type for the corresponding input + for input_el in target.dyn_ref::().into_iter() { + for index in 0..input_el.length() { + if let Some(element) = input_el.get_with_index(index as u32) { + if let Some(input) = element.dyn_into::().ok() { + let name = input.name(); + let input_type = input.type_(); + value_types.insert(name, input_type); + } + } + } + } + let mut values = std::collections::HashMap::new(); // try to fill in form values @@ -400,6 +415,7 @@ fn read_input_to_data(target: Element) -> Rc { Rc::new(FormData { value, values, + value_types, files, }) } From 9f0e00029503ce0a3807ea37f2aa72c44da9e5af Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Thu, 9 Nov 2023 11:09:07 +0530 Subject: [PATCH 040/126] provide proper error message --- packages/html/src/events/form.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 2e2fc5934..e86f72167 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -47,7 +47,7 @@ impl FormData { // what we need it to be: { username: "rust", password: "dioxus"} fn get_parsed_values(&self) -> Result { if self.values.is_empty() { - return Err(serde_json::Error::custom("Values is empty")); + return Err(serde_json::Error::custom("Values array is empty")); } let raw_values = self.values.clone(); From 1b7017f67bcc491533ce3af71650b3236dc0c35b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 9 Nov 2023 08:36:14 -0600 Subject: [PATCH 041/126] fix formatting --- packages/cli-config/Cargo.toml | 4 ---- packages/cli/src/error.rs | 2 -- 2 files changed, 6 deletions(-) diff --git a/packages/cli-config/Cargo.toml b/packages/cli-config/Cargo.toml index d89d411fd..7c21f8025 100644 --- a/packages/cli-config/Cargo.toml +++ b/packages/cli-config/Cargo.toml @@ -88,10 +88,6 @@ interprocess-docfix = { version = "1.2.2" } default = [] plugin = ["mlua"] -[[bin]] -path = "src/main.rs" -name = "dx" - [dev-dependencies] tempfile = "3.3" diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index e8d6c91e4..0088492e8 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -81,8 +81,6 @@ impl From for Error { } } - - #[macro_export] macro_rules! custom_error { ($msg:literal $(,)?) => { From c6b39c05b704e7f0151eff00c5f6f774d7883702 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Fri, 10 Nov 2023 11:44:19 +0530 Subject: [PATCH 042/126] 1. remove value_types from FormData. 2. modify type of values to accept dynamic type. --- packages/html/src/events/form.rs | 69 +++----------------------- packages/rink/src/hooks.rs | 5 +- packages/rink/src/widgets/button.rs | 1 - packages/rink/src/widgets/slider.rs | 1 - packages/rink/src/widgets/text_like.rs | 1 - packages/web/src/dom.rs | 40 +++++++-------- 6 files changed, 26 insertions(+), 91 deletions(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index e86f72167..5bdf5124d 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -1,13 +1,13 @@ use std::{any::Any, collections::HashMap, fmt::Debug}; use dioxus_core::Event; -use serde::{de::Error, Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; pub type FormEvent = Event; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] // this will serialize Text(String) -> String and VecText(Vec) to Vec -enum ValueType { +pub enum ValueType { Text(String), VecText(Vec), } @@ -18,9 +18,7 @@ enum ValueType { pub struct FormData { pub value: String, - pub values: HashMap>, - - pub value_types: HashMap, + pub values: HashMap, #[cfg_attr( feature = "serialize", @@ -42,67 +40,12 @@ where } impl FormData { - // ***** function to parse the 'values' to make it ready to use******* - // e.g - self.values = { username: ["rust"], password: ["dioxus"]} - // what we need it to be: { username: "rust", password: "dioxus"} - fn get_parsed_values(&self) -> Result { - if self.values.is_empty() { - return Err(serde_json::Error::custom("Values array is empty")); - } - - let raw_values = self.values.clone(); - - let mut parsed_values: HashMap = HashMap::new(); - - for (fieldname, values) in raw_values.into_iter() { - // check if the fieldname can hold multiple values based on its types - let field_type = self - .value_types - .get(&fieldname) - .expect("Provided invalid field"); - - let is_multi_valued_input = match field_type.as_str() { - "select" | "checkbox" => true, - _ => false, - }; - - /* - case 1 - multiple values, example { driving_types: ["manual", "automatic"] } - In this case we want to return the values as it is, NO point in making - driving_types: "manual, automatic" - - case 2 - single value, example { favourite_language: ["rust"] } - In this case we would want to deserialize the value as follows - favourite_language: "rust" - */ - parsed_values.insert( - fieldname, - if is_multi_valued_input { - // handling multiple values - case 1 - ValueType::VecText(values) - } else { - // handle single value - case 2 - ValueType::Text( - values - .into_iter() - .next() - .ok_or_else(|| serde_json::Error::custom("Values array is empty"))?, - ) - }, - ); - } - - // convert HashMap to JSON string - convert_hashmap_to_json(&parsed_values) - } - pub fn parse_json(&self) -> Result where T: serde::de::DeserializeOwned, { - let parsed_json = self - .get_parsed_values() - .expect("Failed to parse values to JSON"); + let parsed_json = + convert_hashmap_to_json(&self.values.clone()).expect("Failed to parse values to JSON"); serde_json::from_str(&parsed_json) } diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index 47efc9e7e..f457de29f 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -61,9 +61,7 @@ impl EventData { pub struct FormData { pub value: String, - pub values: HashMap>, - - pub value_types: HashMap, + pub values: HashMap, pub files: Option, } @@ -73,7 +71,6 @@ impl FormData { dioxus_html::FormData { value: self.value, values: self.values, - value_types: self.value_types, files: None, } } diff --git a/packages/rink/src/widgets/button.rs b/packages/rink/src/widgets/button.rs index 41be40da5..523b2392a 100644 --- a/packages/rink/src/widgets/button.rs +++ b/packages/rink/src/widgets/button.rs @@ -93,7 +93,6 @@ impl Button { let data = FormData { value: self.value.to_string(), values: HashMap::new(), - value_types: HashMap::new(), files: None, }; ctx.send(crate::Event { diff --git a/packages/rink/src/widgets/slider.rs b/packages/rink/src/widgets/slider.rs index dd1b50a7b..7080b6c12 100644 --- a/packages/rink/src/widgets/slider.rs +++ b/packages/rink/src/widgets/slider.rs @@ -198,7 +198,6 @@ impl Slider { let data = FormData { value: self.value.to_string(), values: HashMap::new(), - value_types: HashMap::new(), files: None, }; ctx.send(Event { diff --git a/packages/rink/src/widgets/text_like.rs b/packages/rink/src/widgets/text_like.rs index c9dacb74e..6521073d0 100644 --- a/packages/rink/src/widgets/text_like.rs +++ b/packages/rink/src/widgets/text_like.rs @@ -171,7 +171,6 @@ impl TextLike { let data: FormData = FormData { value: self.text.clone(), values: HashMap::new(), - value_types: HashMap::new(), files: None, }; let ctx: UniqueView = world.borrow().expect("expected widget context"); diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 556d9ab9a..3188e0935 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -10,7 +10,7 @@ use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; -use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData}; +use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData, ValueType}; use dioxus_interpreter_js::{get_node, minimal_bindings, save_template, Channel}; use futures_channel::mpsc; use js_sys::Array; @@ -365,22 +365,7 @@ fn read_input_to_data(target: Element) -> Rc { }) .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); - let mut value_types = HashMap::new(); - - // to get the input_type for the corresponding input - for input_el in target.dyn_ref::().into_iter() { - for index in 0..input_el.length() { - if let Some(element) = input_el.get_with_index(index as u32) { - if let Some(input) = element.dyn_into::().ok() { - let name = input.name(); - let input_type = input.type_(); - value_types.insert(name, input_type); - } - } - } - } - - let mut values = std::collections::HashMap::new(); + let mut values = HashMap::new(); // try to fill in form values if let Some(form) = target.dyn_ref::() { @@ -389,10 +374,12 @@ fn read_input_to_data(target: Element) -> Rc { if let Ok(array) = value.dyn_into::() { if let Some(name) = array.get(0).as_string() { if let Ok(item_values) = array.get(1).dyn_into::() { - let item_values = + let item_values: Vec = item_values.iter().filter_map(|v| v.as_string()).collect(); - values.insert(name, item_values); + values.insert(name, ValueType::VecText(item_values)); + } else if let Ok(item_value) = array.get(1).dyn_into::() { + values.insert(name, ValueType::Text(item_value.as_string().unwrap())); } } } @@ -415,7 +402,6 @@ fn read_input_to_data(target: Element) -> Rc { Rc::new(FormData { value, values, - value_types, files, }) } @@ -424,10 +410,22 @@ fn read_input_to_data(target: Element) -> Rc { #[wasm_bindgen(inline_js = r#" export function get_form_data(form) { let values = new Map(); + const formData = new FormData(form); for (let name of formData.keys()) { - values.set(name, formData.getAll(name)); + const fieldType = form.elements[name].type; + + switch (fieldType) { + case "select-multiple": + values.set(name, formData.getAll(name)); + break; + + // add cases for fieldTypes that can hold multiple values here + default: + values.set(name, formData.get(name)); + break; + } } return values; From 6398af1d65fe10a0f4aefc831cdbe78947bf1de8 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Fri, 10 Nov 2023 23:23:26 +0530 Subject: [PATCH 043/126] code cleanup --- packages/rink/src/hooks.rs | 1 + packages/rink/src/widgets/checkbox.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index f457de29f..494d3224f 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -13,6 +13,7 @@ use dioxus_html::geometry::{ use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; +use dioxus_html::ValueType; use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, WheelData}; use std::any::Any; use std::collections::HashMap; diff --git a/packages/rink/src/widgets/checkbox.rs b/packages/rink/src/widgets/checkbox.rs index ea5a0ba53..fab809ed2 100644 --- a/packages/rink/src/widgets/checkbox.rs +++ b/packages/rink/src/widgets/checkbox.rs @@ -129,7 +129,6 @@ impl CheckBox { .then(|| self.value.to_string()) .unwrap_or_default(), values: HashMap::new(), - value_types: HashMap::new(), files: None, }; { From 2b459e6fc14a75aa3820905d9f5e35f6d69d8504 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Fri, 10 Nov 2023 23:32:41 +0530 Subject: [PATCH 044/126] 1. optimize import. 2. modify post event serialization. --- packages/interpreter/src/interpreter.js | 14 ++++++++++++-- packages/rink/src/hooks.rs | 3 +-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index a03983ffa..2a1841171 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -453,8 +453,18 @@ function handler(event, name, bubbles, config) { const formData = new FormData(target); for (let name of formData.keys()) { - let value = formData.getAll(name); - contents.values[name] = value; + const fieldType = form.elements[name].type; + + switch (fieldType) { + case "select-multiple": + contents.values[name] = formData.getAll(name); + break; + + // add cases for fieldTypes that can hold multiple values here + default: + contents.values[name] = formData.get(name); + break; + } } } diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index 494d3224f..8842ffe60 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -13,8 +13,7 @@ use dioxus_html::geometry::{ use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; -use dioxus_html::ValueType; -use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, WheelData}; +use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, ValueType, WheelData}; use std::any::Any; use std::collections::HashMap; use std::{ From 8183935f661cd489b3fa7e101c2ed147547912b3 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Sat, 11 Nov 2023 09:36:26 +0530 Subject: [PATCH 045/126] remove unwanted cloning while passing reference. --- packages/html/src/events/form.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 5bdf5124d..de2494a50 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -45,7 +45,7 @@ impl FormData { T: serde::de::DeserializeOwned, { let parsed_json = - convert_hashmap_to_json(&self.values.clone()).expect("Failed to parse values to JSON"); + convert_hashmap_to_json(&self.values).expect("Failed to parse values to JSON"); serde_json::from_str(&parsed_json) } From 48295b021c377ce53921d50c4521ba183b2de6d6 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Sat, 11 Nov 2023 21:27:44 +0530 Subject: [PATCH 046/126] remove optional config of serde_json --- packages/html/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 5644b440c..b8e91e5ac 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -23,7 +23,7 @@ serde-value = "0.7.0" tokio = { workspace = true, features = ["fs", "io-util"], optional = true } rfd = { version = "0.11.3", optional = true } async-channel = "1.8.0" -serde_json = { version = "1", optional = true } +serde_json = { version = "1" } [dependencies.web-sys] optional = true From 13a82a370500ad0c7f498618d20bd5a3b6f133f6 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Sat, 11 Nov 2023 21:55:08 +0530 Subject: [PATCH 047/126] remove serde and serde_json from serialize dependency. --- packages/html/Cargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index b8e91e5ac..0d9085f6a 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-core = { workspace = true } dioxus-rsx = { workspace = true, features = ["hot_reload"], optional = true } -serde = { version = "1", features = ["derive"], optional = true } +serde = { version = "1", features = ["derive"] } serde_repr = { version = "0.1", optional = true } wasm-bindgen = { workspace = true, optional = true } euclid = "0.22.7" @@ -50,9 +50,7 @@ serde_json = "1" [features] default = ["serialize", "mounted"] serialize = [ - "serde", "serde_repr", - "serde_json", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize", From a74e6d82c73338cf9f5d05ecd47d1f19e86d0a14 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Sun, 12 Nov 2023 13:09:51 +0530 Subject: [PATCH 048/126] rename parse_json to parsed_values --- packages/html/src/events/form.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index de2494a50..d48c20370 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -40,7 +40,7 @@ where } impl FormData { - pub fn parse_json(&self) -> Result + pub fn parsed_values(&self) -> Result where T: serde::de::DeserializeOwned, { From 67670875bba323829dc9f9097700b07dffc66ee8 Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Tue, 14 Nov 2023 01:13:47 +0530 Subject: [PATCH 049/126] fix parsing issue for desktop platform --- packages/interpreter/src/interpreter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 2a1841171..7f55c2da3 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -453,7 +453,7 @@ function handler(event, name, bubbles, config) { const formData = new FormData(target); for (let name of formData.keys()) { - const fieldType = form.elements[name].type; + const fieldType = target.elements[name].type; switch (fieldType) { case "select-multiple": From 20bbf959797f4d6ebdc2e76e2b61ddba2cbfd147 Mon Sep 17 00:00:00 2001 From: ealmloff Date: Tue, 14 Nov 2023 10:45:53 -0600 Subject: [PATCH 050/126] document parsed_values and FormValue --- packages/html/src/events/form.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index d48c20370..1f8127f3b 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -5,9 +5,10 @@ use serde::{Deserialize, Serialize}; pub type FormEvent = Event; +/// A form value that may either be a list of values or a single value #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] // this will serialize Text(String) -> String and VecText(Vec) to Vec -pub enum ValueType { +pub enum FormValue { Text(String), VecText(Vec), } @@ -18,7 +19,7 @@ pub enum ValueType { pub struct FormData { pub value: String, - pub values: HashMap, + pub values: HashMap, #[cfg_attr( feature = "serialize", @@ -40,6 +41,7 @@ where } impl FormData { + /// Parse the values into a struct with one field per value pub fn parsed_values(&self) -> Result where T: serde::de::DeserializeOwned, From 4631050a043da826f5e35f841cf326c63a1f9be1 Mon Sep 17 00:00:00 2001 From: ealmloff Date: Tue, 14 Nov 2023 10:52:39 -0600 Subject: [PATCH 051/126] fix FormValue --- packages/rink/src/hooks.rs | 4 ++-- packages/web/src/dom.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index 8842ffe60..109b39dd7 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -13,7 +13,7 @@ use dioxus_html::geometry::{ use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; -use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, ValueType, WheelData}; +use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, FormValue, WheelData}; use std::any::Any; use std::collections::HashMap; use std::{ @@ -61,7 +61,7 @@ impl EventData { pub struct FormData { pub value: String, - pub values: HashMap, + pub values: HashMap, pub files: Option, } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 3188e0935..1f3cecb0f 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -10,7 +10,7 @@ use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; -use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData, ValueType}; +use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData, FormValue}; use dioxus_interpreter_js::{get_node, minimal_bindings, save_template, Channel}; use futures_channel::mpsc; use js_sys::Array; @@ -377,9 +377,9 @@ fn read_input_to_data(target: Element) -> Rc { let item_values: Vec = item_values.iter().filter_map(|v| v.as_string()).collect(); - values.insert(name, ValueType::VecText(item_values)); + values.insert(name, FormValue::VecText(item_values)); } else if let Ok(item_value) = array.get(1).dyn_into::() { - values.insert(name, ValueType::Text(item_value.as_string().unwrap())); + values.insert(name, FormValue::Text(item_value.as_string().unwrap())); } } } From cf77325f678f1a598b5a2642cd71535f7b3d487a Mon Sep 17 00:00:00 2001 From: Bunny Bites Date: Tue, 14 Nov 2023 23:27:50 +0530 Subject: [PATCH 052/126] fix code format issues (pipeline) --- packages/rink/src/hooks.rs | 2 +- packages/web/src/dom.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index 109b39dd7..96d6c9f62 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -13,7 +13,7 @@ use dioxus_html::geometry::{ use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; -use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, FormValue, WheelData}; +use dioxus_html::{event_bubbles, FocusData, FormValue, KeyboardData, MouseData, WheelData}; use std::any::Any; use std::collections::HashMap; use std::{ diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 1f3cecb0f..d9fec9203 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -10,7 +10,7 @@ use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; -use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData, FormValue}; +use dioxus_html::{event_bubbles, CompositionData, FormData, FormValue, MountedData}; use dioxus_interpreter_js::{get_node, minimal_bindings, save_template, Channel}; use futures_channel::mpsc; use js_sys::Array; From 06be18a5913f208aac45800655f90a07457f215c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 14 Nov 2023 15:34:06 -0600 Subject: [PATCH 053/126] export lazy current config --- packages/cli-config/Cargo.toml | 81 +--- packages/cli-config/src/bundle.rs | 258 ++++++++++ packages/cli-config/src/config.rs | 458 +++++------------- packages/cli-config/src/lib.rs | 46 +- packages/cli/Cargo.toml | 2 +- packages/cli/src/builder.rs | 23 +- packages/cli/src/cli/autoformat.rs | 2 +- packages/cli/src/cli/build.rs | 2 +- packages/cli/src/cli/bundle.rs | 9 +- packages/cli/src/cli/check.rs | 2 +- packages/cli/src/cli/clean.rs | 2 +- packages/cli/src/cli/config.rs | 5 +- packages/cli/src/cli/mod.rs | 3 +- packages/cli/src/cli/serve.rs | 2 +- packages/cli/src/config.rs | 628 ------------------------- packages/cli/src/error.rs | 6 + packages/cli/src/lib.rs | 3 - packages/cli/src/server/desktop/mod.rs | 10 +- packages/cli/src/server/mod.rs | 3 +- packages/cli/src/server/output.rs | 2 +- packages/cli/src/server/web/mod.rs | 4 +- packages/cli/src/server/web/proxy.rs | 3 +- 22 files changed, 486 insertions(+), 1068 deletions(-) create mode 100644 packages/cli-config/src/bundle.rs delete mode 100644 packages/cli/src/config.rs diff --git a/packages/cli-config/Cargo.toml b/packages/cli-config/Cargo.toml index 7c21f8025..2ef7ce8e6 100644 --- a/packages/cli-config/Cargo.toml +++ b/packages/cli-config/Cargo.toml @@ -9,90 +9,17 @@ license = "MIT OR Apache-2.0" keywords = ["react", "gui", "cli", "dioxus", "wasm"] [dependencies] -# cli core clap = { version = "4.2", features = ["derive"] } -thiserror = { workspace = true } -wasm-bindgen-cli-support = "0.2" -colored = "2.0.0" - -# features -log = "0.4.14" -fern = { version = "0.6.0", features = ["colored"] } serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" toml = "0.5.8" -fs_extra = "1.2.0" cargo_toml = "0.16.0" -futures = "0.3.21" -notify = { version = "5.0.0-pre.16", features = ["serde"] } -html_parser = { workspace = true } -cargo_metadata = "0.15.0" -tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] } -atty = "0.2.14" -chrono = "0.4.19" -anyhow = "1.0.53" -hyper = "0.14.17" -hyper-rustls = "0.23.2" -indicatif = "0.17.5" -subprocess = "0.2.9" - -axum = { version = "0.5.1", features = ["ws", "headers"] } -axum-server = { version = "0.5.1", features = ["tls-rustls"] } -tower-http = { version = "0.2.2", features = ["full"] } -headers = "0.3.7" - -walkdir = "2" - -# tools download -dirs = "4.0.0" -reqwest = { version = "0.11", features = [ - "rustls-tls", - "stream", - "trust-dns", - "blocking", -] } -flate2 = "1.0.22" -tar = "0.4.38" -zip = "0.6.2" -tower = "0.4.12" -syn = { version = "2.0", features = ["full", "extra-traits"] } -lazy_static = "1.4.0" - -# plugin packages -mlua = { version = "0.8.1", features = [ - "lua54", - "vendored", - "async", - "send", - "macros", -], optional = true } -ctrlc = "3.2.3" -open = "4.1.0" -cargo-generate = "0.18" -toml_edit = "0.19.11" # bundling -tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] } -tauri-utils = "=1.4.*" - -dioxus-autofmt = { workspace = true } -dioxus-check = { workspace = true } -rsx-rosetta = { workspace = true } -dioxus-rsx = { workspace = true } -dioxus-html = { workspace = true, features = ["hot-reload-context"] } -dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-hot-reload = { workspace = true } -interprocess-docfix = { version = "1.2.2" } +tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"], optional = true } +tauri-utils = { version = "=1.4.*", optional = true } +once_cell = "1.18.0" [features] default = [] -plugin = ["mlua"] - -[dev-dependencies] -tempfile = "3.3" - -[package.metadata.binstall] -pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }" - -[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] -pkg-fmt = "zip" +cli = ["tauri-bundler", "tauri-utils"] diff --git a/packages/cli-config/src/bundle.rs b/packages/cli-config/src/bundle.rs new file mode 100644 index 000000000..cdae4539b --- /dev/null +++ b/packages/cli-config/src/bundle.rs @@ -0,0 +1,258 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct BundleConfig { + pub identifier: Option, + pub publisher: Option, + pub icon: Option>, + pub resources: Option>, + pub copyright: Option, + pub category: Option, + pub short_description: Option, + pub long_description: Option, + pub external_bin: Option>, + pub deb: Option, + pub macos: Option, + pub windows: Option, +} + +#[cfg(feature = "cli")] +impl From for tauri_bundler::BundleSettings { + fn from(val: BundleConfig) -> Self { + tauri_bundler::BundleSettings { + identifier: val.identifier, + publisher: val.publisher, + icon: val.icon, + resources: val.resources, + copyright: val.copyright, + category: val.category.and_then(|c| c.parse().ok()), + short_description: val.short_description, + long_description: val.long_description, + external_bin: val.external_bin, + deb: val.deb.map(Into::into).unwrap_or_default(), + macos: val.macos.map(Into::into).unwrap_or_default(), + windows: val.windows.map(Into::into).unwrap_or_default(), + ..Default::default() + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DebianSettings { + pub depends: Option>, + pub files: HashMap, + pub nsis: Option, +} + +#[cfg(feature = "cli")] +impl From for tauri_bundler::DebianSettings { + fn from(val: DebianSettings) -> Self { + tauri_bundler::DebianSettings { + depends: val.depends, + files: val.files, + desktop_template: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct WixSettings { + pub language: Vec<(String, Option)>, + pub template: Option, + pub fragment_paths: Vec, + pub component_group_refs: Vec, + pub component_refs: Vec, + pub feature_group_refs: Vec, + pub feature_refs: Vec, + pub merge_refs: Vec, + pub skip_webview_install: bool, + pub license: Option, + pub enable_elevated_update_task: bool, + pub banner_path: Option, + pub dialog_image_path: Option, + pub fips_compliant: bool, +} + +#[cfg(feature = "cli")] +impl From for tauri_bundler::WixSettings { + fn from(val: WixSettings) -> Self { + tauri_bundler::WixSettings { + language: tauri_bundler::bundle::WixLanguage({ + let mut languages: Vec<_> = val + .language + .iter() + .map(|l| { + ( + l.0.clone(), + tauri_bundler::bundle::WixLanguageConfig { + locale_path: l.1.clone(), + }, + ) + }) + .collect(); + if languages.is_empty() { + languages.push(("en-US".into(), Default::default())); + } + languages + }), + template: val.template, + fragment_paths: val.fragment_paths, + component_group_refs: val.component_group_refs, + component_refs: val.component_refs, + feature_group_refs: val.feature_group_refs, + feature_refs: val.feature_refs, + merge_refs: val.merge_refs, + skip_webview_install: val.skip_webview_install, + license: val.license, + enable_elevated_update_task: val.enable_elevated_update_task, + banner_path: val.banner_path, + dialog_image_path: val.dialog_image_path, + fips_compliant: val.fips_compliant, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MacOsSettings { + pub frameworks: Option>, + pub minimum_system_version: Option, + pub license: Option, + pub exception_domain: Option, + pub signing_identity: Option, + pub provider_short_name: Option, + pub entitlements: Option, + pub info_plist_path: Option, +} + +#[cfg(feature = "cli")] +impl From for tauri_bundler::MacOsSettings { + fn from(val: MacOsSettings) -> Self { + tauri_bundler::MacOsSettings { + frameworks: val.frameworks, + minimum_system_version: val.minimum_system_version, + license: val.license, + exception_domain: val.exception_domain, + signing_identity: val.signing_identity, + provider_short_name: val.provider_short_name, + entitlements: val.entitlements, + info_plist_path: val.info_plist_path, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WindowsSettings { + pub digest_algorithm: Option, + pub certificate_thumbprint: Option, + pub timestamp_url: Option, + pub tsp: bool, + pub wix: Option, + pub icon_path: Option, + pub webview_install_mode: WebviewInstallMode, + pub webview_fixed_runtime_path: Option, + pub allow_downgrades: bool, + pub nsis: Option, +} + +#[cfg(feature = "cli")] +impl From for tauri_bundler::WindowsSettings { + fn from(val: WindowsSettings) -> Self { + tauri_bundler::WindowsSettings { + digest_algorithm: val.digest_algorithm, + certificate_thumbprint: val.certificate_thumbprint, + timestamp_url: val.timestamp_url, + tsp: val.tsp, + wix: val.wix.map(Into::into), + icon_path: val.icon_path.unwrap_or("icons/icon.ico".into()), + webview_install_mode: val.webview_install_mode.into(), + webview_fixed_runtime_path: val.webview_fixed_runtime_path, + allow_downgrades: val.allow_downgrades, + nsis: val.nsis.map(Into::into), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NsisSettings { + pub template: Option, + pub license: Option, + pub header_image: Option, + pub sidebar_image: Option, + pub installer_icon: Option, + pub install_mode: NSISInstallerMode, + pub languages: Option>, + pub custom_language_files: Option>, + pub display_language_selector: bool, +} + +#[cfg(feature = "cli")] +impl From for tauri_bundler::NsisSettings { + fn from(val: NsisSettings) -> Self { + tauri_bundler::NsisSettings { + license: val.license, + header_image: val.header_image, + sidebar_image: val.sidebar_image, + installer_icon: val.installer_icon, + install_mode: val.install_mode.into(), + languages: val.languages, + display_language_selector: val.display_language_selector, + custom_language_files: None, + template: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NSISInstallerMode { + CurrentUser, + PerMachine, + Both, +} + +#[cfg(feature = "cli")] +impl From for tauri_utils::config::NSISInstallerMode { + fn from(val: NSISInstallerMode) -> Self { + match val { + NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser, + NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine, + NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum WebviewInstallMode { + Skip, + DownloadBootstrapper { silent: bool }, + EmbedBootstrapper { silent: bool }, + OfflineInstaller { silent: bool }, + FixedRuntime { path: PathBuf }, +} + +impl WebviewInstallMode { + fn into(self) -> tauri_utils::config::WebviewInstallMode { + match self { + Self::Skip => tauri_utils::config::WebviewInstallMode::Skip, + Self::DownloadBootstrapper { silent } => { + tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent } + } + Self::EmbedBootstrapper { silent } => { + tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent } + } + Self::OfflineInstaller { silent } => { + tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent } + } + Self::FixedRuntime { path } => { + tauri_utils::config::WebviewInstallMode::FixedRuntime { path } + } + } + } +} + +impl Default for WebviewInstallMode { + fn default() -> Self { + Self::OfflineInstaller { silent: false } + } +} diff --git a/packages/cli-config/src/config.rs b/packages/cli-config/src/config.rs index f7ab89124..fb3afb4ac 100644 --- a/packages/cli-config/src/config.rs +++ b/packages/cli-config/src/config.rs @@ -1,13 +1,14 @@ +use crate::BundleConfig; +use crate::CargoError; +use crate::{crate_root, Metadata}; use clap::ValueEnum; +use core::fmt::{Display, Formatter}; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, - fmt::{Display, Formatter}, path::{Path, PathBuf}, }; -use crate::CargoError; - #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] pub enum Platform { #[clap(name = "web")] @@ -49,6 +50,51 @@ impl std::fmt::Display for LoadDioxusConfigError { impl std::error::Error for LoadDioxusConfigError {} +#[derive(Debug)] +pub enum CrateConfigError { + Cargo(CargoError), + Io(std::io::Error), + Toml(toml::de::Error), + LoadDioxusConfig(LoadDioxusConfigError), +} + +impl From for CrateConfigError { + fn from(err: CargoError) -> Self { + Self::Cargo(err) + } +} + +impl From for CrateConfigError { + fn from(err: std::io::Error) -> Self { + Self::Io(err) + } +} + +impl From for CrateConfigError { + fn from(err: toml::de::Error) -> Self { + Self::Toml(err) + } +} + +impl From for CrateConfigError { + fn from(err: LoadDioxusConfigError) -> Self { + Self::LoadDioxusConfig(err) + } +} + +impl Display for CrateConfigError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Cargo(err) => write!(f, "{}", err), + Self::Io(err) => write!(f, "{}", err), + Self::Toml(err) => write!(f, "{}", err), + Self::LoadDioxusConfig(err) => write!(f, "{}", err), + } + } +} + +impl std::error::Error for CrateConfigError {} + impl DioxusConfig { /// Load the dioxus config from a path pub fn load(bin: Option) -> Result, CrateConfigError> { @@ -117,33 +163,33 @@ fn acquire_dioxus_toml(dir: &Path) -> Option { impl Default for DioxusConfig { fn default() -> Self { - let name = "name"; + let name = default_name(); Self { application: ApplicationConfig { - name: name.into(), - default_platform: Platform::Web, - out_dir: Some(PathBuf::from("dist")), - asset_dir: Some(PathBuf::from("public")), + name: name.clone(), + default_platform: default_platform(), + out_dir: out_dir_default(), + asset_dir: asset_dir_default(), - tools: None, + tools: Default::default(), sub_package: None, }, web: WebConfig { app: WebAppConfig { - title: Some("dioxus | ⛺".into()), + title: default_title(), base_path: None, }, - proxy: Some(vec![]), + proxy: vec![], watcher: WebWatcherConfig { - watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]), - reload_html: Some(false), - index_on_404: Some(true), + watch_path: watch_path_default(), + reload_html: false, + index_on_404: true, }, resource: WebResourceConfig { dev: WebDevResourceConfig { - style: Some(vec![]), - script: Some(vec![]), + style: vec![], + script: vec![], }, style: Some(vec![]), script: Some(vec![]), @@ -157,7 +203,7 @@ impl Default for DioxusConfig { }, bundle: BundleConfig { identifier: Some(format!("io.github.{name}")), - publisher: Some(name.into()), + publisher: Some(name), ..Default::default() }, plugin: toml::Value::Table(toml::map::Map::new()), @@ -167,20 +213,44 @@ impl Default for DioxusConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApplicationConfig { + #[serde(default = "default_name")] pub name: String, + #[serde(default = "default_platform")] pub default_platform: Platform, - pub out_dir: Option, - pub asset_dir: Option, + #[serde(default = "out_dir_default")] + pub out_dir: PathBuf, + #[serde(default = "asset_dir_default")] + pub asset_dir: PathBuf, - pub tools: Option>, + #[serde(default)] + pub tools: HashMap, + #[serde(default)] pub sub_package: Option, } +fn default_name() -> String { + "name".into() +} + +fn default_platform() -> Platform { + Platform::Web +} + +fn asset_dir_default() -> PathBuf { + PathBuf::from("public") +} + +fn out_dir_default() -> PathBuf { + PathBuf::from("dist") +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebConfig { + #[serde(default)] pub app: WebAppConfig, - pub proxy: Option>, + #[serde(default)] + pub proxy: Vec, pub watcher: WebWatcherConfig, pub resource: WebResourceConfig, #[serde(default)] @@ -189,10 +259,24 @@ pub struct WebConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebAppConfig { - pub title: Option, + #[serde(default = "default_title")] + pub title: String, pub base_path: Option, } +impl Default for WebAppConfig { + fn default() -> Self { + Self { + title: default_title(), + base_path: None, + } + } +} + +fn default_title() -> String { + "dioxus | ⛺".into() +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebProxyConfig { pub backend: String, @@ -200,9 +284,16 @@ pub struct WebProxyConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebWatcherConfig { - pub watch_path: Option>, - pub reload_html: Option, - pub index_on_404: Option, + #[serde(default = "watch_path_default")] + pub watch_path: Vec, + #[serde(default)] + pub reload_html: bool, + #[serde(default = "true_bool")] + pub index_on_404: bool, +} + +fn watch_path_default() -> Vec { + vec![PathBuf::from("src"), PathBuf::from("examples")] } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -214,8 +305,10 @@ pub struct WebResourceConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebDevResourceConfig { - pub style: Option>, - pub script: Option>, + #[serde(default)] + pub style: Vec, + #[serde(default)] + pub script: Vec, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -226,7 +319,7 @@ pub struct WebHttpsConfig { pub cert_path: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct CrateConfig { pub out_dir: PathBuf, pub crate_dir: PathBuf, @@ -244,52 +337,7 @@ pub struct CrateConfig { pub features: Option>, } -#[derive(Debug)] -pub enum CrateConfigError { - Cargo(CargoError), - Io(std::io::Error), - Toml(toml::de::Error), - LoadDioxusConfig(LoadDioxusConfigError), -} - -impl From for CrateConfigError { - fn from(err: CargoError) -> Self { - Self::Cargo(err) - } -} - -impl From for CrateConfigError { - fn from(err: std::io::Error) -> Self { - Self::Io(err) - } -} - -impl From for CrateConfigError { - fn from(err: toml::de::Error) -> Self { - Self::Toml(err) - } -} - -impl From for CrateConfigError { - fn from(err: LoadDioxusConfigError) -> Self { - Self::LoadDioxusConfig(err) - } -} - -impl Display for CrateConfigError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Cargo(err) => write!(f, "{}", err), - Self::Io(err) => write!(f, "{}", err), - Self::Toml(err) => write!(f, "{}", err), - Self::LoadDioxusConfig(err) => write!(f, "{}", err), - } - } -} - -impl std::error::Error for CrateConfigError {} - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum ExecutableType { Binary(String), Lib(String), @@ -300,7 +348,7 @@ impl CrateConfig { pub fn new(bin: Option) -> Result { let dioxus_config = DioxusConfig::load(bin.clone())?.unwrap_or_default(); - let crate_root = crate::cargo::crate_root()?; + let crate_root = crate_root()?; let crate_dir = if let Some(package) = &dioxus_config.application.sub_package { crate_root.join(package) @@ -310,21 +358,15 @@ impl CrateConfig { crate_root }; - let meta = crate::cargo::Metadata::get()?; + let meta = Metadata::get()?; let workspace_dir = meta.workspace_root; let target_dir = meta.target_directory; - let out_dir = match dioxus_config.application.out_dir { - Some(ref v) => crate_dir.join(v), - None => crate_dir.join("dist"), - }; + let out_dir = crate_dir.join(&dioxus_config.application.out_dir); let cargo_def = &crate_dir.join("Cargo.toml"); - let asset_dir = match dioxus_config.application.asset_dir { - Some(ref v) => crate_dir.join(v), - None => crate_dir.join("public"), - }; + let asset_dir = crate_dir.join(&dioxus_config.application.asset_dir); let manifest = cargo_toml::Manifest::from_path(cargo_def).unwrap(); @@ -409,250 +451,6 @@ impl CrateConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct BundleConfig { - pub identifier: Option, - pub publisher: Option, - pub icon: Option>, - pub resources: Option>, - pub copyright: Option, - pub category: Option, - pub short_description: Option, - pub long_description: Option, - pub external_bin: Option>, - pub deb: Option, - pub macos: Option, - pub windows: Option, -} - -impl From for tauri_bundler::BundleSettings { - fn from(val: BundleConfig) -> Self { - tauri_bundler::BundleSettings { - identifier: val.identifier, - publisher: val.publisher, - icon: val.icon, - resources: val.resources, - copyright: val.copyright, - category: val.category.and_then(|c| c.parse().ok()), - short_description: val.short_description, - long_description: val.long_description, - external_bin: val.external_bin, - deb: val.deb.map(Into::into).unwrap_or_default(), - macos: val.macos.map(Into::into).unwrap_or_default(), - windows: val.windows.map(Into::into).unwrap_or_default(), - ..Default::default() - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct DebianSettings { - pub depends: Option>, - pub files: HashMap, - pub nsis: Option, -} - -impl From for tauri_bundler::DebianSettings { - fn from(val: DebianSettings) -> Self { - tauri_bundler::DebianSettings { - depends: val.depends, - files: val.files, - desktop_template: None, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct WixSettings { - pub language: Vec<(String, Option)>, - pub template: Option, - pub fragment_paths: Vec, - pub component_group_refs: Vec, - pub component_refs: Vec, - pub feature_group_refs: Vec, - pub feature_refs: Vec, - pub merge_refs: Vec, - pub skip_webview_install: bool, - pub license: Option, - pub enable_elevated_update_task: bool, - pub banner_path: Option, - pub dialog_image_path: Option, - pub fips_compliant: bool, -} - -impl From for tauri_bundler::WixSettings { - fn from(val: WixSettings) -> Self { - tauri_bundler::WixSettings { - language: tauri_bundler::bundle::WixLanguage({ - let mut languages: Vec<_> = val - .language - .iter() - .map(|l| { - ( - l.0.clone(), - tauri_bundler::bundle::WixLanguageConfig { - locale_path: l.1.clone(), - }, - ) - }) - .collect(); - if languages.is_empty() { - languages.push(("en-US".into(), Default::default())); - } - languages - }), - template: val.template, - fragment_paths: val.fragment_paths, - component_group_refs: val.component_group_refs, - component_refs: val.component_refs, - feature_group_refs: val.feature_group_refs, - feature_refs: val.feature_refs, - merge_refs: val.merge_refs, - skip_webview_install: val.skip_webview_install, - license: val.license, - enable_elevated_update_task: val.enable_elevated_update_task, - banner_path: val.banner_path, - dialog_image_path: val.dialog_image_path, - fips_compliant: val.fips_compliant, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct MacOsSettings { - pub frameworks: Option>, - pub minimum_system_version: Option, - pub license: Option, - pub exception_domain: Option, - pub signing_identity: Option, - pub provider_short_name: Option, - pub entitlements: Option, - pub info_plist_path: Option, -} - -impl From for tauri_bundler::MacOsSettings { - fn from(val: MacOsSettings) -> Self { - tauri_bundler::MacOsSettings { - frameworks: val.frameworks, - minimum_system_version: val.minimum_system_version, - license: val.license, - exception_domain: val.exception_domain, - signing_identity: val.signing_identity, - provider_short_name: val.provider_short_name, - entitlements: val.entitlements, - info_plist_path: val.info_plist_path, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WindowsSettings { - pub digest_algorithm: Option, - pub certificate_thumbprint: Option, - pub timestamp_url: Option, - pub tsp: bool, - pub wix: Option, - pub icon_path: Option, - pub webview_install_mode: WebviewInstallMode, - pub webview_fixed_runtime_path: Option, - pub allow_downgrades: bool, - pub nsis: Option, -} - -impl From for tauri_bundler::WindowsSettings { - fn from(val: WindowsSettings) -> Self { - tauri_bundler::WindowsSettings { - digest_algorithm: val.digest_algorithm, - certificate_thumbprint: val.certificate_thumbprint, - timestamp_url: val.timestamp_url, - tsp: val.tsp, - wix: val.wix.map(Into::into), - icon_path: val.icon_path.unwrap_or("icons/icon.ico".into()), - webview_install_mode: val.webview_install_mode.into(), - webview_fixed_runtime_path: val.webview_fixed_runtime_path, - allow_downgrades: val.allow_downgrades, - nsis: val.nsis.map(Into::into), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NsisSettings { - pub template: Option, - pub license: Option, - pub header_image: Option, - pub sidebar_image: Option, - pub installer_icon: Option, - pub install_mode: NSISInstallerMode, - pub languages: Option>, - pub custom_language_files: Option>, - pub display_language_selector: bool, -} - -impl From for tauri_bundler::NsisSettings { - fn from(val: NsisSettings) -> Self { - tauri_bundler::NsisSettings { - license: val.license, - header_image: val.header_image, - sidebar_image: val.sidebar_image, - installer_icon: val.installer_icon, - install_mode: val.install_mode.into(), - languages: val.languages, - display_language_selector: val.display_language_selector, - custom_language_files: None, - template: None, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum NSISInstallerMode { - CurrentUser, - PerMachine, - Both, -} - -impl From for tauri_utils::config::NSISInstallerMode { - fn from(val: NSISInstallerMode) -> Self { - match val { - NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser, - NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine, - NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum WebviewInstallMode { - Skip, - DownloadBootstrapper { silent: bool }, - EmbedBootstrapper { silent: bool }, - OfflineInstaller { silent: bool }, - FixedRuntime { path: PathBuf }, -} - -impl WebviewInstallMode { - fn into(self) -> tauri_utils::config::WebviewInstallMode { - match self { - Self::Skip => tauri_utils::config::WebviewInstallMode::Skip, - Self::DownloadBootstrapper { silent } => { - tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent } - } - Self::EmbedBootstrapper { silent } => { - tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent } - } - Self::OfflineInstaller { silent } => { - tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent } - } - Self::FixedRuntime { path } => { - tauri_utils::config::WebviewInstallMode::FixedRuntime { path } - } - } - } -} - -impl Default for WebviewInstallMode { - fn default() -> Self { - Self::OfflineInstaller { silent: false } - } +fn true_bool() -> bool { + true } diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs index 78531cf62..b7b505760 100644 --- a/packages/cli-config/src/lib.rs +++ b/packages/cli-config/src/lib.rs @@ -4,6 +4,50 @@ mod config; pub use config::*; - +mod bundle; +pub use bundle::*; mod cargo; pub use cargo::*; + +#[doc(hidden)] +pub mod __private { + use crate::CrateConfig; + + pub const CONFIG_ENV: &str = "DIOXUS_CONFIG"; + + pub fn save_config(config: &CrateConfig) -> CrateConfigDropGuard { + std::env::set_var(CONFIG_ENV, serde_json::to_string(config).unwrap()); + CrateConfigDropGuard + } + + /// A guard that removes the config from the environment when dropped. + pub struct CrateConfigDropGuard; + + impl Drop for CrateConfigDropGuard { + fn drop(&mut self) { + std::env::remove_var(CONFIG_ENV); + } + } +} + +/// An error that occurs when the dioxus CLI was not used to build the application. +#[derive(Debug)] +pub struct DioxusCLINotUsed; + +impl std::fmt::Display for DioxusCLINotUsed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("dioxus CLI was not used to build the application") + } +} + +impl std::error::Error for DioxusCLINotUsed {} + +/// The current crate's configuration. +pub static CURRENT_CONFIG: once_cell::sync::Lazy< + Result, +> = once_cell::sync::Lazy::new(|| { + std::env::var(crate::__private::CONFIG_ENV) + .ok() + .and_then(|config| serde_json::from_str(&config).ok()) + .ok_or(DioxusCLINotUsed) +}); diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 4eb9db09f..050d9a78f 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -14,7 +14,7 @@ clap = { version = "4.2", features = ["derive"] } thiserror = { workspace = true } wasm-bindgen-cli-support = "0.2" colored = "2.0.0" -dioxus-cli-config = { workspace = true } +dioxus-cli-config = { workspace = true, features = ["cli"] } # features log = "0.4.14" diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 4dd1de970..1492e0868 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -1,11 +1,12 @@ use crate::{ - config::{CrateConfig, ExecutableType}, error::{Error, Result}, tools::Tool, - DioxusConfig, }; use cargo_metadata::{diagnostic::Diagnostic, Message}; use dioxus_cli_config::crate_root; +use dioxus_cli_config::CrateConfig; +use dioxus_cli_config::DioxusConfig; +use dioxus_cli_config::ExecutableType; use indicatif::{ProgressBar, ProgressStyle}; use serde::Serialize; use std::{ @@ -89,6 +90,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { ExecutableType::Example(name) => cmd.arg("--example").arg(name), }; + let _ = dioxus_cli_config::__private::save_config(config); + let warning_messages = prettier_build(cmd)?; // [2] Establish the output directory structure @@ -276,11 +279,13 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result cmd.arg("--bin").arg(name), - crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), - crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name), + ExecutableType::Binary(name) => cmd.arg("--bin").arg(name), + ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), + ExecutableType::Example(name) => cmd.arg("--example").arg(name), }; + let _ = dioxus_cli_config::__private::save_config(config); + let warning_messages = prettier_build(cmd)?; let release_type = match config.release { @@ -290,11 +295,11 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result { + ExecutableType::Binary(name) | ExecutableType::Lib(name) => { file_name = name.clone(); config.target_dir.join(release_type).join(name) } - crate::ExecutableType::Example(name) => { + ExecutableType::Example(name) => { file_name = name.clone(); config .target_dir @@ -444,8 +449,8 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { let mut script_list = resources.script.unwrap_or_default(); if serve { - let mut dev_style = resouces.dev.style.clone(); - let mut dev_script = resouces.dev.script.clone(); + let mut dev_style = resources.dev.style.clone(); + let mut dev_script = resources.dev.script.clone(); style_list.append(&mut dev_style); script_list.append(&mut dev_script); } diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index 1d1cbcd24..e1e726733 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -88,7 +88,7 @@ impl Autoformat { /// /// Doesn't do mod-descending, so it will still try to format unreachable files. TODO. async fn autoformat_project(check: bool) -> Result<()> { - let crate_config = crate::CrateConfig::new(None)?; + let crate_config = dioxus_cli_config::CrateConfig::new(None)?; let mut files_to_format = vec![]; collect_rs_files(&crate_config.crate_dir, &mut files_to_format); diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 675880111..f4d1d2457 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -14,7 +14,7 @@ pub struct Build { impl Build { pub fn build(self, bin: Option) -> Result<()> { - let mut crate_config = crate::CrateConfig::new(bin)?; + let mut crate_config = dioxus_cli_config::CrateConfig::new(bin)?; // change the release state. crate_config.with_release(self.build.release); diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index 80b52cf2f..8ee0c2fb1 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -1,4 +1,5 @@ use core::panic; +use dioxus_cli_config::ExecutableType; use std::{fs::create_dir_all, str::FromStr}; use tauri_bundler::{BundleSettings, PackageSettings, SettingsBuilder}; @@ -62,7 +63,7 @@ impl From for tauri_bundler::PackageType { impl Bundle { pub fn bundle(self, bin: Option) -> Result<()> { - let mut crate_config = crate::CrateConfig::new(bin)?; + let mut crate_config = dioxus_cli_config::CrateConfig::new(bin)?; // change the release state. crate_config.with_release(self.build.release); @@ -83,9 +84,9 @@ impl Bundle { let package = crate_config.manifest.package.unwrap(); let mut name: PathBuf = match &crate_config.executable { - crate::ExecutableType::Binary(name) - | crate::ExecutableType::Lib(name) - | crate::ExecutableType::Example(name) => name, + ExecutableType::Binary(name) + | ExecutableType::Lib(name) + | ExecutableType::Example(name) => name, } .into(); if cfg!(windows) { diff --git a/packages/cli/src/cli/check.rs b/packages/cli/src/cli/check.rs index a4d84addb..0832647f2 100644 --- a/packages/cli/src/cli/check.rs +++ b/packages/cli/src/cli/check.rs @@ -47,7 +47,7 @@ async fn check_file_and_report(path: PathBuf) -> Result<()> { /// /// Doesn't do mod-descending, so it will still try to check unreachable files. TODO. async fn check_project_and_report() -> Result<()> { - let crate_config = crate::CrateConfig::new(None)?; + let crate_config = dioxus_cli_config::CrateConfig::new(None)?; let mut files_to_check = vec![]; collect_rs_files(&crate_config.crate_dir, &mut files_to_check); diff --git a/packages/cli/src/cli/clean.rs b/packages/cli/src/cli/clean.rs index 09fb21469..cfe44e374 100644 --- a/packages/cli/src/cli/clean.rs +++ b/packages/cli/src/cli/clean.rs @@ -7,7 +7,7 @@ pub struct Clean {} impl Clean { pub fn clean(self, bin: Option) -> Result<()> { - let crate_config = crate::CrateConfig::new(bin)?; + let crate_config = dioxus_cli_config::CrateConfig::new(bin)?; let output = Command::new("cargo") .arg("clean") diff --git a/packages/cli/src/cli/config.rs b/packages/cli/src/cli/config.rs index ab29f95b2..1a1072c2f 100644 --- a/packages/cli/src/cli/config.rs +++ b/packages/cli/src/cli/config.rs @@ -50,7 +50,10 @@ impl Config { log::info!("🚩 Init config file completed."); } Config::FormatPrint {} => { - println!("{:#?}", crate::CrateConfig::new(None)?.dioxus_config); + println!( + "{:#?}", + dioxus_cli_config::CrateConfig::new(None)?.dioxus_config + ); } Config::CustomHtml {} => { let html_path = crate_root.join("index.html"); diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index 9b3be33bc..4295bfa9b 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -15,9 +15,10 @@ use crate::{ cfg::{ConfigOptsBuild, ConfigOptsServe}, custom_error, error::Result, - gen_page, server, CrateConfig, Error, + gen_page, server, Error, }; use clap::{Parser, Subcommand}; +use dioxus_cli_config::CrateConfig; use html_parser::Dom; use serde::Deserialize; use std::{ diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index 6d5c063f7..b4d60b769 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -13,7 +13,7 @@ pub struct Serve { impl Serve { pub async fn serve(self, bin: Option) -> Result<()> { - let mut crate_config = crate::CrateConfig::new(bin)?; + let mut crate_config = dioxus_cli_config::CrateConfig::new(bin)?; // change the relase state. crate_config.with_hot_reload(self.serve.hot_reload); diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs deleted file mode 100644 index 14e345c7a..000000000 --- a/packages/cli/src/config.rs +++ /dev/null @@ -1,628 +0,0 @@ -use crate::error::Result; -use dioxus_cli_config::{crate_root, Metadata, Platform}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DioxusConfig { - pub application: ApplicationConfig, - - pub web: WebConfig, - - #[serde(default)] - pub bundle: BundleConfig, - - #[serde(default = "default_plugin")] - pub plugin: toml::Value, -} - -fn default_plugin() -> toml::Value { - toml::Value::Boolean(true) -} - -impl DioxusConfig { - pub fn load(bin: Option) -> crate::error::Result> { - let crate_dir = crate_root(); - - let crate_dir = match crate_dir { - Ok(dir) => { - if let Some(bin) = bin { - dir.join(bin) - } else { - dir - } - } - Err(_) => return Ok(None), - }; - let crate_dir = crate_dir.as_path(); - - let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else { - return Ok(None); - }; - - let dioxus_conf_file = dioxus_conf_file.as_path(); - let cfg = toml::from_str::(&std::fs::read_to_string(dioxus_conf_file)?) - .map_err(|err| { - let error_location = dioxus_conf_file - .strip_prefix(crate_dir) - .unwrap_or(dioxus_conf_file) - .display(); - crate::Error::Unique(format!("{error_location} {err}")) - }) - .map(Some); - match cfg { - Ok(Some(mut cfg)) => { - let name = cfg.application.name.clone(); - if cfg.bundle.identifier.is_none() { - cfg.bundle.identifier = Some(format!("io.github.{name}")); - } - if cfg.bundle.publisher.is_none() { - cfg.bundle.publisher = Some(name); - } - Ok(Some(cfg)) - } - cfg => cfg, - } - } -} - -fn acquire_dioxus_toml(dir: &Path) -> Option { - // prefer uppercase - let uppercase_conf = dir.join("Dioxus.toml"); - if uppercase_conf.is_file() { - return Some(uppercase_conf); - } - - // lowercase is fine too - let lowercase_conf = dir.join("dioxus.toml"); - if lowercase_conf.is_file() { - return Some(lowercase_conf); - } - - None -} - -impl Default for DioxusConfig { - fn default() -> Self { - let name = default_name(); - Self { - application: ApplicationConfig { - name: name.clone(), - default_platform: default_platform(), - out_dir: out_dir_default(), - asset_dir: asset_dir_default(), - - tools: Default::default(), - - sub_package: None, - }, - web: WebConfig { - app: WebAppConfig { - title: default_title(), - base_path: None, - }, - proxy: vec![], - watcher: WebWatcherConfig { - watch_path: watch_path_default(), - reload_html: false, - index_on_404: true, - }, - resource: WebResourceConfig { - dev: WebDevResourceConfig { - style: vec![], - script: vec![], - }, - style: Some(vec![]), - script: Some(vec![]), - }, - https: WebHttpsConfig { - enabled: None, - mkcert: None, - key_path: None, - cert_path: None, - }, - }, - bundle: BundleConfig { - identifier: Some(format!("io.github.{name}")), - publisher: Some(name), - ..Default::default() - }, - plugin: toml::Value::Table(toml::map::Map::new()), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ApplicationConfig { - #[serde(default = "default_name")] - pub name: String, - #[serde(default = "default_platform")] - pub default_platform: Platform, - #[serde(default = "out_dir_default")] - pub out_dir: PathBuf, - #[serde(default = "asset_dir_default")] - pub asset_dir: PathBuf, - - #[serde(default)] - pub tools: HashMap, - - #[serde(default)] - pub sub_package: Option, -} - -fn default_name() -> String { - "name".into() -} - -fn default_platform() -> Platform { - Platform::Web -} - -fn asset_dir_default() -> PathBuf { - PathBuf::from("public") -} - -fn out_dir_default() -> PathBuf { - PathBuf::from("dist") -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebConfig { - #[serde(default)] - pub app: WebAppConfig, - #[serde(default)] - pub proxy: Vec, - pub watcher: WebWatcherConfig, - pub resource: WebResourceConfig, - #[serde(default)] - pub https: WebHttpsConfig, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebAppConfig { - #[serde(default = "default_title")] - pub title: String, - pub base_path: Option, -} - -impl Default for WebAppConfig { - fn default() -> Self { - Self { - title: default_title(), - base_path: None, - } - } -} - -fn default_title() -> String { - "dioxus | ⛺".into() -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebProxyConfig { - pub backend: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebWatcherConfig { - #[serde(default = "watch_path_default")] - pub watch_path: Vec, - #[serde(default)] - pub reload_html: bool, - #[serde(default = "true_bool")] - pub index_on_404: bool, -} - -fn watch_path_default() -> Vec { - vec![PathBuf::from("src"), PathBuf::from("examples")] -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebResourceConfig { - pub dev: WebDevResourceConfig, - pub style: Option>, - pub script: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebDevResourceConfig { - #[serde(default)] - pub style: Vec, - #[serde(default)] - pub script: Vec, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct WebHttpsConfig { - pub enabled: Option, - pub mkcert: Option, - pub key_path: Option, - pub cert_path: Option, -} - -#[derive(Debug, Clone)] -pub struct CrateConfig { - pub out_dir: PathBuf, - pub crate_dir: PathBuf, - pub workspace_dir: PathBuf, - pub target_dir: PathBuf, - pub asset_dir: PathBuf, - pub manifest: cargo_toml::Manifest, - pub executable: ExecutableType, - pub dioxus_config: DioxusConfig, - pub release: bool, - pub hot_reload: bool, - pub cross_origin_policy: bool, - pub verbose: bool, - pub custom_profile: Option, - pub features: Option>, -} - -#[derive(Debug, Clone)] -pub enum ExecutableType { - Binary(String), - Lib(String), - Example(String), -} - -impl CrateConfig { - pub fn new(bin: Option) -> Result { - let dioxus_config = DioxusConfig::load(bin.clone())?.unwrap_or_default(); - - let crate_root = crate_root()?; - - let crate_dir = if let Some(package) = &dioxus_config.application.sub_package { - crate_root.join(package) - } else if let Some(bin) = bin { - crate_root.join(bin) - } else { - crate_root - }; - - let meta = Metadata::get()?; - let workspace_dir = meta.workspace_root; - let target_dir = meta.target_directory; - - let out_dir = crate_dir.join(&dioxus_config.application.out_dir); - - let cargo_def = &crate_dir.join("Cargo.toml"); - - let asset_dir = crate_dir.join(&dioxus_config.application.asset_dir); - - let manifest = cargo_toml::Manifest::from_path(cargo_def).unwrap(); - - let mut output_filename = String::from("dioxus_app"); - if let Some(package) = &manifest.package.as_ref() { - output_filename = match &package.default_run { - Some(default_run_target) => default_run_target.to_owned(), - None => manifest - .bin - .iter() - .find(|b| b.name == manifest.package.as_ref().map(|pkg| pkg.name.clone())) - .or(manifest - .bin - .iter() - .find(|b| b.path == Some("src/main.rs".to_owned()))) - .or(manifest.bin.first()) - .or(manifest.lib.as_ref()) - .and_then(|prod| prod.name.clone()) - .unwrap_or(String::from("dioxus_app")), - }; - } - - let executable = ExecutableType::Binary(output_filename); - - let release = false; - let hot_reload = false; - let verbose = false; - let custom_profile = None; - let features = None; - - Ok(Self { - out_dir, - crate_dir, - workspace_dir, - target_dir, - asset_dir, - manifest, - executable, - release, - dioxus_config, - hot_reload, - cross_origin_policy: false, - custom_profile, - features, - verbose, - }) - } - - pub fn as_example(&mut self, example_name: String) -> &mut Self { - self.executable = ExecutableType::Example(example_name); - self - } - - pub fn with_release(&mut self, release: bool) -> &mut Self { - self.release = release; - self - } - - pub fn with_hot_reload(&mut self, hot_reload: bool) -> &mut Self { - self.hot_reload = hot_reload; - self - } - - pub fn with_cross_origin_policy(&mut self, cross_origin_policy: bool) -> &mut Self { - self.cross_origin_policy = cross_origin_policy; - self - } - - pub fn with_verbose(&mut self, verbose: bool) -> &mut Self { - self.verbose = verbose; - self - } - - pub fn set_profile(&mut self, profile: String) -> &mut Self { - self.custom_profile = Some(profile); - self - } - - pub fn set_features(&mut self, features: Vec) -> &mut Self { - self.features = Some(features); - self - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct BundleConfig { - pub identifier: Option, - pub publisher: Option, - pub icon: Option>, - pub resources: Option>, - pub copyright: Option, - pub category: Option, - pub short_description: Option, - pub long_description: Option, - pub external_bin: Option>, - pub deb: Option, - pub macos: Option, - pub windows: Option, -} - -impl From for tauri_bundler::BundleSettings { - fn from(val: BundleConfig) -> Self { - tauri_bundler::BundleSettings { - identifier: val.identifier, - publisher: val.publisher, - icon: val.icon, - resources: val.resources, - copyright: val.copyright, - category: val.category.and_then(|c| c.parse().ok()), - short_description: val.short_description, - long_description: val.long_description, - external_bin: val.external_bin, - deb: val.deb.map(Into::into).unwrap_or_default(), - macos: val.macos.map(Into::into).unwrap_or_default(), - windows: val.windows.map(Into::into).unwrap_or_default(), - ..Default::default() - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct DebianSettings { - pub depends: Option>, - pub files: HashMap, - pub nsis: Option, -} - -impl From for tauri_bundler::DebianSettings { - fn from(val: DebianSettings) -> Self { - tauri_bundler::DebianSettings { - depends: val.depends, - files: val.files, - desktop_template: None, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct WixSettings { - pub language: Vec<(String, Option)>, - pub template: Option, - pub fragment_paths: Vec, - pub component_group_refs: Vec, - pub component_refs: Vec, - pub feature_group_refs: Vec, - pub feature_refs: Vec, - pub merge_refs: Vec, - pub skip_webview_install: bool, - pub license: Option, - pub enable_elevated_update_task: bool, - pub banner_path: Option, - pub dialog_image_path: Option, - pub fips_compliant: bool, -} - -impl From for tauri_bundler::WixSettings { - fn from(val: WixSettings) -> Self { - tauri_bundler::WixSettings { - language: tauri_bundler::bundle::WixLanguage({ - let mut languages: Vec<_> = val - .language - .iter() - .map(|l| { - ( - l.0.clone(), - tauri_bundler::bundle::WixLanguageConfig { - locale_path: l.1.clone(), - }, - ) - }) - .collect(); - if languages.is_empty() { - languages.push(("en-US".into(), Default::default())); - } - languages - }), - template: val.template, - fragment_paths: val.fragment_paths, - component_group_refs: val.component_group_refs, - component_refs: val.component_refs, - feature_group_refs: val.feature_group_refs, - feature_refs: val.feature_refs, - merge_refs: val.merge_refs, - skip_webview_install: val.skip_webview_install, - license: val.license, - enable_elevated_update_task: val.enable_elevated_update_task, - banner_path: val.banner_path, - dialog_image_path: val.dialog_image_path, - fips_compliant: val.fips_compliant, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct MacOsSettings { - pub frameworks: Option>, - pub minimum_system_version: Option, - pub license: Option, - pub exception_domain: Option, - pub signing_identity: Option, - pub provider_short_name: Option, - pub entitlements: Option, - pub info_plist_path: Option, -} - -impl From for tauri_bundler::MacOsSettings { - fn from(val: MacOsSettings) -> Self { - tauri_bundler::MacOsSettings { - frameworks: val.frameworks, - minimum_system_version: val.minimum_system_version, - license: val.license, - exception_domain: val.exception_domain, - signing_identity: val.signing_identity, - provider_short_name: val.provider_short_name, - entitlements: val.entitlements, - info_plist_path: val.info_plist_path, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WindowsSettings { - pub digest_algorithm: Option, - pub certificate_thumbprint: Option, - pub timestamp_url: Option, - pub tsp: bool, - pub wix: Option, - pub icon_path: Option, - pub webview_install_mode: WebviewInstallMode, - pub webview_fixed_runtime_path: Option, - pub allow_downgrades: bool, - pub nsis: Option, -} - -impl From for tauri_bundler::WindowsSettings { - fn from(val: WindowsSettings) -> Self { - tauri_bundler::WindowsSettings { - digest_algorithm: val.digest_algorithm, - certificate_thumbprint: val.certificate_thumbprint, - timestamp_url: val.timestamp_url, - tsp: val.tsp, - wix: val.wix.map(Into::into), - icon_path: val.icon_path.unwrap_or("icons/icon.ico".into()), - webview_install_mode: val.webview_install_mode.into(), - webview_fixed_runtime_path: val.webview_fixed_runtime_path, - allow_downgrades: val.allow_downgrades, - nsis: val.nsis.map(Into::into), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NsisSettings { - pub template: Option, - pub license: Option, - pub header_image: Option, - pub sidebar_image: Option, - pub installer_icon: Option, - pub install_mode: NSISInstallerMode, - pub languages: Option>, - pub custom_language_files: Option>, - pub display_language_selector: bool, -} - -impl From for tauri_bundler::NsisSettings { - fn from(val: NsisSettings) -> Self { - tauri_bundler::NsisSettings { - license: val.license, - header_image: val.header_image, - sidebar_image: val.sidebar_image, - installer_icon: val.installer_icon, - install_mode: val.install_mode.into(), - languages: val.languages, - display_language_selector: val.display_language_selector, - custom_language_files: None, - template: None, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum NSISInstallerMode { - CurrentUser, - PerMachine, - Both, -} - -impl From for tauri_utils::config::NSISInstallerMode { - fn from(val: NSISInstallerMode) -> Self { - match val { - NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser, - NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine, - NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum WebviewInstallMode { - Skip, - DownloadBootstrapper { silent: bool }, - EmbedBootstrapper { silent: bool }, - OfflineInstaller { silent: bool }, - FixedRuntime { path: PathBuf }, -} - -impl WebviewInstallMode { - fn into(self) -> tauri_utils::config::WebviewInstallMode { - match self { - Self::Skip => tauri_utils::config::WebviewInstallMode::Skip, - Self::DownloadBootstrapper { silent } => { - tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent } - } - Self::EmbedBootstrapper { silent } => { - tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent } - } - Self::OfflineInstaller { silent } => { - tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent } - } - Self::FixedRuntime { path } => { - tauri_utils::config::WebviewInstallMode::FixedRuntime { path } - } - } - } -} - -impl Default for WebviewInstallMode { - fn default() -> Self { - Self::OfflineInstaller { silent: false } - } -} - -fn true_bool() -> bool { - true -} diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index 0088492e8..a9578351b 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -81,6 +81,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: dioxus_cli_config::CrateConfigError) -> Self { + Self::RuntimeError(e.to_string()) + } +} + #[macro_export] macro_rules! custom_error { ($msg:literal $(,)?) => { diff --git a/packages/cli/src/lib.rs b/packages/cli/src/lib.rs index 9a87a2dfa..63ec1fac0 100644 --- a/packages/cli/src/lib.rs +++ b/packages/cli/src/lib.rs @@ -13,9 +13,6 @@ pub use builder::*; pub mod cli; pub use cli::*; -pub mod config; -pub use config::*; - pub mod error; pub use error::*; diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 978b97d43..3fd9de2a2 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -3,8 +3,10 @@ use crate::{ output::{print_console_info, PrettierOptions}, setup_file_watcher, }, - BuildResult, CrateConfig, Result, + BuildResult, Result, }; +use dioxus_cli_config::CrateConfig; +use dioxus_cli_config::ExecutableType; use dioxus_hot_reload::HotReloadMsg; use dioxus_html::HtmlCtx; @@ -215,9 +217,9 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { let result = crate::builder::build_desktop(config, true)?; match &config.executable { - crate::ExecutableType::Binary(name) - | crate::ExecutableType::Lib(name) - | crate::ExecutableType::Example(name) => { + ExecutableType::Binary(name) + | ExecutableType::Lib(name) + | ExecutableType::Example(name) => { let mut file = config.out_dir.join(name); if cfg!(windows) { file.set_extension("exe"); diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 7b60c8178..a386362c0 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -1,4 +1,5 @@ -use crate::{BuildResult, CrateConfig, Result}; +use crate::{BuildResult, Result}; +use dioxus_cli_config::CrateConfig; use cargo_metadata::diagnostic::Diagnostic; use dioxus_core::Template; diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs index 151e29099..64bd305dd 100644 --- a/packages/cli/src/server/output.rs +++ b/packages/cli/src/server/output.rs @@ -1,7 +1,7 @@ use crate::server::Diagnostic; -use crate::CrateConfig; use colored::Colorize; use dioxus_cli_config::crate_root; +use dioxus_cli_config::CrateConfig; use std::path::PathBuf; use std::process::Command; diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index f192f0f52..1c5bade15 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -5,7 +5,7 @@ use crate::{ output::{print_console_info, PrettierOptions, WebServerInfo}, setup_file_watcher, HotReloadState, }, - BuildResult, CrateConfig, Result, WebHttpsConfig, + BuildResult, Result, }; use axum::{ body::{Full, HttpBody}, @@ -19,6 +19,8 @@ use axum::{ Router, }; use axum_server::tls_rustls::RustlsConfig; +use dioxus_cli_config::CrateConfig; +use dioxus_cli_config::WebHttpsConfig; use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; diff --git a/packages/cli/src/server/web/proxy.rs b/packages/cli/src/server/web/proxy.rs index e35635b6b..4242a55eb 100644 --- a/packages/cli/src/server/web/proxy.rs +++ b/packages/cli/src/server/web/proxy.rs @@ -1,4 +1,5 @@ -use crate::{Result, WebProxyConfig}; +use crate::Result; +use dioxus_cli_config::WebProxyConfig; use anyhow::Context; use axum::{http::StatusCode, routing::any, Router}; From 1ed66a54ccb1bea750ee8dd6cdb91bb45b5d33cd Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 14 Nov 2023 15:50:04 -0600 Subject: [PATCH 054/126] fix cli configuration environment variable --- packages/cli-config/Cargo.toml | 1 + packages/cli-config/src/assets/dioxus.toml | 74 ---------------------- packages/cli-config/src/bundle.rs | 1 + packages/cli-config/src/lib.rs | 11 +++- packages/cli/src/builder.rs | 6 +- packages/cli/src/main.rs | 1 + 6 files changed, 13 insertions(+), 81 deletions(-) delete mode 100644 packages/cli-config/src/assets/dioxus.toml diff --git a/packages/cli-config/Cargo.toml b/packages/cli-config/Cargo.toml index 2ef7ce8e6..376dd3240 100644 --- a/packages/cli-config/Cargo.toml +++ b/packages/cli-config/Cargo.toml @@ -19,6 +19,7 @@ cargo_toml = "0.16.0" tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"], optional = true } tauri-utils = { version = "=1.4.*", optional = true } once_cell = "1.18.0" +tracing.workspace = true [features] default = [] diff --git a/packages/cli-config/src/assets/dioxus.toml b/packages/cli-config/src/assets/dioxus.toml deleted file mode 100644 index 892a6cdf8..000000000 --- a/packages/cli-config/src/assets/dioxus.toml +++ /dev/null @@ -1,74 +0,0 @@ -[application] - -# dioxus project name -name = "{{project-name}}" - -# default platfrom -# you can also use `dx serve/build --platform XXX` to use other platform -# value: web | desktop -default_platform = "{{default-platform}}" - -# Web `build` & `serve` dist path -out_dir = "dist" - -# resource (static) file folder -asset_dir = "public" - -[web.app] - -# HTML title tag content -title = "Dioxus | An elegant GUI library for Rust" - -[web.watcher] - -index_on_404 = true - -watch_path = ["src", "examples"] - -# include `assets` in web platform -[web.resource] - -# CSS style file -style = [] - -# Javascript code file -script = [] - -[web.resource.dev] - -# Javascript code file -# serve: [dev-server] only -script = [] - -[application.plugins] - -available = true - -required = [] - -[bundler] -# Bundle identifier -identifier = "io.github.{{project-name}}" - -# Bundle publisher -publisher = "{{project-name}}" - -# Bundle icon -icon = ["icons/icon.png"] - -# Bundle resources -resources = ["public/*"] - -# Bundle copyright -copyright = "" - -# Bundle category -category = "Utility" - -# Bundle short description -short_description = "An amazing dioxus application." - -# Bundle long description -long_description = """ -An amazing dioxus application. -""" \ No newline at end of file diff --git a/packages/cli-config/src/bundle.rs b/packages/cli-config/src/bundle.rs index cdae4539b..4f44036d7 100644 --- a/packages/cli-config/src/bundle.rs +++ b/packages/cli-config/src/bundle.rs @@ -231,6 +231,7 @@ pub enum WebviewInstallMode { FixedRuntime { path: PathBuf }, } +#[cfg(feature = "cli")] impl WebviewInstallMode { fn into(self) -> tauri_utils::config::WebviewInstallMode { match self { diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs index b7b505760..776d1d10c 100644 --- a/packages/cli-config/src/lib.rs +++ b/packages/cli-config/src/lib.rs @@ -46,8 +46,13 @@ impl std::error::Error for DioxusCLINotUsed {} pub static CURRENT_CONFIG: once_cell::sync::Lazy< Result, > = once_cell::sync::Lazy::new(|| { - std::env::var(crate::__private::CONFIG_ENV) - .ok() + CURRENT_CONFIG_JSON .and_then(|config| serde_json::from_str(&config).ok()) - .ok_or(DioxusCLINotUsed) + .ok_or_else(|| { + tracing::error!("A library is trying to access the crate's configuration, but the dioxus CLI was not used to build the application."); + DioxusCLINotUsed + }) }); + +/// The current crate's configuration. +pub const CURRENT_CONFIG_JSON: Option<&str> = std::option_env!("DIOXUS_CONFIG"); diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 1492e0868..a2ee2c73f 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -47,6 +47,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { let ignore_files = build_assets(config)?; let t_start = std::time::Instant::now(); + let _guard = dioxus_cli_config::__private::save_config(config); // [1] Build the .wasm module log::info!("🚅 Running build command..."); @@ -90,8 +91,6 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { ExecutableType::Example(name) => cmd.arg("--example").arg(name), }; - let _ = dioxus_cli_config::__private::save_config(config); - let warning_messages = prettier_build(cmd)?; // [2] Establish the output directory structure @@ -254,6 +253,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result Result cmd.arg("--example").arg(name), }; - let _ = dioxus_cli_config::__private::save_config(config); - let warning_messages = prettier_build(cmd)?; let release_type = match config.release { diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index fe860a0e6..51b884a9a 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -1,3 +1,4 @@ +use dioxus_cli_config::DioxusConfig; use std::path::PathBuf; use anyhow::anyhow; From 15984b78dbac9c6935b564b2579bfcc1b2fd99b4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 14 Nov 2023 15:53:11 -0600 Subject: [PATCH 055/126] use the name in the dioxus.toml in desktop if no name was set in the desktop config --- packages/desktop/Cargo.toml | 1 + packages/desktop/src/cfg.rs | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index dcb994120..24c938c01 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -14,6 +14,7 @@ dioxus-core = { workspace = true, features = ["serialize"] } dioxus-html = { workspace = true, features = ["serialize", "native-bind"] } dioxus-interpreter-js = { workspace = true } dioxus-hot-reload = { workspace = true, optional = true } +dioxus-cli-config = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 07281e364..a2dad21c5 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -49,7 +49,12 @@ impl Config { /// Initializes a new `WindowBuilder` with default values. #[inline] pub fn new() -> Self { - let window = WindowBuilder::new().with_title("Dioxus app"); + let window = WindowBuilder::new().with_title( + dioxus_cli_config::CURRENT_CONFIG + .as_ref() + .map(|c| c.dioxus_config.application.name.clone()) + .unwrap_or("Dioxus App".to_string()), + ); Self { // event_handler: None, From 109e10e406e6bf1c90f4c635e79126a43d9e573f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 14 Nov 2023 16:49:06 -0600 Subject: [PATCH 056/126] use a default base path for the web router from the config --- packages/router/Cargo.toml | 1 + packages/router/src/history/web.rs | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index da9d3fe85..f07c3a724 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -30,6 +30,7 @@ gloo-utils = { version = "0.1.6", optional = true } dioxus-liveview = { workspace = true, optional = true } dioxus-ssr = { workspace = true, optional = true } tokio = { workspace = true, features = ["full"], optional = true } +dioxus-cli-config.workspace = true [features] default = ["web"] diff --git a/packages/router/src/history/web.rs b/packages/router/src/history/web.rs index 789eed76e..5b6ae0528 100644 --- a/packages/router/src/history/web.rs +++ b/packages/router/src/history/web.rs @@ -13,6 +13,17 @@ use super::{ HistoryProvider, }; +#[allow(dead_code)] +fn base_path() -> Option<&'static str> { + dioxus_cli_config::CURRENT_CONFIG.as_ref().ok().and_then(|c| { + c.dioxus_config + .web + .app + .base_path + .as_deref() + }) + } + #[cfg(not(feature = "serde"))] #[allow(clippy::extra_unused_type_parameters)] fn update_scroll(window: &Window, history: &History) { @@ -165,7 +176,7 @@ impl WebHistory { history, listener_navigation: None, listener_animation_frame: Default::default(), - prefix, + prefix: prefix.or_else(||base_path().map(|s| s.to_string())), window, phantom: Default::default(), } @@ -198,6 +209,16 @@ where let location = self.window.location(); let path = location.pathname().unwrap_or_else(|_| "/".into()) + &location.search().unwrap_or("".into()); + let path = match self.prefix { + None => path, + Some(ref prefix) => { + if path.starts_with(prefix) { + path[prefix.len()..].to_string() + } else { + path + } + } + }; R::from_str(&path).unwrap_or_else(|err| panic!("{}", err)) } From cd48b3b7f942a47d1e2126a3c5c5640325fd4552 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 15 Nov 2023 08:23:12 -0600 Subject: [PATCH 057/126] allow base_path in the CLI --- packages/cli/Cargo.toml | 2 +- packages/cli/src/cli/serve.rs | 3 --- packages/cli/src/server/web/mod.rs | 15 +++++++++++++++ packages/router/src/history/web.rs | 21 ++++++++++++--------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 050d9a78f..0730b59e7 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -37,7 +37,7 @@ hyper-rustls = "0.23.2" indicatif = "0.17.5" subprocess = "0.2.9" -axum = { version = "0.5.1", features = ["ws", "headers"] } +axum = { version = "0.6.20", features = ["ws", "headers"] } axum-server = { version = "0.5.1", features = ["tls-rustls"] } tower-http = { version = "0.2.2", features = ["full"] } headers = "0.3.7" diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index b4d60b769..2f4d81402 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -33,9 +33,6 @@ impl Serve { crate_config.set_features(self.serve.features.unwrap()); } - // Subdirectories don't work with the server - crate_config.dioxus_config.web.app.base_path = None; - let platform = self .serve .platform diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 1c5bade15..42a235f4a 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -310,6 +310,21 @@ async fn setup_router( }, )); + router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() { + let base_path = format!("/{}", base_path.trim_matches('/')); + Router::new() + .nest_service( + &base_path, + axum::routing::method_routing::any_service(router), + ) + .fallback(get(move || { + let base_path = base_path.clone(); + async move { format!("Outside of the base path: {}", base_path) } + })) + } else { + router + }; + // Setup routes router = router .route("/_dioxus/hot_reload", get(hot_reload_handler)) diff --git a/packages/router/src/history/web.rs b/packages/router/src/history/web.rs index 5b6ae0528..a0f677e24 100644 --- a/packages/router/src/history/web.rs +++ b/packages/router/src/history/web.rs @@ -15,14 +15,13 @@ use super::{ #[allow(dead_code)] fn base_path() -> Option<&'static str> { - dioxus_cli_config::CURRENT_CONFIG.as_ref().ok().and_then(|c| { - c.dioxus_config - .web - .app - .base_path - .as_deref() - }) - } + let base_path = dioxus_cli_config::CURRENT_CONFIG + .as_ref() + .ok() + .and_then(|c| c.dioxus_config.web.app.base_path.as_deref()); + tracing::trace!("Using base_path from Dioxus.toml: {:?}", base_path); + base_path +} #[cfg(not(feature = "serde"))] #[allow(clippy::extra_unused_type_parameters)] @@ -171,12 +170,16 @@ impl WebHistory { .expect("`history` can set scroll restoration"); } + let prefix = prefix + .or_else(|| base_path().map(|s| s.to_string())) + .map(|prefix| format!("/{}", prefix.trim_matches('/'))); + Self { do_scroll_restoration, history, listener_navigation: None, listener_animation_frame: Default::default(), - prefix: prefix.or_else(||base_path().map(|s| s.to_string())), + prefix, window, phantom: Default::default(), } From 9bb464dd7d687cf72735b670786e5d92be5d5630 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 15 Nov 2023 09:29:15 -0600 Subject: [PATCH 058/126] fix cli tests --- packages/cli/Cargo.toml | 2 +- packages/cli/README.md | 8 ++++---- packages/cli/src/server/web/mod.rs | 4 ++-- packages/cli/src/server/web/proxy.rs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 0730b59e7..050d9a78f 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -37,7 +37,7 @@ hyper-rustls = "0.23.2" indicatif = "0.17.5" subprocess = "0.2.9" -axum = { version = "0.6.20", features = ["ws", "headers"] } +axum = { version = "0.5.1", features = ["ws", "headers"] } axum-server = { version = "0.5.1", features = ["tls-rustls"] } tower-http = { version = "0.2.2", features = ["full"] } headers = "0.3.7" diff --git a/packages/cli/README.md b/packages/cli/README.md index 8379f22cc..be053a2d2 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -10,7 +10,7 @@ It handles building, bundling, development and publishing to simplify developmen ### Install the stable version (recommended) -``` +```sh cargo install dioxus-cli ``` @@ -20,7 +20,7 @@ To get the latest bug fixes and features, you can install the development versio However, this is not fully tested. That means you're probably going to have more bugs despite having the latest bug fixes. -``` +```sh cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli ``` @@ -29,7 +29,7 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default). ### Install from local folder -``` +```sh cargo install --path . --debug ``` @@ -40,7 +40,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu Alternatively, you can specify the template path: -``` +```sh dx create hello --template gh:dioxuslabs/dioxus-template ``` diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 42a235f4a..b38c535f4 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -313,9 +313,9 @@ async fn setup_router( router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() { let base_path = format!("/{}", base_path.trim_matches('/')); Router::new() - .nest_service( + .nest( &base_path, - axum::routing::method_routing::any_service(router), + axum::routing::any_service(router), ) .fallback(get(move || { let base_path = base_path.clone(); diff --git a/packages/cli/src/server/web/proxy.rs b/packages/cli/src/server/web/proxy.rs index 4242a55eb..d7b4837e8 100644 --- a/packages/cli/src/server/web/proxy.rs +++ b/packages/cli/src/server/web/proxy.rs @@ -1,6 +1,6 @@ -use crate::Result; +use crate::{Result,}; use dioxus_cli_config::WebProxyConfig; - + use anyhow::Context; use axum::{http::StatusCode, routing::any, Router}; use hyper::{Request, Response, Uri}; From e11f3fdc484fb22a4983467d846c99479606227c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 15 Nov 2023 09:30:15 -0600 Subject: [PATCH 059/126] fix clippy --- packages/cli-config/src/lib.rs | 2 +- packages/cli/src/server/web/mod.rs | 5 +---- packages/cli/src/server/web/proxy.rs | 4 ++-- packages/core/src/diff.rs | 2 +- packages/rsx/src/lib.rs | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs index 776d1d10c..da7446918 100644 --- a/packages/cli-config/src/lib.rs +++ b/packages/cli-config/src/lib.rs @@ -47,7 +47,7 @@ pub static CURRENT_CONFIG: once_cell::sync::Lazy< Result, > = once_cell::sync::Lazy::new(|| { CURRENT_CONFIG_JSON - .and_then(|config| serde_json::from_str(&config).ok()) + .and_then(|config| serde_json::from_str(config).ok()) .ok_or_else(|| { tracing::error!("A library is trying to access the crate's configuration, but the dioxus CLI was not used to build the application."); DioxusCLINotUsed diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index b38c535f4..5ebc66772 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -313,10 +313,7 @@ async fn setup_router( router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() { let base_path = format!("/{}", base_path.trim_matches('/')); Router::new() - .nest( - &base_path, - axum::routing::any_service(router), - ) + .nest(&base_path, axum::routing::any_service(router)) .fallback(get(move || { let base_path = base_path.clone(); async move { format!("Outside of the base path: {}", base_path) } diff --git a/packages/cli/src/server/web/proxy.rs b/packages/cli/src/server/web/proxy.rs index d7b4837e8..4242a55eb 100644 --- a/packages/cli/src/server/web/proxy.rs +++ b/packages/cli/src/server/web/proxy.rs @@ -1,6 +1,6 @@ -use crate::{Result,}; +use crate::Result; use dioxus_cli_config::WebProxyConfig; - + use anyhow::Context; use axum::{http::StatusCode, routing::any, Router}; use hyper::{Request, Response, Uri}; diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 74017db0f..b2145a806 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -560,7 +560,7 @@ impl<'b> VirtualDom { // If none of the old keys are reused by the new children, then we remove all the remaining old children and // create the new children afresh. if shared_keys.is_empty() { - if old.get(0).is_some() { + if !old.is_empty() { self.remove_nodes(&old[1..]); self.replace(&old[0], new); } else { diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index bdf32b272..5db6baee9 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -193,7 +193,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { fn to_tokens(&self, out_tokens: &mut TokenStream2) { let mut context = DynamicContext::default(); - let key = match self.roots.get(0) { + let key = match self.roots.first() { Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key.clone(), Some(BodyNode::Component(comp)) if self.roots.len() == 1 => comp.key().cloned(), _ => None, From d51e9b60dfe22792554175428a27d191dab36b2a Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Nov 2023 11:16:15 -0600 Subject: [PATCH 060/126] make WebDevResourceConfig optional --- packages/cli-config/src/config.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/cli-config/src/config.rs b/packages/cli-config/src/config.rs index fb3afb4ac..0c9963222 100644 --- a/packages/cli-config/src/config.rs +++ b/packages/cli-config/src/config.rs @@ -181,11 +181,7 @@ impl Default for DioxusConfig { base_path: None, }, proxy: vec![], - watcher: WebWatcherConfig { - watch_path: watch_path_default(), - reload_html: false, - index_on_404: true, - }, + watcher: Default::default(), resource: WebResourceConfig { dev: WebDevResourceConfig { style: vec![], @@ -251,7 +247,9 @@ pub struct WebConfig { pub app: WebAppConfig, #[serde(default)] pub proxy: Vec, + #[serde(default)] pub watcher: WebWatcherConfig, + #[serde(default)] pub resource: WebResourceConfig, #[serde(default)] pub https: WebHttpsConfig, @@ -292,18 +290,28 @@ pub struct WebWatcherConfig { pub index_on_404: bool, } +impl Default for WebWatcherConfig { + fn default() -> Self { + Self { + watch_path: watch_path_default(), + reload_html: false, + index_on_404: true, + } + } +} + fn watch_path_default() -> Vec { vec![PathBuf::from("src"), PathBuf::from("examples")] } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct WebResourceConfig { pub dev: WebDevResourceConfig, pub style: Option>, pub script: Option>, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct WebDevResourceConfig { #[serde(default)] pub style: Vec, From 532ffaa614031aec4a8e0655484fa06d61343c84 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 20 Nov 2023 18:36:18 -0600 Subject: [PATCH 061/126] fix merge --- packages/cli/src/builder.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 40bc31d55..4c9254bba 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -460,13 +460,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { &style.to_str().unwrap(), )) } - if config - .application - .tools - .clone() - .unwrap_or_default() - .contains_key("tailwindcss") - { + if config.application.tools.clone().contains_key("tailwindcss") { style_str.push_str("\n"); } From 4ff86d97637ceea6419a26b508bc33d8bfcf3d30 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 21 Nov 2023 13:11:49 -0600 Subject: [PATCH 062/126] make some extra dependencies only enabled for the CLI --- packages/cli-config/Cargo.toml | 16 +++++++------- packages/cli-config/src/config.rs | 35 +++++++++++++++++++------------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/cli-config/Cargo.toml b/packages/cli-config/Cargo.toml index 376dd3240..06d7adf49 100644 --- a/packages/cli-config/Cargo.toml +++ b/packages/cli-config/Cargo.toml @@ -9,18 +9,18 @@ license = "MIT OR Apache-2.0" keywords = ["react", "gui", "cli", "dioxus", "wasm"] [dependencies] -clap = { version = "4.2", features = ["derive"] } +clap = { version = "4.2", features = ["derive"], optional = true } serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" -toml = "0.5.8" -cargo_toml = "0.16.0" - -# bundling -tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"], optional = true } -tauri-utils = { version = "=1.4.*", optional = true } +toml = { version = "0.5.8", optional = true } +cargo_toml = { version = "0.16.0", optional = true } once_cell = "1.18.0" tracing.workspace = true +# bundling +tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"], optional = true } +tauri-utils = { version = "=1.4.*", optional = true } + [features] default = [] -cli = ["tauri-bundler", "tauri-utils"] +cli = ["tauri-bundler", "tauri-utils", "clap", "toml", "cargo_toml"] diff --git a/packages/cli-config/src/config.rs b/packages/cli-config/src/config.rs index 0c9963222..ae76f5314 100644 --- a/packages/cli-config/src/config.rs +++ b/packages/cli-config/src/config.rs @@ -1,20 +1,16 @@ use crate::BundleConfig; use crate::CargoError; -use crate::{crate_root, Metadata}; -use clap::ValueEnum; use core::fmt::{Display, Formatter}; use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; +use std::path::PathBuf; -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)] +#[cfg_attr(feature = "cli", derive(clap::ValueEnum))] pub enum Platform { - #[clap(name = "web")] + #[cfg_attr(feature = "cli", clap(name = "web"))] #[serde(rename = "web")] Web, - #[clap(name = "desktop")] + #[cfg_attr(feature = "cli", clap(name = "desktop"))] #[serde(rename = "desktop")] Desktop, } @@ -28,10 +24,12 @@ pub struct DioxusConfig { #[serde(default)] pub bundle: BundleConfig, + #[cfg(feature = "cli")] #[serde(default = "default_plugin")] pub plugin: toml::Value, } +#[cfg(feature = "cli")] fn default_plugin() -> toml::Value { toml::Value::Boolean(true) } @@ -54,6 +52,7 @@ impl std::error::Error for LoadDioxusConfigError {} pub enum CrateConfigError { Cargo(CargoError), Io(std::io::Error), + #[cfg(feature = "cli")] Toml(toml::de::Error), LoadDioxusConfig(LoadDioxusConfigError), } @@ -70,6 +69,7 @@ impl From for CrateConfigError { } } +#[cfg(feature = "cli")] impl From for CrateConfigError { fn from(err: toml::de::Error) -> Self { Self::Toml(err) @@ -87,6 +87,7 @@ impl Display for CrateConfigError { match self { Self::Cargo(err) => write!(f, "{}", err), Self::Io(err) => write!(f, "{}", err), + #[cfg(feature = "cli")] Self::Toml(err) => write!(f, "{}", err), Self::LoadDioxusConfig(err) => write!(f, "{}", err), } @@ -96,6 +97,7 @@ impl Display for CrateConfigError { impl std::error::Error for CrateConfigError {} impl DioxusConfig { + #[cfg(feature = "cli")] /// Load the dioxus config from a path pub fn load(bin: Option) -> Result, CrateConfigError> { let crate_dir = crate::cargo::crate_root(); @@ -145,7 +147,8 @@ impl DioxusConfig { } } -fn acquire_dioxus_toml(dir: &Path) -> Option { +#[cfg(feature = "cli")] +fn acquire_dioxus_toml(dir: &std::path::Path) -> Option { // prefer uppercase let uppercase_conf = dir.join("Dioxus.toml"); if uppercase_conf.is_file() { @@ -171,6 +174,7 @@ impl Default for DioxusConfig { out_dir: out_dir_default(), asset_dir: asset_dir_default(), + #[cfg(feature = "cli")] tools: Default::default(), sub_package: None, @@ -202,6 +206,7 @@ impl Default for DioxusConfig { publisher: Some(name), ..Default::default() }, + #[cfg(feature = "cli")] plugin: toml::Value::Table(toml::map::Map::new()), } } @@ -218,8 +223,9 @@ pub struct ApplicationConfig { #[serde(default = "asset_dir_default")] pub asset_dir: PathBuf, + #[cfg(feature = "cli")] #[serde(default)] - pub tools: HashMap, + pub tools: std::collections::HashMap, #[serde(default)] pub sub_package: Option, @@ -334,6 +340,7 @@ pub struct CrateConfig { pub workspace_dir: PathBuf, pub target_dir: PathBuf, pub asset_dir: PathBuf, + #[cfg(feature = "cli")] pub manifest: cargo_toml::Manifest, pub executable: ExecutableType, pub dioxus_config: DioxusConfig, @@ -353,10 +360,11 @@ pub enum ExecutableType { } impl CrateConfig { + #[cfg(feature = "cli")] pub fn new(bin: Option) -> Result { let dioxus_config = DioxusConfig::load(bin.clone())?.unwrap_or_default(); - let crate_root = crate_root()?; + let crate_root = crate::crate_root()?; let crate_dir = if let Some(package) = &dioxus_config.application.sub_package { crate_root.join(package) @@ -366,7 +374,7 @@ impl CrateConfig { crate_root }; - let meta = Metadata::get()?; + let meta = crate::Metadata::get()?; let workspace_dir = meta.workspace_root; let target_dir = meta.target_directory; @@ -411,6 +419,7 @@ impl CrateConfig { workspace_dir, target_dir, asset_dir, + #[cfg(feature = "cli")] manifest, executable, release, From 23090cb56b2186a649e9ad97fe549a1046e2efd5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 21 Nov 2023 14:46:47 -0600 Subject: [PATCH 063/126] optimize fullstack compile times --- packages/fullstack/Cargo.toml | 36 ++++++++----------- .../examples/axum-hello-world/Cargo.toml | 3 +- .../examples/salvo-hello-world/Cargo.toml | 3 +- .../examples/warp-hello-world/Cargo.toml | 3 +- .../fullstack/src/html_storage/deserialize.rs | 2 +- packages/fullstack/src/html_storage/mod.rs | 13 ++++--- .../fullstack/src/html_storage/serialize.rs | 13 +++---- packages/fullstack/src/render.rs | 9 +++-- packages/html/Cargo.toml | 21 ++++++----- packages/router/Cargo.toml | 5 +-- packages/rsx/Cargo.toml | 4 +-- packages/ssr/Cargo.toml | 5 ++- 12 files changed, 55 insertions(+), 62 deletions(-) diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 8de27a9b1..62dd46a5e 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -16,18 +16,14 @@ dioxus_server_macro = { workspace = true } # warp warp = { version = "0.3.5", features = ["compression-gzip"], optional = true } -http-body = { version = "0.4.5", optional = true } -http-body-util = "0.1.0-rc.2" # axum -axum = { version = "0.6.1", features = ["ws", "macros"], optional = true } +axum = { version = "0.6.1", features = ["ws", "macros"], default-features = false, optional = true } tower-http = { version = "0.4.0", optional = true, features = ["fs", "compression-gzip"] } -tower = { version = "0.4.13", features = ["util"], optional = true } -axum-macros = "0.3.7" # salvo salvo = { version = "0.46.0", optional = true, features = ["serve-static", "websocket", "compression"] } -serde = "1.0.159" +http-body-util = { version = "0.1.0-rc.2", optional = true } # Dioxus + SSR dioxus = { workspace = true } @@ -45,24 +41,25 @@ dioxus-desktop = { workspace = true, optional = true } dioxus-router = { workspace = true, optional = true } tracing = { workspace = true } -tracing-futures = { workspace = true } +tracing-futures = { workspace = true, optional = true } once_cell = "1.17.1" -thiserror = { workspace = true } -tokio = { workspace = true, features = ["full"], optional = true } +tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], optional = true } tokio-util = { version = "0.7.8", features = ["rt"], optional = true } -object-pool = "0.5.4" -anymap = "0.12.1" +anymap = { version = "0.12.1", optional = true } +serde = "1.0.159" serde_json = { version = "1.0.95", optional = true } tokio-stream = { version = "0.1.12", features = ["sync"], optional = true } -futures-util = { workspace = true, optional = true } -postcard = { version = "1.0.4", features = ["use-std"] } +futures-util = { workspace = true, default-features = false, optional = true } +ciborium = "0.2.1" base64 = "0.21.0" -pin-project = "1.1.2" +pin-project = { version = "1.1.2", optional = true } +thiserror = { workspace = true, optional = true } async-trait = "0.1.71" bytes = "1.4.0" -tower-layer = "0.3.2" +tower = { version = "0.4.13", features = ["util"], optional = true } +tower-layer = { version = "0.3.2", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dioxus-hot-reload = { workspace = true } @@ -71,17 +68,14 @@ dioxus-hot-reload = { workspace = true } web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] } [features] -default = ["hot-reload", "default-tls"] +default = ["hot-reload"] router = ["dioxus-router"] hot-reload = ["serde_json", "futures-util"] web = ["dioxus-web"] desktop = ["dioxus-desktop"] warp = ["dep:warp", "ssr"] axum = ["dep:axum", "tower-http", "ssr"] -salvo = ["dep:salvo", "ssr"] -ssr = ["server_fn/ssr", "dioxus_server_macro/ssr", "tokio", "tokio-util", "dioxus-ssr", "tower", "hyper", "http", "http-body", "dioxus-router/ssr", "tokio-stream"] +salvo = ["dep:salvo", "ssr", "http-body-util"] +ssr = ["server_fn/ssr", "dioxus_server_macro/ssr", "tokio", "tokio-util", "tokio-stream", "dioxus-ssr", "tower", "hyper", "http", "dioxus-router?/ssr", "tower-layer", "anymap", "tracing-futures", "pin-project", "thiserror"] default-tls = ["server_fn/default-tls"] rustls = ["server_fn/rustls"] - -[dev-dependencies] -dioxus-fullstack = { path = ".", features = ["router"] } diff --git a/packages/fullstack/examples/axum-hello-world/Cargo.toml b/packages/fullstack/examples/axum-hello-world/Cargo.toml index a3b68b05a..4dda8df5b 100644 --- a/packages/fullstack/examples/axum-hello-world/Cargo.toml +++ b/packages/fullstack/examples/axum-hello-world/Cargo.toml @@ -9,7 +9,6 @@ publish = false [dependencies] dioxus = { workspace = true } dioxus-fullstack = { workspace = true } -axum = { version = "0.6.12", optional = true } serde = "1.0.159" simple_logger = "4.2.0" tracing-wasm = "0.2.1" @@ -19,5 +18,5 @@ reqwest = "0.11.18" [features] default = [] -ssr = ["axum", "dioxus-fullstack/axum"] +ssr = ["dioxus-fullstack/axum"] web = ["dioxus-fullstack/web"] diff --git a/packages/fullstack/examples/salvo-hello-world/Cargo.toml b/packages/fullstack/examples/salvo-hello-world/Cargo.toml index ca0d85ecf..51dad9dff 100644 --- a/packages/fullstack/examples/salvo-hello-world/Cargo.toml +++ b/packages/fullstack/examples/salvo-hello-world/Cargo.toml @@ -10,7 +10,6 @@ publish = false dioxus-web = { workspace = true, features=["hydrate"], optional = true } dioxus = { workspace = true } dioxus-fullstack = { workspace = true } -tokio = { workspace = true, features = ["full"], optional = true } serde = "1.0.159" salvo = { version = "0.37.9", optional = true } execute = "0.2.12" @@ -22,5 +21,5 @@ tracing-subscriber = "0.3.17" [features] default = [] -ssr = ["salvo", "tokio", "dioxus-fullstack/salvo"] +ssr = ["salvo", "dioxus-fullstack/salvo"] web = ["dioxus-web"] diff --git a/packages/fullstack/examples/warp-hello-world/Cargo.toml b/packages/fullstack/examples/warp-hello-world/Cargo.toml index 1474facf3..c2246a58d 100644 --- a/packages/fullstack/examples/warp-hello-world/Cargo.toml +++ b/packages/fullstack/examples/warp-hello-world/Cargo.toml @@ -10,7 +10,6 @@ publish = false dioxus-web = { workspace = true, features=["hydrate"], optional = true } dioxus = { workspace = true } dioxus-fullstack = { workspace = true } -tokio = { workspace = true, features = ["full"], optional = true } serde = "1.0.159" warp = { version = "0.3.3", optional = true } execute = "0.2.12" @@ -22,5 +21,5 @@ tracing-subscriber = "0.3.17" [features] default = [] -ssr = ["warp", "tokio", "dioxus-fullstack/warp"] +ssr = ["warp", "dioxus-fullstack/warp"] web = ["dioxus-web"] diff --git a/packages/fullstack/src/html_storage/deserialize.rs b/packages/fullstack/src/html_storage/deserialize.rs index 7fc70ba6c..147f0d237 100644 --- a/packages/fullstack/src/html_storage/deserialize.rs +++ b/packages/fullstack/src/html_storage/deserialize.rs @@ -15,7 +15,7 @@ pub(crate) fn serde_from_bytes(string: &[u8]) -> Option } }; - match postcard::from_bytes(&decompressed) { + match ciborium::from_reader(std::io::Cursor::new(decompressed)) { Ok(data) => Some(data), Err(err) => { tracing::error!("Failed to deserialize: {}", err); diff --git a/packages/fullstack/src/html_storage/mod.rs b/packages/fullstack/src/html_storage/mod.rs index 5b4d2804f..a6ae2ebe1 100644 --- a/packages/fullstack/src/html_storage/mod.rs +++ b/packages/fullstack/src/html_storage/mod.rs @@ -1,6 +1,6 @@ #![allow(unused)] -use std::sync::atomic::AtomicUsize; +use std::{io::Cursor, sync::atomic::AtomicUsize}; use serde::{de::DeserializeOwned, Serialize}; @@ -15,7 +15,8 @@ pub(crate) struct HTMLData { impl HTMLData { pub(crate) fn push(&mut self, value: &T) { - let serialized = postcard::to_allocvec(value).unwrap(); + let mut serialized = Vec::new(); + serialize::serde_to_writable(value, &mut serialized).unwrap(); self.data.push(serialized); } @@ -45,7 +46,7 @@ impl HTMLDataCursor { } let mut cursor = &self.data[current]; self.index.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - match postcard::from_bytes(cursor) { + match ciborium::from_reader(Cursor::new(cursor)) { Ok(x) => Some(x), Err(e) => { tracing::error!("Error deserializing data: {:?}", e); @@ -57,7 +58,7 @@ impl HTMLDataCursor { #[test] fn serialized_and_deserializes() { - use postcard::to_allocvec; + use ciborium::{from_reader, into_writer}; #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] struct Data { @@ -97,7 +98,9 @@ fn serialized_and_deserializes() { "original size: {}", std::mem::size_of::() * data.len() ); - println!("serialized size: {}", to_allocvec(&data).unwrap().len()); + let mut bytes = Vec::new(); + into_writer(&data, &mut bytes).unwrap(); + println!("serialized size: {}", bytes.len()); println!("compressed size: {}", as_string.len()); let decoded: Vec = deserialize::serde_from_bytes(&as_string).unwrap(); diff --git a/packages/fullstack/src/html_storage/serialize.rs b/packages/fullstack/src/html_storage/serialize.rs index 7f0a7efc7..482864eb3 100644 --- a/packages/fullstack/src/html_storage/serialize.rs +++ b/packages/fullstack/src/html_storage/serialize.rs @@ -7,8 +7,9 @@ use base64::Engine; pub(crate) fn serde_to_writable( value: &T, write_to: &mut impl std::io::Write, -) -> std::io::Result<()> { - let serialized = postcard::to_allocvec(value).unwrap(); +) -> Result<(), ciborium::ser::Error> { + let mut serialized = Vec::new(); + ciborium::into_writer(value, &mut serialized)?; write_to.write_all(STANDARD.encode(serialized).as_bytes())?; Ok(()) } @@ -18,12 +19,12 @@ pub(crate) fn serde_to_writable( pub(crate) fn encode_props_in_element( data: &T, write_to: &mut impl std::io::Write, -) -> std::io::Result<()> { +) -> Result<(), ciborium::ser::Error> { write_to.write_all( r#""#.as_bytes()) + Ok(write_to.write_all(r#"" />"#.as_bytes())?) } #[cfg(feature = "ssr")] @@ -31,10 +32,10 @@ pub(crate) fn encode_props_in_element( pub(crate) fn encode_in_element( data: &super::HTMLData, write_to: &mut impl std::io::Write, -) -> std::io::Result<()> { +) -> Result<(), ciborium::ser::Error> { write_to.write_all( r#""#.as_bytes()) + Ok(write_to.write_all(r#"" />"#.as_bytes())?) } diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index cda41f82c..5cef0f5c2 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -63,7 +63,7 @@ impl SsrRendererPool { } if let Err(err) = renderer.render_to(&mut to, &vdom) { let _ = tx.send(Err( - dioxus_router::prelude::IncrementalRendererError::RenderError( + dioxus_ssr::incremental::IncrementalRendererError::RenderError( err, ), )); @@ -234,7 +234,9 @@ impl dioxus_ssr::incremental::Wrap to: &mut R, ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { // serialize the props - crate::html_storage::serialize::encode_props_in_element(&self.cfg.props, to)?; + crate::html_storage::serialize::encode_props_in_element(&self.cfg.props, to).map_err( + |err| dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)), + )?; // serialize the server state crate::html_storage::serialize::encode_in_element( &*self.server_context.html_data().map_err(|_| { @@ -256,7 +258,8 @@ impl dioxus_ssr::incremental::Wrap })) })?, to, - )?; + ) + .map_err(|err| dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)))?; #[cfg(all(debug_assertions, feature = "hot-reload"))] { diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 5644b440c..28b36b970 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -19,11 +19,10 @@ euclid = "0.22.7" enumset = "1.0.11" keyboard-types = "0.7" async-trait = "0.1.58" -serde-value = "0.7.0" +serde-value = { version = "0.7.0", optional = true } tokio = { workspace = true, features = ["fs", "io-util"], optional = true } rfd = { version = "0.11.3", optional = true } -async-channel = "1.8.0" -serde_json = { version = "1", optional = true } +serde_json = { version = "1" } [dependencies.web-sys] optional = true @@ -48,22 +47,22 @@ features = [ serde_json = "1" [features] -default = ["serialize", "mounted"] +default = ["mounted"] serialize = [ "serde", "serde_repr", - "serde_json", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize", + "serde-value", ] mounted = [ - "web-sys/Element", - "web-sys/DomRect", - "web-sys/ScrollIntoViewOptions", - "web-sys/ScrollLogicalPosition", - "web-sys/ScrollBehavior", - "web-sys/HtmlElement", + "web-sys?/Element", + "web-sys?/DomRect", + "web-sys?/ScrollIntoViewOptions", + "web-sys?/ScrollLogicalPosition", + "web-sys?/ScrollBehavior", + "web-sys?/HtmlElement", ] wasm-bind = ["web-sys", "wasm-bindgen"] native-bind = ["tokio"] diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index da9d3fe85..6307dacc9 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -10,13 +10,10 @@ homepage = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -anyhow = "1.0.66" dioxus = { workspace = true } dioxus-router-macro = { workspace = true } gloo = { version = "0.8.0", optional = true } tracing = { workspace = true } -thiserror = { workspace = true } -futures-util = { workspace = true } urlencoding = "2.1.3" serde = { version = "1", features = ["derive"], optional = true } serde_json = { version = "1.0.91", optional = true } @@ -36,7 +33,7 @@ default = ["web"] ssr = ["dioxus-ssr", "tokio"] liveview = ["dioxus-liveview", "tokio", "dep:serde", "serde_json"] wasm_test = [] -serde = ["dep:serde", "gloo-utils/serde"] +serde = ["dep:serde", "gloo-utils?/serde"] web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"] [dev-dependencies] diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml index bc866acb7..3ed47d944 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] proc-macro2 = { version = "1.0", features = ["span-locations"] } -dioxus-core = { workspace = true} +dioxus-core = { workspace = true, optional = true } syn = { version = "2.0", features = ["full", "extra-traits"] } quote = { version = "1.0" } serde = { version = "1.0", features = ["derive"], optional = true } @@ -22,5 +22,5 @@ internment = { version = "0.7.0", optional = true } krates = { version = "0.12.6", optional = true } [features] -hot_reload = ["krates", "internment"] +hot_reload = ["krates", "internment", "dioxus-core"] serde = ["dep:serde"] diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index f6376452d..ef9b6b5a6 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -9,18 +9,17 @@ repository = "https://github.com/DioxusLabs/dioxus/" keywords = ["dom", "ui", "gui", "react", "ssr"] [dependencies] -dioxus-core = { workspace = true, features = ["serialize"] } +dioxus-core = { workspace = true } askama_escape = "0.10.3" thiserror = "1.0.23" rustc-hash = "1.1.0" lru = "0.10.0" tracing = { workspace = true } http = "0.2.9" -tokio = { version = "1.28", features = ["full"], optional = true } +tokio = { version = "1.28", features = ["fs", "io-util"], optional = true } [dev-dependencies] dioxus = { workspace = true } -thiserror = { workspace = true } tracing = { workspace = true } fern = { version = "0.6.0", features = ["colored"] } anyhow = "1.0" From b3c7ebfdbab8bd4f88f4db61ecf706492c531a43 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 27 Nov 2023 16:12:48 -0600 Subject: [PATCH 064/126] fix merged changes --- packages/core/src/scopes.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 6123efddd..f54e3d5cf 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -351,20 +351,22 @@ impl<'src> ScopeState { let mut listeners = self.attributes_to_drop_before_render.borrow_mut(); for attr in element.dynamic_attrs { - match attr.value { - // We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped - AttributeValue::Listener(_) => { - let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; - listeners.push(unbounded); - } - // We need to drop any values manually to make sure that their drop implementation is called before the next render - AttributeValue::Any(_) => { - let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; - self.previous_frame().add_attribute_to_drop(unbounded); - } + attr.ty.for_each(|attr| { + match attr.value { + // We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped + AttributeValue::Listener(_) => { + let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; + listeners.push(unbounded); + } + // We need to drop any values manually to make sure that their drop implementation is called before the next render + AttributeValue::Any(_) => { + let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; + self.previous_frame().add_attribute_to_drop(unbounded); + } - _ => (), - } + _ => (), + } + }) } let mut props = self.borrowed_props.borrow_mut(); From 3feee729bcfc344cb7f4e1eaa64d2f16d6107cb8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 20 Dec 2023 09:53:41 -0600 Subject: [PATCH 065/126] enabled the liveview history by default if the liveview feature is enabled in the router --- packages/router/examples/simple_routes.rs | 41 ++++++++++++--------- packages/router/src/history/liveview.rs | 44 ++++++++++++++--------- packages/router/src/router_cfg.rs | 18 +++++++--- 3 files changed, 65 insertions(+), 38 deletions(-) diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs index aac1e8658..b30e0f3d7 100644 --- a/packages/router/examples/simple_routes.rs +++ b/packages/router/examples/simple_routes.rs @@ -54,10 +54,7 @@ fn main() { #[cfg(feature = "liveview")] #[component] fn Root(cx: Scope) -> Element { - let history = LiveviewHistory::new(cx); - render! { Router:: { - config: || RouterConfig::default().history(history), - } } + render! { Router:: {} } } #[cfg(not(feature = "liveview"))] @@ -84,12 +81,19 @@ fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: Strin "Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\tquery:{query},\n\textra:{extra}\n}}" } Link { - to: Route::Route1 { user_id: *user_id, dynamic: *dynamic, query: String::new(), extra: extra.clone() + "." }, + to: Route::Route1 { + user_id: *user_id, + dynamic: *dynamic, + query: String::new(), + extra: extra.clone() + ".", + }, "Route1 with extra+\".\"" } p { "Footer" } Link { - to: Route::Route3 { dynamic: String::new() }, + to: Route::Route3 { + dynamic: String::new(), + }, "Home" } } @@ -98,13 +102,13 @@ fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: Strin #[component] fn Route2(cx: Scope, user_id: usize) -> Element { render! { - pre { - "Route2{{\n\tuser_id:{user_id}\n}}" - } + pre { "Route2{{\n\tuser_id:{user_id}\n}}" } (0..*user_id).map(|i| rsx!{ p { "{i}" } }), p { "Footer" } Link { - to: Route::Route3 { dynamic: String::new() }, + to: Route::Route3 { + dynamic: String::new(), + }, "Home" } } @@ -130,22 +134,25 @@ fn Route3(cx: Scope, dynamic: String) -> Element { value: "{current_route_str.read()}" } "dynamic: {dynamic}" - Link { - to: Route::Route2 { user_id: 8888 }, - "hello world link" - } + Link { to: Route::Route2 { user_id: 8888 }, "hello world link" } button { disabled: !navigator.can_go_back(), - onclick: move |_| { navigator.go_back(); }, + onclick: move |_| { + navigator.go_back(); + }, "go back" } button { disabled: !navigator.can_go_forward(), - onclick: move |_| { navigator.go_forward(); }, + onclick: move |_| { + navigator.go_forward(); + }, "go forward" } button { - onclick: move |_| { navigator.push("https://www.google.com"); }, + onclick: move |_| { + navigator.push("https://www.google.com"); + }, "google link" } p { "Site Map" } diff --git a/packages/router/src/history/liveview.rs b/packages/router/src/history/liveview.rs index 9ffb09c34..354d259b1 100644 --- a/packages/router/src/history/liveview.rs +++ b/packages/router/src/history/liveview.rs @@ -132,6 +132,15 @@ where } } +impl Default for LiveviewHistory +where + ::Err: std::fmt::Display, +{ + fn default() -> Self { + Self::new() + } +} + impl LiveviewHistory where ::Err: std::fmt::Display, @@ -141,10 +150,9 @@ where /// /// # Panics /// - /// Panics if not in a Liveview context. - pub fn new(cx: &ScopeState) -> Self { + /// Panics if the function is not called in a dioxus runtime with a Liveview context. + pub fn new() -> Self { Self::new_with_initial_path( - cx, "/".parse().unwrap_or_else(|err| { panic!("index route does not exist:\n{}\n use LiveviewHistory::new_with_initial_path to set a custom path", err) }), @@ -156,17 +164,16 @@ where /// /// # Panics /// - /// Panics if not in a Liveview context. - pub fn new_with_initial_path(cx: &ScopeState, initial_path: R) -> Self { + /// Panics if the function is not called in a dioxus runtime with a Liveview context. + pub fn new_with_initial_path(initial_path: R) -> Self { let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel::>(); let action_rx = Arc::new(Mutex::new(action_rx)); let timeline = Arc::new(Mutex::new(Timeline::new(initial_path))); let updater_callback: Arc>> = Arc::new(RwLock::new(Arc::new(|| {}))); - let eval_provider = cx - .consume_context::>() - .expect("evaluator not provided"); + let eval_provider = + consume_context::>().expect("evaluator not provided"); let create_eval = Rc::new(move |script: &str| { eval_provider @@ -175,7 +182,7 @@ where }) as Rc Result>; // Listen to server actions - cx.push_future({ + push_future({ let timeline = timeline.clone(); let action_rx = action_rx.clone(); let create_eval = create_eval.clone(); @@ -235,7 +242,7 @@ where }); // Listen to browser actions - cx.push_future({ + push_future({ let updater = updater_callback.clone(); let timeline = timeline.clone(); let create_eval = create_eval.clone(); @@ -256,15 +263,16 @@ where Option, Option>, usize, - )>(init_eval).expect("serializable state"); + )>(init_eval) + .expect("serializable state"); let Ok(route) = R::from_str(&route.to_string()) else { return; }; let mut timeline = timeline.lock().expect("unpoisoned mutex"); let state = timeline.init(route.clone(), state, session, depth); let state = serde_json::to_string(&state).expect("serializable state"); - let session = serde_json::to_string(&timeline.session()) - .expect("serializable session"); + let session = + serde_json::to_string(&timeline.session()).expect("serializable session"); // Call the updater callback (updater.read().unwrap())(); @@ -288,22 +296,24 @@ where Ok(event) => event, Err(_) => continue, }; - let (route, state) = serde_json::from_value::<(String, Option)>(event).expect("serializable state"); + let (route, state) = serde_json::from_value::<(String, Option)>(event) + .expect("serializable state"); let Ok(route) = R::from_str(&route.to_string()) else { return; }; let mut timeline = timeline.lock().expect("unpoisoned mutex"); let state = timeline.update(route.clone(), state); let state = serde_json::to_string(&state).expect("serializable state"); - let session = serde_json::to_string(&timeline.session()) - .expect("serializable session"); + let session = + serde_json::to_string(&timeline.session()).expect("serializable session"); let _ = create_eval(&format!( r#" // this does not trigger a PopState event history.replaceState({state}, "", "{route}"); sessionStorage.setItem("liveview", '{session}'); - "#)); + "# + )); // Call the updater callback (updater.read().unwrap())(); diff --git a/packages/router/src/router_cfg.rs b/packages/router/src/router_cfg.rs index dd379e493..d243209f6 100644 --- a/packages/router/src/router_cfg.rs +++ b/packages/router/src/router_cfg.rs @@ -53,10 +53,15 @@ where { pub(crate) fn get_history(self) -> Box> { self.history.unwrap_or_else(|| { - #[cfg(all(target_arch = "wasm32", feature = "web"))] + #[cfg(all(not(feature = "liveview"), target_arch = "wasm32", feature = "web"))] let history = Box::>::default(); - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] + #[cfg(all( + not(feature = "liveview"), + any(not(target_arch = "wasm32"), not(feature = "web")) + ))] let history = Box::>::default(); + #[cfg(feature = "liveview")] + let history = Box::>::default(); history }) } @@ -83,10 +88,15 @@ where { pub(crate) fn take_history(&mut self) -> Box { self.history.take().unwrap_or_else(|| { - #[cfg(all(target_arch = "wasm32", feature = "web"))] + #[cfg(all(not(feature = "liveview"), target_arch = "wasm32", feature = "web"))] let history = Box::>>::default(); - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] + #[cfg(all( + not(feature = "liveview"), + any(not(target_arch = "wasm32"), not(feature = "web")) + ))] let history = Box::>>::default(); + #[cfg(feature = "liveview")] + let history = Box::>>::default(); history }) } From 9a6d74533967dd57bc7eda8a42626d2d616f5930 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 15:21:30 -0800 Subject: [PATCH 066/126] Refactor desktop into App type --- examples/mobile_demo/Cargo.toml | 2 +- packages/core/src/error_boundary.rs | 6 +- packages/desktop/Cargo.toml | 9 +- packages/desktop/src/app.rs | 569 ++++++++++++++++++++++++ packages/desktop/src/asset_handler.rs | 0 packages/desktop/src/cfg.rs | 9 +- packages/desktop/src/desktop_context.rs | 76 ++-- packages/desktop/src/events.rs | 22 +- packages/desktop/src/hooks.rs | 1 + packages/desktop/src/lib.rs | 550 ++--------------------- packages/desktop/src/protocol.rs | 10 +- packages/desktop/src/shortcut.rs | 2 +- packages/desktop/src/waker.rs | 4 +- packages/desktop/src/webview.rs | 111 ++--- packages/interpreter/Cargo.toml | 2 +- 15 files changed, 728 insertions(+), 645 deletions(-) create mode 100644 packages/desktop/src/app.rs create mode 100644 packages/desktop/src/asset_handler.rs create mode 100644 packages/desktop/src/hooks.rs diff --git a/examples/mobile_demo/Cargo.toml b/examples/mobile_demo/Cargo.toml index a90ff4f36..e980774e7 100644 --- a/examples/mobile_demo/Cargo.toml +++ b/examples/mobile_demo/Cargo.toml @@ -35,7 +35,7 @@ frameworks = ["WebKit"] [dependencies] anyhow = "1.0.56" log = "0.4.11" -wry = "0.34.0" +wry = "0.35.0" dioxus = { path = "../../packages/dioxus" } dioxus-desktop = { path = "../../packages/desktop", features = [ "tokio_runtime", diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index d42f59395..d4fe7c1ba 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -6,7 +6,7 @@ use crate::{ use std::{ any::{Any, TypeId}, backtrace::Backtrace, - cell::RefCell, + cell::{Cell, RefCell}, error::Error, fmt::{Debug, Display}, rc::Rc, @@ -472,8 +472,8 @@ pub fn ErrorBoundary<'a>(cx: Scope<'a, ErrorBoundaryProps<'a>>) -> Element { attr_paths: &[], }; VNode { - parent: Default::default(), - stable_id: Default::default(), + parent: Cell::new(None), + stable_id: Cell::new(None), key: None, template: std::cell::Cell::new(TEMPLATE), root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(), diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 1064d2c1e..4eed74b84 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -19,7 +19,10 @@ serde = "1.0.136" serde_json = "1.0.79" thiserror = { workspace = true } tracing = { workspace = true } -wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] } +wry = { version = "0.35.0", default-features = false, features = [ + "protocol", + "file-drop", +] } futures-channel = { workspace = true } tokio = { workspace = true, features = [ "sync", @@ -39,11 +42,13 @@ futures-util = { workspace = true } urlencoding = "2.1.2" async-trait = "0.1.68" crossbeam-channel = "0.5.8" +tao = { version = "0.24.0", features = ["rwh_05"] } +muda = "0.11.3" [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] rfd = "0.12" -global-hotkey = { git = "https://github.com/tauri-apps/global-hotkey" } +global-hotkey = "0.4.1" [target.'cfg(target_os = "ios")'.dependencies] objc = "0.2.7" diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs new file mode 100644 index 000000000..f88970f03 --- /dev/null +++ b/packages/desktop/src/app.rs @@ -0,0 +1,569 @@ +pub use crate::cfg::{Config, WindowCloseBehaviour}; +pub use crate::desktop_context::DesktopContext; +pub use crate::desktop_context::{ + use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId, +}; +use crate::desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; +use crate::events::{IpcMessage, KnownIpcMethod}; +use crate::file_upload; +use crate::query::QueryResult; +use crate::shortcut::GlobalHotKeyEvent; +use dioxus_core::*; +use dioxus_html::{event_bubbles, MountedData}; +use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; +use global_hotkey::{ + hotkey::{Code, HotKey, Modifiers}, + GlobalHotKeyManager, +}; +// use dioxus_interpreter_js::binary_protocol::Channel; +use crate::element::DesktopElement; +use crate::eval::init_eval; +pub use crate::protocol::{ + use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse, +}; +use crate::shortcut::ShortcutRegistry; +pub use crate::shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; +use futures_util::{pin_mut, FutureExt}; +use rustc_hash::FxHashMap; +use std::rc::Rc; +use std::sync::atomic::AtomicU16; +use std::task::Waker; +use std::{borrow::Borrow, cell::Cell}; +use std::{collections::HashMap, sync::Arc}; +pub use tao::dpi::{LogicalSize, PhysicalSize}; +use tao::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; +pub use tao::window::WindowBuilder; +use tao::window::WindowId; +use tao::{ + event::{Event, StartCause, WindowEvent}, + event_loop::ControlFlow, +}; +use tao::{event_loop::EventLoopBuilder, window::Window}; +use tokio::runtime::Builder; +pub use wry; +use wry::WebContext; +use wry::WebView; + +pub struct App

{ + // move the props into a cell so we can pop it out later to create the first window + // iOS panics if we create a window before the event loop is started + pub(crate) props: Rc>>, + pub(crate) cfg: Rc>>, + pub(crate) root: Component

, + pub(crate) webviews: HashMap, + pub(crate) event_handlers: WindowEventHandlers, + pub(crate) queue: WebviewQueue, + pub(crate) shortcut_manager: ShortcutRegistry, + pub(crate) global_hotkey_channel: crossbeam_channel::Receiver, + pub(crate) proxy: EventLoopProxy, + pub(crate) window_behavior: WindowCloseBehaviour, + pub(crate) control_flow: ControlFlow, + pub(crate) is_visible_before_start: bool, +} + +impl

App

{ + pub fn new(cfg: Config, props: P, root: Component

) -> (EventLoop, Self) { + let event_loop = EventLoopBuilder::::with_user_event().build(); + + let mut app = Self { + root, + window_behavior: cfg.last_window_close_behaviour, + is_visible_before_start: true, + webviews: HashMap::new(), + event_handlers: WindowEventHandlers::default(), + queue: WebviewQueue::default(), + shortcut_manager: ShortcutRegistry::new(), + global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(), + proxy: event_loop.create_proxy(), + props: Rc::new(Cell::new(Some(props))), + cfg: Rc::new(Cell::new(Some(cfg))), + control_flow: ControlFlow::Wait, + }; + + #[cfg(all(feature = "hot-reload", debug_assertions))] + app.connect_hotreload(); + + (event_loop, app) + } + + pub fn tick( + &mut self, + window_event: &Event<'_, UserWindowEvent>, + event_loop: &EventLoopWindowTarget, + ) { + // Set the control flow here, but make sure to update it at the end of the match + self.control_flow = ControlFlow::Wait; + + self.event_handlers.apply_event(window_event, event_loop); + + if let Ok(event) = self.global_hotkey_channel.try_recv() { + self.shortcut_manager.call_handlers(event); + } + } + + pub fn connect_hotreload(&mut self) { + let proxy = self.proxy.clone(); + dioxus_hot_reload::connect({ + move |template| { + let _ = proxy.send_event(UserWindowEvent( + EventData::HotReloadEvent(template), + unsafe { WindowId::dummy() }, + )); + } + }); + } + + // + pub fn handle_new_window(&mut self) { + for handler in self.queue.borrow_mut().drain(..) { + let id = handler.desktop_context.window.id(); + self.webviews.insert(id, handler); + _ = self.proxy.send_event(UserWindowEvent(EventData::Poll, id)); + } + } + + pub fn handle_close_requested(&mut self, id: WindowId) { + use WindowCloseBehaviour::*; + + match self.window_behavior { + LastWindowExitsApp => { + self.webviews.remove(&id); + if self.webviews.is_empty() { + self.control_flow = ControlFlow::Exit + } + } + + LastWindowHides => { + let Some(webview) = self.webviews.get(&id) else { + return; + }; + + hide_app_window(&webview.desktop_context.webview); + } + + CloseWindow => { + self.webviews.remove(&id); + } + } + } + + pub fn window_destroyed(&mut self, id: WindowId) { + self.webviews.remove(&id); + + if matches!( + self.window_behavior, + WindowCloseBehaviour::LastWindowExitsApp + ) && self.webviews.is_empty() + { + self.control_flow = ControlFlow::Exit + } + } + + pub fn handle_start_cause_init(&mut self, target: &EventLoopWindowTarget) { + let props = self.props.take().unwrap(); + let cfg = self.cfg.take().unwrap(); + + let dom = VirtualDom::new_with_props(self.root, props); + + self.is_visible_before_start = cfg.window.window.visible; + + let handler = create_new_window( + cfg, + target, + &self.proxy, + dom, + &self.queue, + &self.event_handlers, + self.shortcut_manager.clone(), + ); + + let id = handler.desktop_context.window.id(); + self.webviews.insert(id, handler); + + _ = self.proxy.send_event(UserWindowEvent(EventData::Poll, id)); + } + + pub fn handle_browser_open(&mut self, msg: IpcMessage) { + 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 { + tracing::error!("Open Browser error: {:?}", e); + } + } + } + } + + pub fn handle_initialize_msg(&mut self, id: WindowId) { + let view = self.webviews.get_mut(&id).unwrap(); + send_edits(view.dom.rebuild(), &view.desktop_context); + view.desktop_context + .window + .set_visible(self.is_visible_before_start); + } + + pub fn handle_close_msg(&mut self, id: WindowId) { + self.webviews.remove(&id); + + if self.webviews.is_empty() { + self.control_flow = ControlFlow::Exit + } + } + + pub fn handle_poll_msg(&mut self, id: WindowId) { + self.poll_vdom(id); + } + + pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) { + let Ok(result) = serde_json::from_value::(msg.params()) else { + return; + }; + + let Some(view) = self.webviews.get(&id) else { + return; + }; + + view.dom + .base_scope() + .consume_context::() + .unwrap() + .query + .send(result); + } + + pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) { + let params = msg.params(); + + let evt = match serde_json::from_value::(params) { + Ok(value) => value, + Err(err) => { + tracing::error!("Error parsing user_event: {:?}", err); + return; + } + }; + + let HtmlEvent { + element, + name, + bubbles, + data, + } = evt; + + let view = self.webviews.get_mut(&id).unwrap(); + + // check for a mounted event placeholder and replace it with a desktop specific element + let as_any = if let dioxus_html::EventData::Mounted = &data { + let query = view + .dom + .base_scope() + .consume_context::() + .unwrap() + .query + .clone(); + + let element = DesktopElement::new(element, view.desktop_context.clone(), query); + + Rc::new(MountedData::new(element)) + } else { + data.into_any() + }; + + view.dom.handle_event(&name, as_any, element, bubbles); + + send_edits(view.dom.render_immediate(), &view.desktop_context); + } + + #[cfg(all(feature = "hot-reload", debug_assertions))] + pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::HotReloadMsg) { + match msg { + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { + for webview in self.webviews.values_mut() { + webview.dom.replace_template(template); + + // poll_vdom(webview); + todo!() + } + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + self.control_flow = ControlFlow::Exit; + } + } + } + + pub fn handle_file_dialog_msg(&self, msg: IpcMessage, window: WindowId) { + if let Ok(file_diolog) = + serde_json::from_value::(msg.params()) + { + let id = ElementId(file_diolog.target); + let event_name = &file_diolog.event; + let event_bubbles = file_diolog.bubbles; + let files = file_upload::get_file_event(&file_diolog); + let data = Rc::new(FormData { + value: Default::default(), + values: Default::default(), + files: Some(Arc::new(NativeFileEngine::new(files))), + }); + + let view = self.webviews.get_mut(&window).unwrap(); + + if event_name == "change&input" { + view.dom + .handle_event("input", data.clone(), id, event_bubbles); + view.dom.handle_event("change", data, id, event_bubbles); + } else { + view.dom.handle_event(event_name, data, id, event_bubbles); + } + + todo!() + // send_edits(view.dom.render_immediate(), &view.desktop_context); + } + } + + /// 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 + pub fn poll_vdom(&mut self, id: WindowId) { + let view = self.webviews.get_mut(&id).unwrap(); + + let mut cx = std::task::Context::from_waker(&view.waker); + + loop { + { + let fut = view.dom.wait_for_work(); + pin_mut!(fut); + + match fut.poll_unpin(&mut cx) { + std::task::Poll::Ready(_) => {} + std::task::Poll::Pending => break, + } + } + + send_edits(view.dom.render_immediate(), &view.desktop_context); + } + } +} + +pub fn create_new_window( + mut cfg: Config, + event_loop: &EventLoopWindowTarget, + proxy: &EventLoopProxy, + dom: VirtualDom, + queue: &WebviewQueue, + event_handlers: &WindowEventHandlers, + shortcut_manager: ShortcutRegistry, +) -> WebviewHandler { + let (webview, web_context, asset_handlers, edit_queue, window) = + crate::webview::build(&mut cfg, event_loop, proxy.clone()); + + let desktop_context = Rc::from(DesktopService::new( + window, + webview, + proxy.clone(), + event_loop.clone(), + queue.clone(), + event_handlers.clone(), + shortcut_manager, + edit_queue, + asset_handlers, + )); + + let cx = dom.base_scope(); + cx.provide_context(desktop_context.clone()); + + // Init eval + init_eval(cx); + + WebviewHandler { + // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both + waker: crate::waker::tao_waker(proxy, desktop_context.window.id()), + desktop_context, + dom, + _web_context: web_context, + } +} + +pub struct WebviewHandler { + dom: VirtualDom, + desktop_context: DesktopContext, + waker: Waker, + + // Wry assumes the webcontext is alive for the lifetime of the webview. + // We need to keep the webcontext alive, otherwise the webview will crash + _web_context: WebContext, +} + +/// Send a list of mutations to the webview +pub fn send_edits(edits: Mutations, desktop_context: &DesktopContext) { + let mut channel = desktop_context.channel.borrow_mut(); + let mut templates = desktop_context.templates.borrow_mut(); + if let Some(bytes) = apply_edits( + edits, + &mut channel, + &mut templates, + &desktop_context.max_template_count, + ) { + desktop_context.edit_queue.add_edits(bytes) + } +} + +pub struct Channel {} + +pub fn apply_edits( + mutations: Mutations, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &AtomicU16, +) -> Option> { + use dioxus_core::Mutation::*; + if mutations.templates.is_empty() && mutations.edits.is_empty() { + return None; + } + for template in mutations.templates { + add_template(&template, channel, templates, max_template_count); + } + for edit in mutations.edits { + match edit { + AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16), + AssignId { path, id } => channel.assign_id(path, id.0 as u32), + CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32), + CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32), + HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32), + LoadTemplate { name, index, id } => { + if let Some(tmpl_id) = templates.get(name) { + channel.load_template(*tmpl_id, index as u16, id.0 as u32) + } + } + ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16), + ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16), + InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16), + InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16), + SetAttribute { + name, + value, + id, + ns, + } => match value { + BorrowedAttributeValue::Text(txt) => { + channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default()) + } + BorrowedAttributeValue::Float(f) => { + channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Int(n) => { + channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Bool(b) => channel.set_attribute( + id.0 as u32, + name, + if b { "true" } else { "false" }, + ns.unwrap_or_default(), + ), + BorrowedAttributeValue::None => { + channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default()) + } + _ => unreachable!(), + }, + SetText { value, id } => channel.set_text(id.0 as u32, value), + NewEventListener { name, id, .. } => { + channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + RemoveEventListener { name, id } => { + channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + Remove { id } => channel.remove(id.0 as u32), + PushRoot { id } => channel.push_root(id.0 as u32), + } + } + + let bytes: Vec<_> = channel.export_memory().collect(); + channel.reset(); + Some(bytes) +} + +pub fn add_template( + template: &Template<'static>, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &AtomicU16, +) { + let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed); + for root in template.roots.iter() { + create_template_node(channel, root); + templates.insert(template.name.to_owned(), current_max_template_count); + } + channel.add_templates(current_max_template_count, template.roots.len() as u16); + + max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); +} + +pub fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { + use TemplateNode::*; + match v { + Element { + tag, + namespace, + attrs, + children, + .. + } => { + // Push the current node onto the stack + match namespace { + Some(ns) => channel.create_element_ns(tag, ns), + None => channel.create_element(tag), + } + // Set attributes on the current node + for attr in *attrs { + if let TemplateAttribute::Static { + name, + value, + namespace, + } = attr + { + channel.set_top_attribute(name, value, namespace.unwrap_or_default()) + } + } + // Add each child to the stack + for child in *children { + create_template_node(channel, child); + } + // Add all children to the parent + channel.append_children_to_top(children.len() as u16); + } + Text { text } => channel.create_raw_text(text), + DynamicText { .. } => channel.create_raw_text("p"), + Dynamic { .. } => channel.add_placeholder(), + } +} + +/// Different hide implementations per platform +#[allow(unused)] +pub fn hide_app_window(webview: &WebView) { + #[cfg(target_os = "windows")] + { + use wry::application::platform::windows::WindowExtWindows; + window.set_visible(false); + window.set_skip_taskbar(true); + } + + #[cfg(target_os = "linux")] + { + use wry::application::platform::unix::WindowExtUnix; + window.set_visible(false); + } + + #[cfg(target_os = "macos")] + { + // window.set_visible(false); has the wrong behaviour on macOS + // It will hide the window but not show it again when the user switches + // back to the app. `NSApplication::hide:` has the correct behaviour + use objc::runtime::Object; + use objc::{msg_send, sel, sel_impl}; + objc::rc::autoreleasepool(|| unsafe { + let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication]; + let nil = std::ptr::null_mut::(); + let _: () = msg_send![app, hide: nil]; + }); + } +} diff --git a/packages/desktop/src/asset_handler.rs b/packages/desktop/src/asset_handler.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 596b21938..9d1854bdf 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,11 +1,10 @@ use std::borrow::Cow; use std::path::PathBuf; -use wry::application::window::Icon; +use tao::window::{Icon, Window, WindowBuilder}; use wry::{ - application::window::{Window, WindowBuilder}, http::{Request as HttpRequest, Response as HttpResponse}, - webview::FileDropEvent, + FileDropEvent, }; // pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); @@ -38,7 +37,7 @@ pub struct Config { pub(crate) enable_default_menu_bar: bool, } -type DropHandler = Box bool>; +type DropHandler = Box bool>; pub(crate) type WryProtocol = ( String, @@ -120,7 +119,7 @@ impl Config { /// Set a file drop handler pub fn with_file_drop_handler( mut self, - handler: impl Fn(&Window, FileDropEvent) -> bool + 'static, + handler: impl Fn(FileDropEvent) -> bool + 'static, ) -> Self { self.file_drop_handler = Some(Box::new(handler)); self diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 62a574d9b..763c00c39 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,38 +1,29 @@ -use crate::create_new_window; -use crate::events::IpcMessage; use crate::protocol::AssetFuture; use crate::protocol::AssetHandlerRegistry; use crate::query::QueryEngine; use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; use crate::AssetHandler; use crate::Config; -use crate::WebviewHandler; +use crate::{app::WebviewHandler, events::IpcMessage}; use dioxus_core::ScopeState; use dioxus_core::VirtualDom; -#[cfg(all(feature = "hot-reload", debug_assertions))] -use dioxus_hot_reload::HotReloadMsg; use dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; use slab::Slab; -use std::cell::RefCell; -use std::fmt::Debug; -use std::fmt::Formatter; -use std::rc::Rc; -use std::rc::Weak; -use std::sync::atomic::AtomicU16; -use std::sync::Arc; -use std::sync::Mutex; -use wry::application::event::Event; -use wry::application::event_loop::EventLoopProxy; -use wry::application::event_loop::EventLoopWindowTarget; -#[cfg(target_os = "ios")] -use wry::application::platform::ios::WindowExtIOS; -use wry::application::window::Fullscreen as WryFullscreen; -use wry::application::window::Window; -use wry::application::window::WindowId; -use wry::webview::WebView; +use std::{ + cell::RefCell, fmt::Debug, fmt::Formatter, rc::Rc, rc::Weak, sync::atomic::AtomicU16, + sync::Arc, sync::Mutex, +}; +use tao::{ + event::Event, + event_loop::{EventLoopProxy, EventLoopWindowTarget}, + window::{Fullscreen as WryFullscreen, Window, WindowId}, +}; -pub type ProxyType = EventLoopProxy; +use wry::{RequestAsyncResponder, WebView}; + +#[cfg(target_os = "ios")] +use tao::platform::ios::WindowExtIOS; /// Get an imperative handle to the current window without using a hook /// @@ -44,7 +35,6 @@ pub fn window() -> DesktopContext { } /// Get an imperative handle to the current window -#[deprecated = "Prefer the using the `window` function directly for cleaner code"] pub fn use_window(cx: &ScopeState) -> &DesktopContext { cx.use_hook(|| cx.consume_context::()) .as_ref() @@ -56,7 +46,7 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { #[derive(Default, Clone)] pub(crate) struct EditQueue { queue: Arc>>>, - responder: Arc>>, + responder: Arc>>, } impl Debug for EditQueue { @@ -71,7 +61,7 @@ impl Debug for EditQueue { } impl EditQueue { - pub fn handle_request(&self, responder: wry::webview::RequestAsyncResponder) { + pub fn handle_request(&self, responder: RequestAsyncResponder) { let mut queue = self.queue.lock().unwrap(); if let Some(bytes) = queue.pop() { responder.respond(wry::http::Response::new(bytes)); @@ -106,10 +96,12 @@ pub(crate) type WebviewQueue = Rc>>; /// ``` pub struct DesktopService { /// The wry/tao proxy to the current window - pub webview: Rc, + pub webview: WebView, + + pub window: Window, /// The proxy to the event loop - pub proxy: ProxyType, + pub proxy: EventLoopProxy, /// The receiver for queries about the current window pub(super) query: QueryEngine, @@ -141,14 +133,15 @@ impl std::ops::Deref for DesktopService { type Target = Window; fn deref(&self) -> &Self::Target { - self.webview.window() + &self.window } } impl DesktopService { pub(crate) fn new( + window: Window, webview: WebView, - proxy: ProxyType, + proxy: EventLoopProxy, event_loop: EventLoopWindowTarget, webviews: WebviewQueue, event_handlers: WindowEventHandlers, @@ -157,7 +150,8 @@ impl DesktopService { asset_handlers: AssetHandlerRegistry, ) -> Self { Self { - webview: Rc::new(webview), + window, + webview, proxy, event_loop, query: Default::default(), @@ -198,7 +192,7 @@ impl DesktopService { .consume_context::>() .unwrap(); - let id = window.desktop_context.webview.window().id(); + let id = window.desktop_context.window.id(); self.proxy .send_event(UserWindowEvent(EventData::NewWindow, id)) @@ -222,7 +216,7 @@ impl DesktopService { /// onmousedown: move |_| { desktop.drag_window(); } /// ``` pub fn drag(&self) { - let window = self.webview.window(); + let window = &self.window; // if the drag_window has any errors, we don't do anything if window.fullscreen().is_none() { @@ -232,7 +226,7 @@ impl DesktopService { /// Toggle whether the window is maximized or not pub fn toggle_maximized(&self) { - let window = self.webview.window(); + let window = &self.window; window.set_maximized(!window.is_maximized()) } @@ -253,10 +247,10 @@ impl DesktopService { /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { - if let Some(handle) = self.webview.window().current_monitor() { - self.webview - .window() - .set_fullscreen(fullscreen.then_some(WryFullscreen::Borderless(Some(handle)))); + if let Some(handle) = &self.window.current_monitor() { + self.window.set_fullscreen( + fullscreen.then_some(WryFullscreen::Borderless(Some(handle.clone()))), + ); } } @@ -336,7 +330,7 @@ impl DesktopService { /// 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(); + let window = &self.window; unsafe { use objc::runtime::Object; @@ -356,7 +350,7 @@ impl DesktopService { /// Pop an objc view from the window #[cfg(target_os = "ios")] pub fn pop_view(&self) { - let window = self.webview.window(); + let window = &self.window; unsafe { use objc::runtime::Object; @@ -380,7 +374,7 @@ pub enum EventData { Ipc(IpcMessage), #[cfg(all(feature = "hot-reload", debug_assertions))] - HotReloadEvent(HotReloadMsg), + HotReloadEvent(dioxus_hot_reload::HotReloadMsg), NewWindow, diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 7059a7c85..a877b52d6 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -8,9 +8,27 @@ pub struct IpcMessage { params: serde_json::Value, } +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum KnownIpcMethod<'a> { + FileDialog, + UserEvent, + Query, + BrowserOpen, + Initialize, + Other(&'a str), +} + impl IpcMessage { - pub(crate) fn method(&self) -> &str { - self.method.as_str() + pub(crate) fn method(&self) -> KnownIpcMethod { + match self.method.as_str() { + // todo: this is a misspelling + "file_diolog" => KnownIpcMethod::FileDialog, + "user_event" => KnownIpcMethod::UserEvent, + "query" => KnownIpcMethod::Query, + "browser_open" => KnownIpcMethod::BrowserOpen, + "initialize" => KnownIpcMethod::Initialize, + _ => KnownIpcMethod::Other(&self.method), + } } pub(crate) fn params(self) -> serde_json::Value { diff --git a/packages/desktop/src/hooks.rs b/packages/desktop/src/hooks.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/desktop/src/hooks.rs @@ -0,0 +1 @@ + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 9460ca87d..1fc3e99c0 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -3,6 +3,8 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![deny(missing_docs)] +mod app; +mod asset_handler; mod cfg; mod desktop_context; mod element; @@ -10,6 +12,7 @@ mod escape; mod eval; mod events; mod file_upload; +mod hooks; #[cfg(any(target_os = "ios", target_os = "android"))] mod mobile_shortcut; mod protocol; @@ -18,44 +21,24 @@ mod shortcut; mod waker; mod webview; -use crate::query::QueryResult; -use crate::shortcut::GlobalHotKeyEvent; pub use cfg::{Config, WindowCloseBehaviour}; +pub use desktop_context::use_window; pub use desktop_context::DesktopContext; #[allow(deprecated)] pub use desktop_context::{ - use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId, + use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId, }; -use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; +use desktop_context::{EventData, UserWindowEvent}; use dioxus_core::*; -use dioxus_html::{event_bubbles, MountedData}; -use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; -use dioxus_interpreter_js::binary_protocol::Channel; -use element::DesktopElement; -use eval::init_eval; -use futures_util::{pin_mut, FutureExt}; +use events::KnownIpcMethod; pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse}; -use rustc_hash::FxHashMap; -use shortcut::ShortcutRegistry; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; -use std::cell::Cell; -use std::rc::Rc; -use std::sync::atomic::AtomicU16; -use std::task::Waker; -use std::{collections::HashMap, sync::Arc}; pub use tao::dpi::{LogicalSize, PhysicalSize}; -use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; +use tao::event::{Event, StartCause, WindowEvent}; pub use tao::window::WindowBuilder; -use tao::{ - event::{Event, StartCause, WindowEvent}, - event_loop::ControlFlow, -}; -// pub use webview::build_default_menu_bar; +use tokio::runtime::Builder; +pub use webview::build_default_menu_bar; pub use wry; -pub use wry::application as tao; -use wry::application::event_loop::EventLoopBuilder; -use wry::webview::WebView; -use wry::{application::window::WindowId, webview::WebContext}; /// Launch the WebView and run the event loop. /// @@ -127,512 +110,47 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// } /// ``` pub fn launch_with_props(root: Component

, props: P, cfg: Config) { - let event_loop = EventLoopBuilder::::with_user_event().build(); - - let proxy = event_loop.create_proxy(); - - let window_behaviour = cfg.last_window_close_behaviour; - - // Intialize hot reloading if it is enabled - #[cfg(all(feature = "hot-reload", debug_assertions))] - dioxus_hot_reload::connect({ - let proxy = proxy.clone(); - move |template| { - let _ = proxy.send_event(UserWindowEvent( - EventData::HotReloadEvent(template), - unsafe { WindowId::dummy() }, - )); - } - }); - // 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() + // I would love to just allow dioxus to work with any runtime... but tokio is weird + let _guard = Builder::new_multi_thread() .enable_all() .build() - .unwrap(); + .unwrap() + .enter(); - // We enter the runtime but we poll futures manually, circumventing the per-task runtime budget - let _guard = rt.enter(); - - // 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(); - - // We use this to allow dynamically adding and removing window event handlers - let event_handlers = WindowEventHandlers::default(); - - let queue = WebviewQueue::default(); - - let shortcut_manager = ShortcutRegistry::new(); - let global_hotkey_channel = GlobalHotKeyEvent::receiver(); - - // move the props into a cell so we can pop it out later to create the first window - // iOS panics if we create a window before the event loop is started - let props = Rc::new(Cell::new(Some(props))); - let cfg = Rc::new(Cell::new(Some(cfg))); - let mut is_visible_before_start = true; + let (event_loop, mut app) = app::App::new(cfg, props, root); event_loop.run(move |window_event, event_loop, control_flow| { - *control_flow = ControlFlow::Poll; - - event_handlers.apply_event(&window_event, event_loop); - - if let Ok(event) = global_hotkey_channel.try_recv() { - shortcut_manager.call_handlers(event); - } + app.tick(&window_event, event_loop); match window_event { + Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(event_loop), Event::WindowEvent { event, window_id, .. } => match event { - WindowEvent::CloseRequested => match window_behaviour { - cfg::WindowCloseBehaviour::LastWindowExitsApp => { - webviews.remove(&window_id); - - if webviews.is_empty() { - *control_flow = ControlFlow::Exit - } - } - cfg::WindowCloseBehaviour::LastWindowHides => { - let Some(webview) = webviews.get(&window_id) else { - return; - }; - hide_app_window(&webview.desktop_context.webview); - } - cfg::WindowCloseBehaviour::CloseWindow => { - webviews.remove(&window_id); - } - }, - WindowEvent::Destroyed { .. } => { - webviews.remove(&window_id); - - if matches!( - window_behaviour, - cfg::WindowCloseBehaviour::LastWindowExitsApp - ) && webviews.is_empty() - { - *control_flow = ControlFlow::Exit - } - } + WindowEvent::CloseRequested => app.handle_close_requested(window_id), + WindowEvent::Destroyed { .. } => app.window_destroyed(window_id), _ => {} }, - - Event::NewEvents(StartCause::Init) => { - let props = props.take().unwrap(); - let cfg = cfg.take().unwrap(); - - // Create a dom - let dom = VirtualDom::new_with_props(root, props); - - is_visible_before_start = cfg.window.window.visible; - - let handler = create_new_window( - cfg, - event_loop, - &proxy, - dom, - &queue, - &event_handlers, - shortcut_manager.clone(), - ); - - let id = handler.desktop_context.webview.window().id(); - webviews.insert(id, handler); - _ = proxy.send_event(UserWindowEvent(EventData::Poll, id)); - } - - Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => { - for handler in queue.borrow_mut().drain(..) { - let id = handler.desktop_context.webview.window().id(); - webviews.insert(id, handler); - _ = proxy.send_event(UserWindowEvent(EventData::Poll, id)); - } - } - - Event::UserEvent(event) => match event.0 { - #[cfg(all(feature = "hot-reload", debug_assertions))] - EventData::HotReloadEvent(msg) => match msg { - dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { - for webview in webviews.values_mut() { - webview.dom.replace_template(template); - - poll_vdom(webview); - } - } - dioxus_hot_reload::HotReloadMsg::Shutdown => { - *control_flow = ControlFlow::Exit; - } + Event::UserEvent(UserWindowEvent(event, id)) => match event { + EventData::Poll => app.handle_poll_msg(id), + EventData::NewWindow => app.handle_new_window(), + EventData::CloseWindow => app.handle_close_msg(id), + EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), + EventData::Ipc(msg) => match msg.method() { + KnownIpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), + KnownIpcMethod::UserEvent => app.handle_user_event_msg(msg, id), + KnownIpcMethod::Query => app.handle_query_msg(msg, id), + KnownIpcMethod::BrowserOpen => app.handle_browser_open(msg), + KnownIpcMethod::Initialize => app.handle_initialize_msg(id), + KnownIpcMethod::Other(_) => {} }, - - EventData::CloseWindow => { - webviews.remove(&event.1); - - if webviews.is_empty() { - *control_flow = ControlFlow::Exit - } - } - - EventData::Poll => { - if let Some(view) = webviews.get_mut(&event.1) { - poll_vdom(view); - } - } - - EventData::Ipc(msg) if msg.method() == "user_event" => { - let params = msg.params(); - - let evt = match serde_json::from_value::(params) { - Ok(value) => value, - Err(err) => { - tracing::error!("Error parsing user_event: {:?}", err); - return; - } - }; - - let HtmlEvent { - element, - name, - bubbles, - data, - } = evt; - - let view = webviews.get_mut(&event.1).unwrap(); - - // check for a mounted event placeholder and replace it with a desktop specific element - let as_any = if let dioxus_html::EventData::Mounted = &data { - let query = view - .dom - .base_scope() - .consume_context::() - .unwrap() - .query - .clone(); - - let element = - DesktopElement::new(element, view.desktop_context.clone(), query); - - Rc::new(MountedData::new(element)) - } else { - data.into_any() - }; - - view.dom.handle_event(&name, as_any, element, bubbles); - - send_edits(view.dom.render_immediate(), &view.desktop_context); - } - - // When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query - EventData::Ipc(msg) if msg.method() == "query" => { - let params = msg.params(); - - if let Ok(result) = serde_json::from_value::(params) { - let view = webviews.get(&event.1).unwrap(); - let query = view - .dom - .base_scope() - .consume_context::() - .unwrap() - .query - .clone(); - - query.send(result); - } - } - - EventData::Ipc(msg) if msg.method() == "initialize" => { - let view = webviews.get_mut(&event.1).unwrap(); - send_edits(view.dom.rebuild(), &view.desktop_context); - view.desktop_context - .webview - .window() - .set_visible(is_visible_before_start); - } - - EventData::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 { - tracing::error!("Open Browser error: {:?}", e); - } - } - } - } - - EventData::Ipc(msg) if msg.method() == "file_diolog" => { - if let Ok(file_diolog) = - serde_json::from_value::(msg.params()) - { - let id = ElementId(file_diolog.target); - let event_name = &file_diolog.event; - let event_bubbles = file_diolog.bubbles; - let files = file_upload::get_file_event(&file_diolog); - let data = Rc::new(FormData { - value: Default::default(), - values: Default::default(), - files: Some(Arc::new(NativeFileEngine::new(files))), - }); - - let view = webviews.get_mut(&event.1).unwrap(); - - if event_name == "change&input" { - view.dom - .handle_event("input", data.clone(), id, event_bubbles); - view.dom.handle_event("change", data, id, event_bubbles); - } else { - view.dom.handle_event(event_name, data, id, event_bubbles); - } - - send_edits(view.dom.render_immediate(), &view.desktop_context); - } - } - - _ => {} }, _ => {} } + + // Update the control flow + *control_flow = app.control_flow; }) } - -fn create_new_window( - mut cfg: Config, - event_loop: &EventLoopWindowTarget, - proxy: &EventLoopProxy, - dom: VirtualDom, - queue: &WebviewQueue, - event_handlers: &WindowEventHandlers, - shortcut_manager: ShortcutRegistry, -) -> WebviewHandler { - let (webview, web_context, asset_handlers, edit_queue) = - webview::build(&mut cfg, event_loop, proxy.clone()); - let desktop_context = Rc::from(DesktopService::new( - webview, - proxy.clone(), - event_loop.clone(), - queue.clone(), - event_handlers.clone(), - shortcut_manager, - edit_queue, - asset_handlers, - )); - - let cx = dom.base_scope(); - cx.provide_context(desktop_context.clone()); - - // Init eval - init_eval(cx); - - WebviewHandler { - // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both - waker: waker::tao_waker(proxy, desktop_context.webview.window().id()), - desktop_context, - dom, - _web_context: web_context, - } -} - -struct WebviewHandler { - dom: VirtualDom, - desktop_context: DesktopContext, - waker: Waker, - - // Wry assumes the webcontext is alive for the lifetime of the webview. - // We need to keep the webcontext alive, otherwise the webview will crash - _web_context: WebContext, -} - -/// 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(view: &mut WebviewHandler) { - let mut cx = std::task::Context::from_waker(&view.waker); - - loop { - { - let fut = view.dom.wait_for_work(); - pin_mut!(fut); - - match fut.poll_unpin(&mut cx) { - std::task::Poll::Ready(_) => {} - std::task::Poll::Pending => break, - } - } - - send_edits(view.dom.render_immediate(), &view.desktop_context); - } -} - -/// Send a list of mutations to the webview -fn send_edits(edits: Mutations, desktop_context: &DesktopContext) { - let mut channel = desktop_context.channel.borrow_mut(); - let mut templates = desktop_context.templates.borrow_mut(); - if let Some(bytes) = apply_edits( - edits, - &mut channel, - &mut templates, - &desktop_context.max_template_count, - ) { - desktop_context.edit_queue.add_edits(bytes) - } -} - -fn apply_edits( - mutations: Mutations, - channel: &mut Channel, - templates: &mut FxHashMap, - max_template_count: &AtomicU16, -) -> Option> { - use dioxus_core::Mutation::*; - if mutations.templates.is_empty() && mutations.edits.is_empty() { - return None; - } - for template in mutations.templates { - add_template(&template, channel, templates, max_template_count); - } - for edit in mutations.edits { - match edit { - AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16), - AssignId { path, id } => channel.assign_id(path, id.0 as u32), - CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32), - CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32), - HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32), - LoadTemplate { name, index, id } => { - if let Some(tmpl_id) = templates.get(name) { - channel.load_template(*tmpl_id, index as u16, id.0 as u32) - } - } - ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16), - ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16), - InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16), - InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16), - SetAttribute { - name, - value, - id, - ns, - } => match value { - BorrowedAttributeValue::Text(txt) => { - channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default()) - } - BorrowedAttributeValue::Float(f) => { - channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default()) - } - BorrowedAttributeValue::Int(n) => { - channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default()) - } - BorrowedAttributeValue::Bool(b) => channel.set_attribute( - id.0 as u32, - name, - if b { "true" } else { "false" }, - ns.unwrap_or_default(), - ), - BorrowedAttributeValue::None => { - channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default()) - } - _ => unreachable!(), - }, - SetText { value, id } => channel.set_text(id.0 as u32, value), - NewEventListener { name, id, .. } => { - channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8) - } - RemoveEventListener { name, id } => { - channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) - } - Remove { id } => channel.remove(id.0 as u32), - PushRoot { id } => channel.push_root(id.0 as u32), - } - } - - let bytes: Vec<_> = channel.export_memory().collect(); - channel.reset(); - Some(bytes) -} - -fn add_template( - template: &Template<'static>, - channel: &mut Channel, - templates: &mut FxHashMap, - max_template_count: &AtomicU16, -) { - let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed); - for root in template.roots.iter() { - create_template_node(channel, root); - templates.insert(template.name.to_owned(), current_max_template_count); - } - channel.add_templates(current_max_template_count, template.roots.len() as u16); - - max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); -} - -fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { - use TemplateNode::*; - match v { - Element { - tag, - namespace, - attrs, - children, - .. - } => { - // Push the current node onto the stack - match namespace { - Some(ns) => channel.create_element_ns(tag, ns), - None => channel.create_element(tag), - } - // Set attributes on the current node - for attr in *attrs { - if let TemplateAttribute::Static { - name, - value, - namespace, - } = attr - { - channel.set_top_attribute(name, value, namespace.unwrap_or_default()) - } - } - // Add each child to the stack - for child in *children { - create_template_node(channel, child); - } - // Add all children to the parent - channel.append_children_to_top(children.len() as u16); - } - Text { text } => channel.create_raw_text(text), - DynamicText { .. } => channel.create_raw_text("p"), - Dynamic { .. } => channel.add_placeholder(), - } -} - -/// Different hide implementations per platform -#[allow(unused)] -fn hide_app_window(webview: &WebView) { - #[cfg(target_os = "windows")] - { - use wry::application::platform::windows::WindowExtWindows; - webview.window().set_visible(false); - webview.window().set_skip_taskbar(true); - } - - #[cfg(target_os = "linux")] - { - use wry::application::platform::unix::WindowExtUnix; - webview.window().set_visible(false); - } - - #[cfg(target_os = "macos")] - { - // webview.window().set_visible(false); has the wrong behaviour on macOS - // It will hide the window but not show it again when the user switches - // back to the app. `NSApplication::hide:` has the correct behaviour - use objc::runtime::Object; - use objc::{msg_send, sel, sel_impl}; - objc::rc::autoreleasepool(|| unsafe { - let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication]; - let nil = std::ptr::null_mut::(); - let _: () = msg_send![app, hide: nil]; - }); - } -} diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 61c6af661..36400ce95 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,4 +1,4 @@ -use crate::{window, DesktopContext}; +use crate::{use_window, DesktopContext}; use dioxus_core::ScopeState; use dioxus_interpreter_js::INTERPRETER_JS; use slab::Slab; @@ -17,7 +17,7 @@ use tokio::{ }; use wry::{ http::{status::StatusCode, Request, Response}, - Result, + RequestAsyncResponder, Result, }; use crate::desktop_context::EditQueue; @@ -259,8 +259,7 @@ pub(super) async fn desktop_handler( .body(Cow::from(body)) { Ok(response) => { - responder.respond(response); - return; + return Ok(response); } Err(err) => tracing::error!("error building response: {}", err), } @@ -307,8 +306,7 @@ pub(super) async fn desktop_handler( .body(Cow::from(asset)) { Ok(response) => { - responder.respond(response); - return; + return response; } Err(err) => tracing::error!("error building response: {}", err), } diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs index bf8ed1382..55d49b405 100644 --- a/packages/desktop/src/shortcut.rs +++ b/packages/desktop/src/shortcut.rs @@ -3,7 +3,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr}; use dioxus_core::ScopeState; use dioxus_html::input_data::keyboard_types::Modifiers; use slab::Slab; -use wry::application::keyboard::ModifiersState; +use tao::keyboard::ModifiersState; use crate::{desktop_context::DesktopContext, window}; diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index 1e34baffc..11f23d8d8 100644 --- a/packages/desktop/src/waker.rs +++ b/packages/desktop/src/waker.rs @@ -1,7 +1,7 @@ use crate::desktop_context::{EventData, UserWindowEvent}; -use futures_util::task::ArcWake; +use futures_util::{pin_mut, task::ArcWake, FutureExt}; use std::sync::Arc; -use wry::application::{event_loop::EventLoopProxy, window::WindowId}; +use tao::{event_loop::EventLoopProxy, window::WindowId}; /// Create a waker that will send a poll event to the event loop. /// diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index de5ff121c..d98e60099 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,20 +1,18 @@ use crate::desktop_context::{EditQueue, EventData}; use crate::protocol::{self, AssetHandlerRegistry}; use crate::{desktop_context::UserWindowEvent, Config}; +use muda::{Menu, MenuItem, PredefinedMenuItem, Submenu}; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; -pub use wry; -pub use wry::application as tao; -use wry::application::window::Window; +use tao::window::Window; use wry::http::Response; -use wry::webview::{WebContext, WebView, WebViewBuilder}; +use wry::{WebContext, WebView, WebViewBuilder}; pub(crate) fn build( cfg: &mut Config, event_loop: &EventLoopWindowTarget, proxy: EventLoopProxy, -) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue) { +) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue, Window) { let builder = cfg.window.clone(); - let window = builder.with_visible(false).build(event_loop).unwrap(); let file_handler = cfg.file_drop_handler.take(); let custom_head = cfg.custom_head.clone(); let index_file = cfg.custom_index.clone(); @@ -43,15 +41,14 @@ pub(crate) fn build( let asset_handlers = AssetHandlerRegistry::new(); let asset_handlers_ref = asset_handlers.clone(); - let mut webview = WebViewBuilder::new(window) - .unwrap() + let mut webview = WebViewBuilder::new(&window) .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() - .with_ipc_handler(move |window: &Window, payload: String| { + .with_ipc_handler(move |payload: String| { // defer the event to the main thread if let Ok(message) = serde_json::from_str(&payload) { - _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id())); + _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window_id)); } }) .with_asynchronous_custom_protocol(String::from("dioxus"), { @@ -77,10 +74,10 @@ pub(crate) fn build( }); } }) - .with_file_drop_handler(move |window, evet| { + .with_file_drop_handler(move |event| { file_handler .as_ref() - .map(|handler| handler(window, evet)) + .map(|handler| handler(event)) .unwrap_or_default() }) .with_web_context(&mut web_context); @@ -129,63 +126,47 @@ pub(crate) fn build( web_context, asset_handlers, edit_queue, + window, ) } -// /// Builds a standard menu bar depending on the users platform. It may be used as a starting point -// /// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder). -// /// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste. -// /// > The menu bar differs per platform because of constraints introduced -// /// > by [`MenuItem`](tao::menu::MenuItem). -// pub fn build_default_menu_bar() -> MenuBar { -// let mut menu_bar = MenuBar::new(); +/// Builds a standard menu bar depending on the users platform. It may be used as a starting point +/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder). +/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste. +/// > The menu bar differs per platform because of constraints introduced +/// > by [`MenuItem`](tao::menu::MenuItem). +pub fn build_default_menu_bar() -> Menu { + let menu = Menu::new(); -// // since it is uncommon on windows to have an "application menu" -// // we add a "window" menu to be more consistent across platforms with the standard menu -// let mut window_menu = MenuBar::new(); -// #[cfg(target_os = "macos")] -// { -// window_menu.add_native_item(MenuItem::EnterFullScreen); -// window_menu.add_native_item(MenuItem::Zoom); -// window_menu.add_native_item(MenuItem::Separator); -// } + // since it is uncommon on windows to have an "application menu" + // we add a "window" menu to be more consistent across platforms with the standard menu + let window_menu = Submenu::new("Window", true); + window_menu.append_items(&[ + &PredefinedMenuItem::fullscreen(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::hide(None), + &PredefinedMenuItem::hide_others(None), + &PredefinedMenuItem::show_all(None), + &PredefinedMenuItem::maximize(None), + &PredefinedMenuItem::minimize(None), + &PredefinedMenuItem::close_window(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::quit(None), + ]); -// window_menu.add_native_item(MenuItem::Hide); + let edit_menu = Submenu::new("Window", true); + edit_menu.append_items(&[ + &PredefinedMenuItem::undo(None), + &PredefinedMenuItem::redo(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::cut(None), + &PredefinedMenuItem::copy(None), + &PredefinedMenuItem::paste(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::select_all(None), + ]); -// #[cfg(target_os = "macos")] -// { -// window_menu.add_native_item(MenuItem::HideOthers); -// window_menu.add_native_item(MenuItem::ShowAll); -// } + menu.append_items(&[&window_menu, &edit_menu]); -// window_menu.add_native_item(MenuItem::Minimize); -// window_menu.add_native_item(MenuItem::CloseWindow); -// window_menu.add_native_item(MenuItem::Separator); -// window_menu.add_native_item(MenuItem::Quit); -// menu_bar.add_submenu("Window", true, window_menu); - -// // since tao supports none of the below items on linux we should only add them on macos/windows -// #[cfg(not(target_os = "linux"))] -// { -// let mut edit_menu = MenuBar::new(); -// #[cfg(target_os = "macos")] -// { -// edit_menu.add_native_item(MenuItem::Undo); -// edit_menu.add_native_item(MenuItem::Redo); -// edit_menu.add_native_item(MenuItem::Separator); -// } - -// edit_menu.add_native_item(MenuItem::Cut); -// edit_menu.add_native_item(MenuItem::Copy); -// edit_menu.add_native_item(MenuItem::Paste); - -// #[cfg(target_os = "macos")] -// { -// edit_menu.add_native_item(MenuItem::Separator); -// edit_menu.add_native_item(MenuItem::SelectAll); -// } -// menu_bar.add_submenu("Edit", true, edit_menu); -// } - -// menu_bar -// } + menu +} diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 7d046fbc8..153b69ba6 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] wasm-bindgen = { workspace = true, optional = true } js-sys = { version = "0.3.56", optional = true } web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } -sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true } +sledgehammer_bindgen = { version = "0.3.0", default-features = false, optional = true } sledgehammer_utils = { version = "0.2", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } From 436635386d77ea7cb51965c9888263d602b2a404 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 16:28:15 -0800 Subject: [PATCH 067/126] Get compiling --- packages/desktop/src/app.rs | 37 +++++++++++++------------ packages/desktop/src/desktop_context.rs | 3 +- packages/desktop/src/protocol.rs | 10 +++---- packages/desktop/src/webview.rs | 16 +++++++---- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index f88970f03..15abc0d24 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -4,26 +4,26 @@ pub use crate::desktop_context::{ use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId, }; use crate::desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; +use crate::element::DesktopElement; +use crate::eval::init_eval; use crate::events::{IpcMessage, KnownIpcMethod}; use crate::file_upload; +pub use crate::protocol::{ + use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse, +}; use crate::query::QueryResult; use crate::shortcut::GlobalHotKeyEvent; +use crate::shortcut::ShortcutRegistry; +pub use crate::shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; use dioxus_core::*; use dioxus_html::{event_bubbles, MountedData}; use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; +use dioxus_interpreter_js::binary_protocol::Channel; +use futures_util::{pin_mut, FutureExt}; use global_hotkey::{ hotkey::{Code, HotKey, Modifiers}, GlobalHotKeyManager, }; -// use dioxus_interpreter_js::binary_protocol::Channel; -use crate::element::DesktopElement; -use crate::eval::init_eval; -pub use crate::protocol::{ - use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse, -}; -use crate::shortcut::ShortcutRegistry; -pub use crate::shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; -use futures_util::{pin_mut, FutureExt}; use rustc_hash::FxHashMap; use std::rc::Rc; use std::sync::atomic::AtomicU16; @@ -61,7 +61,7 @@ pub struct App

{ pub(crate) is_visible_before_start: bool, } -impl

App

{ +impl App

{ pub fn new(cfg: Config, props: P, root: Component

) -> (EventLoop, Self) { let event_loop = EventLoopBuilder::::with_user_event().build(); @@ -279,9 +279,12 @@ impl

App

{ dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { for webview in self.webviews.values_mut() { webview.dom.replace_template(template); + } - // poll_vdom(webview); - todo!() + let ids = self.webviews.keys().copied().collect::>(); + + for id in ids { + self.poll_vdom(id); } } dioxus_hot_reload::HotReloadMsg::Shutdown => { @@ -290,7 +293,7 @@ impl

App

{ } } - pub fn handle_file_dialog_msg(&self, msg: IpcMessage, window: WindowId) { + pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) { if let Ok(file_diolog) = serde_json::from_value::(msg.params()) { @@ -385,9 +388,9 @@ pub fn create_new_window( } pub struct WebviewHandler { - dom: VirtualDom, - desktop_context: DesktopContext, - waker: Waker, + pub dom: VirtualDom, + pub desktop_context: DesktopContext, + pub waker: Waker, // Wry assumes the webcontext is alive for the lifetime of the webview. // We need to keep the webcontext alive, otherwise the webview will crash @@ -408,8 +411,6 @@ pub fn send_edits(edits: Mutations, desktop_context: &DesktopContext) { } } -pub struct Channel {} - pub fn apply_edits( mutations: Mutations, channel: &mut Channel, diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 763c00c39..0ed40d707 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -98,6 +98,7 @@ pub struct DesktopService { /// The wry/tao proxy to the current window pub webview: WebView, + /// The tao window itself pub window: Window, /// The proxy to the event loop @@ -176,7 +177,7 @@ impl DesktopService { /// /// Be careful to not create a cycle of windows, or you might leak memory. pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak { - let window = create_new_window( + let window = crate::app::create_new_window( cfg, &self.event_loop, &self.proxy, diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 36400ce95..6e28ca67c 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -197,7 +197,7 @@ pub fn use_asset_handler( handler: impl AssetHandler, ) -> &AssetHandlerHandle { cx.use_hook(|| { - let desktop = window(); + let desktop = crate::window(); let handler_id = Rc::new(OnceCell::new()); let handler_id_ref = Rc::clone(&handler_id); let desktop_ref = Rc::clone(&desktop); @@ -220,7 +220,7 @@ pub(super) async fn desktop_handler( asset_handlers: &AssetHandlerRegistry, edit_queue: &EditQueue, headless: bool, - responder: wry::webview::RequestAsyncResponder, + responder: wry::RequestAsyncResponder, ) { let request = AssetRequest::from(request); @@ -259,7 +259,7 @@ pub(super) async fn desktop_handler( .body(Cow::from(body)) { Ok(response) => { - return Ok(response); + return responder.respond(response); } Err(err) => tracing::error!("error building response: {}", err), } @@ -305,9 +305,7 @@ pub(super) async fn desktop_handler( .header("Content-Type", content_type) .body(Cow::from(asset)) { - Ok(response) => { - return response; - } + Ok(response) => return responder.respond(response), Err(err) => tracing::error!("error building response: {}", err), } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index d98e60099..229fc3b9b 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -12,17 +12,21 @@ pub(crate) fn build( event_loop: &EventLoopWindowTarget, proxy: EventLoopProxy, ) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue, Window) { - let builder = cfg.window.clone(); + let mut builder = cfg.window.clone(); + + // TODO: restore the menu bar with muda: https://github.com/tauri-apps/muda/blob/dev/examples/wry.rs + if cfg.enable_default_menu_bar { + // builder = builder.with_menu(build_default_menu_bar()); + } + + let window = builder.build(event_loop).unwrap(); + + let window_id = window.id(); let file_handler = cfg.file_drop_handler.take(); let custom_head = cfg.custom_head.clone(); let index_file = cfg.custom_index.clone(); let root_name = cfg.root_name.clone(); - // TODO: restore the menu bar with muda: https://github.com/tauri-apps/muda/blob/dev/examples/wry.rs - // if cfg.enable_default_menu_bar { - // builder = builder.with_menu(build_default_menu_bar()); - // } - // 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( From d38ce349b139aea30b6910a4d7d195ccf7bc9ed1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 16:36:19 -0800 Subject: [PATCH 068/126] Tiny bit of cleanups --- packages/desktop/src/app.rs | 12 ++++---- packages/desktop/src/webview.rs | 54 ++++++++++++++++----------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 15abc0d24..265991bea 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -91,16 +91,17 @@ impl App

{ window_event: &Event<'_, UserWindowEvent>, event_loop: &EventLoopWindowTarget, ) { - // Set the control flow here, but make sure to update it at the end of the match self.control_flow = ControlFlow::Wait; self.event_handlers.apply_event(window_event, event_loop); - if let Ok(event) = self.global_hotkey_channel.try_recv() { - self.shortcut_manager.call_handlers(event); - } + _ = self + .global_hotkey_channel + .try_recv() + .map(|event| self.shortcut_manager.call_handlers(event)); } + #[cfg(all(feature = "hot-reload", debug_assertions))] pub fn connect_hotreload(&mut self) { let proxy = self.proxy.clone(); dioxus_hot_reload::connect({ @@ -317,8 +318,7 @@ impl App

{ view.dom.handle_event(event_name, data, id, event_bubbles); } - todo!() - // send_edits(view.dom.render_immediate(), &view.desktop_context); + send_edits(view.dom.render_immediate(), &view.desktop_context); } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 229fc3b9b..e1ec06269 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -97,10 +97,6 @@ pub(crate) fn build( webview = webview.with_background_color(color); } - // These are commented out because wry is currently broken in wry - // let mut web_context = WebContext::new(cfg.data_dir.clone()); - // .with_web_context(&mut web_context); - for (name, handler) in cfg.protocols.drain(..) { webview = webview.with_custom_protocol(name, move |r| handler(r)) } @@ -145,32 +141,36 @@ pub fn build_default_menu_bar() -> Menu { // since it is uncommon on windows to have an "application menu" // we add a "window" menu to be more consistent across platforms with the standard menu let window_menu = Submenu::new("Window", true); - window_menu.append_items(&[ - &PredefinedMenuItem::fullscreen(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::hide(None), - &PredefinedMenuItem::hide_others(None), - &PredefinedMenuItem::show_all(None), - &PredefinedMenuItem::maximize(None), - &PredefinedMenuItem::minimize(None), - &PredefinedMenuItem::close_window(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::quit(None), - ]); + window_menu + .append_items(&[ + &PredefinedMenuItem::fullscreen(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::hide(None), + &PredefinedMenuItem::hide_others(None), + &PredefinedMenuItem::show_all(None), + &PredefinedMenuItem::maximize(None), + &PredefinedMenuItem::minimize(None), + &PredefinedMenuItem::close_window(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::quit(None), + ]) + .unwrap(); let edit_menu = Submenu::new("Window", true); - edit_menu.append_items(&[ - &PredefinedMenuItem::undo(None), - &PredefinedMenuItem::redo(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::cut(None), - &PredefinedMenuItem::copy(None), - &PredefinedMenuItem::paste(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::select_all(None), - ]); + edit_menu + .append_items(&[ + &PredefinedMenuItem::undo(None), + &PredefinedMenuItem::redo(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::cut(None), + &PredefinedMenuItem::copy(None), + &PredefinedMenuItem::paste(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::select_all(None), + ]) + .unwrap(); - menu.append_items(&[&window_menu, &edit_menu]); + menu.append_items(&[&window_menu, &edit_menu]).unwrap(); menu } From 1518c223f636c674740bb0a2b6237856c7608576 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 16:38:49 -0800 Subject: [PATCH 069/126] Don't drop tokio runtime immediately --- packages/desktop/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 1fc3e99c0..96c763045 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -113,11 +113,8 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) // We start the tokio runtime *on this thread* // Any future we poll later will use this runtime to spawn tasks and for IO // I would love to just allow dioxus to work with any runtime... but tokio is weird - let _guard = Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - .enter(); + let rt = &Builder::new_multi_thread().enable_all().build().unwrap(); + let _guard = rt.enter(); let (event_loop, mut app) = app::App::new(cfg, props, root); From 62a9583fffb32e274ee3075430adfeae3b1148e6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 16:44:16 -0800 Subject: [PATCH 070/126] Fix merge conflict --- packages/desktop/headless_tests/events.rs | 6 ------ packages/desktop/src/lib.rs | 1 - 2 files changed, 7 deletions(-) diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index b0aef5f79..4660441a2 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -231,12 +231,6 @@ fn app(cx: Scope) -> Element { assert!(event.data.held_buttons().is_empty()); assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); received_events.modify(|x| *x + 1) - }, - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Primary), - ); - recieved_events.modify(|x| *x + 1) } } div { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 96c763045..cfc5aeb5e 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -147,7 +147,6 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) _ => {} } - // Update the control flow *control_flow = app.control_flow; }) } From ec3eaa6b26dc36005a2e0d5fb42ee0d127450d49 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 16:48:53 -0800 Subject: [PATCH 071/126] Add ID to filedrop handler --- packages/desktop/headless_tests/rendering.rs | 2 +- packages/desktop/src/app.rs | 2 +- packages/desktop/src/cfg.rs | 6 ++-- packages/desktop/src/events.rs | 16 +++++----- packages/desktop/src/lib.rs | 15 ++++----- packages/desktop/src/protocol.rs | 32 ++------------------ packages/desktop/src/webview.rs | 2 +- 7 files changed, 24 insertions(+), 51 deletions(-) diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index 0907121d4..f228b79da 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -2,8 +2,8 @@ use dioxus::prelude::*; use dioxus_desktop::DesktopContext; pub(crate) fn check_app_exits(app: Component) { - use dioxus_desktop::tao::window::WindowBuilder; use dioxus_desktop::Config; + use tao::window::WindowBuilder; // This is a deadman's switch to ensure that the app exits let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); let should_panic_clone = should_panic.clone(); diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 265991bea..59feb2f94 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -6,7 +6,7 @@ pub use crate::desktop_context::{ use crate::desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use crate::element::DesktopElement; use crate::eval::init_eval; -use crate::events::{IpcMessage, KnownIpcMethod}; +use crate::events::{IpcMessage, IpcMethod}; use crate::file_upload; pub use crate::protocol::{ use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse, diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 9d1854bdf..4fb7f6ecb 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::path::PathBuf; -use tao::window::{Icon, Window, WindowBuilder}; +use tao::window::{Icon, WindowBuilder, WindowId}; use wry::{ http::{Request as HttpRequest, Response as HttpResponse}, FileDropEvent, @@ -37,7 +37,7 @@ pub struct Config { pub(crate) enable_default_menu_bar: bool, } -type DropHandler = Box bool>; +type DropHandler = Box bool>; pub(crate) type WryProtocol = ( String, @@ -119,7 +119,7 @@ impl Config { /// Set a file drop handler pub fn with_file_drop_handler( mut self, - handler: impl Fn(FileDropEvent) -> bool + 'static, + handler: impl Fn(WindowId, FileDropEvent) -> bool + 'static, ) -> Self { self.file_drop_handler = Some(Box::new(handler)); self diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index a877b52d6..e0a5a4319 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -9,7 +9,7 @@ pub struct IpcMessage { } #[derive(Deserialize, Serialize, Debug, Clone)] -pub enum KnownIpcMethod<'a> { +pub enum IpcMethod<'a> { FileDialog, UserEvent, Query, @@ -19,15 +19,15 @@ pub enum KnownIpcMethod<'a> { } impl IpcMessage { - pub(crate) fn method(&self) -> KnownIpcMethod { + pub(crate) fn method(&self) -> IpcMethod { match self.method.as_str() { // todo: this is a misspelling - "file_diolog" => KnownIpcMethod::FileDialog, - "user_event" => KnownIpcMethod::UserEvent, - "query" => KnownIpcMethod::Query, - "browser_open" => KnownIpcMethod::BrowserOpen, - "initialize" => KnownIpcMethod::Initialize, - _ => KnownIpcMethod::Other(&self.method), + "file_diolog" => IpcMethod::FileDialog, + "user_event" => IpcMethod::UserEvent, + "query" => IpcMethod::Query, + "browser_open" => IpcMethod::BrowserOpen, + "initialize" => IpcMethod::Initialize, + _ => IpcMethod::Other(&self.method), } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index cfc5aeb5e..2e191ef8f 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -30,9 +30,10 @@ pub use desktop_context::{ }; use desktop_context::{EventData, UserWindowEvent}; use dioxus_core::*; -use events::KnownIpcMethod; +use events::IpcMethod; pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse}; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; +pub use tao; pub use tao::dpi::{LogicalSize, PhysicalSize}; use tao::event::{Event, StartCause, WindowEvent}; pub use tao::window::WindowBuilder; @@ -136,12 +137,12 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) EventData::CloseWindow => app.handle_close_msg(id), EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), EventData::Ipc(msg) => match msg.method() { - KnownIpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), - KnownIpcMethod::UserEvent => app.handle_user_event_msg(msg, id), - KnownIpcMethod::Query => app.handle_query_msg(msg, id), - KnownIpcMethod::BrowserOpen => app.handle_browser_open(msg), - KnownIpcMethod::Initialize => app.handle_initialize_msg(id), - KnownIpcMethod::Other(_) => {} + IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), + IpcMethod::UserEvent => app.handle_user_event_msg(msg, id), + IpcMethod::Query => app.handle_query_msg(msg, id), + IpcMethod::BrowserOpen => app.handle_browser_open(msg), + IpcMethod::Initialize => app.handle_initialize_msg(id), + IpcMethod::Other(_) => {} }, }, _ => {} diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 6e28ca67c..d621d58d9 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,6 +1,5 @@ -use crate::{use_window, DesktopContext}; +use crate::DesktopContext; use dioxus_core::ScopeState; -use dioxus_interpreter_js::INTERPRETER_JS; use slab::Slab; use std::{ borrow::Cow, @@ -17,7 +16,7 @@ use tokio::{ }; use wry::{ http::{status::StatusCode, Request, Response}, - RequestAsyncResponder, Result, + Result, }; use crate::desktop_context::EditQueue; @@ -25,33 +24,6 @@ use crate::desktop_context::EditQueue; static MINIFIED: &str = include_str!("./minified.js"); fn module_loader(root_name: &str, headless: bool) -> String { - let js = INTERPRETER_JS.replace( - "/*POST_HANDLE_EDITS*/", - r#"// Prevent file inputs from opening the file dialog on click - let inputs = document.querySelectorAll("input"); - for (let input of inputs) { - if (!input.getAttribute("data-dioxus-file-listener")) { - // prevent file inputs from opening the file dialog on click - const type = input.getAttribute("type"); - if (type === "file") { - input.setAttribute("data-dioxus-file-listener", true); - input.addEventListener("click", (event) => { - let target = event.target; - let target_id = find_real_id(target); - if (target_id !== null) { - const send = (event_name) => { - const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); - window.ipc.postMessage(message); - }; - send("change&input"); - } - event.preventDefault(); - }); - } - } - }"#, - ); - format!( r#" -"# - ) -} - -/// An arbitrary asset is an HTTP response containing a binary body. -pub type AssetResponse = Response>; - -/// A future that returns an [`AssetResponse`]. This future may be spawned in a new thread, -/// so it must be [`Send`], [`Sync`], and `'static`. -pub trait AssetFuture: Future> + Send + Sync + 'static {} -impl> + Send + Sync + 'static> AssetFuture for T {} - -#[derive(Debug, Clone)] -/// A request for an asset. This is a wrapper around [`Request>`] that provides methods specific to asset requests. -pub struct AssetRequest { - path: PathBuf, - request: Arc>>, -} - -impl AssetRequest { - /// Get the path the asset request is for - pub fn path(&self) -> &Path { - &self.path - } -} - -impl From>> for AssetRequest { - fn from(request: Request>) -> Self { - let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/')) - .expect("expected URL to be UTF-8 encoded"); - let path = PathBuf::from(&*decoded); - Self { - request: Arc::new(request), - path, - } - } -} - -impl Deref for AssetRequest { - type Target = Request>; - - fn deref(&self) -> &Self::Target { - &self.request - } -} - -/// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`. -/// This handler is stashed indefinitely in a context object, so it must be `'static`. -pub trait AssetHandler: Send + Sync + 'static { - /// Handle an asset request, returning a future that either loads the asset, or returns `None` - fn handle_request(&self, request: &AssetRequest) -> F; -} - -impl F + Send + Sync + 'static> AssetHandler for T { - fn handle_request(&self, request: &AssetRequest) -> F { - self(request) - } -} - -type AssetHandlerRegistryInner = - Slab Pin> + Send + Sync + 'static>>; - -#[derive(Clone)] -pub struct AssetHandlerRegistry(Arc>); - -impl AssetHandlerRegistry { - pub fn new() -> Self { - AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new()))) - } - - pub async fn register_handler(&self, f: impl AssetHandler) -> usize { - let mut registry = self.0.write().await; - registry.insert(Box::new(move |req| Box::pin(f.handle_request(req)))) - } - - pub async fn remove_handler(&self, id: usize) -> Option<()> { - let mut registry = self.0.write().await; - registry.try_remove(id).map(|_| ()) - } - - pub async fn try_handlers(&self, req: &AssetRequest) -> Option { - let registry = self.0.read().await; - for (_, handler) in registry.iter() { - if let Some(response) = handler(req).await { - return Some(response); - } - } - None - } -} - -/// A handle to a registered asset handler. -pub struct AssetHandlerHandle { - desktop: DesktopContext, - handler_id: Rc>, -} - -impl AssetHandlerHandle { - /// Returns the ID for this handle. - /// - /// Because registering an ID is asynchronous, this may return `None` if the - /// registration has not completed yet. - pub fn handler_id(&self) -> Option { - self.handler_id.get().copied() - } -} - -impl Drop for AssetHandlerHandle { - fn drop(&mut self) { - let cell = Rc::clone(&self.handler_id); - let desktop = Rc::clone(&self.desktop); - tokio::task::block_in_place(move || { - Handle::current().block_on(async move { - if let Some(id) = cell.get() { - desktop.asset_handlers.remove_handler(*id).await; - } - }) - }); - } -} - -/// Provide a callback to handle asset loading yourself. -/// -/// The callback takes a path as requested by the web view, and it should return `Some(response)` -/// if you want to load the asset, and `None` if you want to fallback on the default behavior. -pub fn use_asset_handler( - cx: &ScopeState, - handler: impl AssetHandler, -) -> &AssetHandlerHandle { - cx.use_hook(|| { - let desktop = crate::window(); - let handler_id = Rc::new(OnceCell::new()); - let handler_id_ref = Rc::clone(&handler_id); - let desktop_ref = Rc::clone(&desktop); - cx.push_future(async move { - let id = desktop.asset_handlers.register_handler(handler).await; - handler_id.set(id).unwrap(); - }); - AssetHandlerHandle { - desktop: desktop_ref, - handler_id: handler_id_ref, - } - }) -} - pub(super) async fn desktop_handler( request: Request>, custom_head: Option, custom_index: Option, root_name: &str, asset_handlers: &AssetHandlerRegistry, - edit_queue: &EditQueue, + edit_queue: &crate::edits::EditQueue, headless: bool, responder: wry::RequestAsyncResponder, ) { @@ -293,6 +119,26 @@ pub(super) async fn desktop_handler( } } +fn module_loader(root_name: &str, headless: bool) -> String { + format!( + r#" + +"# + ) +} + #[allow(unreachable_code)] fn get_asset_root() -> Option { /* diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index 11f23d8d8..488c8f677 100644 --- a/packages/desktop/src/waker.rs +++ b/packages/desktop/src/waker.rs @@ -1,5 +1,5 @@ use crate::desktop_context::{EventData, UserWindowEvent}; -use futures_util::{pin_mut, task::ArcWake, FutureExt}; +use futures_util::task::ArcWake; use std::sync::Arc; use tao::{event_loop::EventLoopProxy, window::WindowId}; @@ -8,7 +8,7 @@ use tao::{event_loop::EventLoopProxy, window::WindowId}; /// 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, id: WindowId) -> std::task::Waker { +pub fn tao_waker(proxy: EventLoopProxy, id: WindowId) -> std::task::Waker { struct DomHandle { proxy: EventLoopProxy, id: WindowId, @@ -27,8 +27,5 @@ pub fn tao_waker(proxy: &EventLoopProxy, id: WindowId) -> std:: } } - futures_util::task::waker(Arc::new(DomHandle { - id, - proxy: proxy.clone(), - })) + futures_util::task::waker(Arc::new(DomHandle { id, proxy })) } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 275678d73..537b3e241 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,26 +1,50 @@ -use crate::desktop_context::{EditQueue, EventData}; -use crate::protocol::{self, AssetHandlerRegistry}; -use crate::{desktop_context::UserWindowEvent, Config}; -use muda::{Menu, MenuItem, PredefinedMenuItem, Submenu}; +use std::{rc::Rc, task::Waker}; + +use crate::edits::{EditQueue, WebviewQueue}; +use crate::{ + assets::AssetHandlerRegistry, desktop_context::UserWindowEvent, waker::tao_waker, Config, + DesktopContext, +}; +use crate::{ + desktop_context::{EventData, WindowEventHandlers}, + eval::init_eval, + shortcut::ShortcutRegistry, +}; +use crate::{ + protocol::{self}, + DesktopService, +}; +use dioxus_core::VirtualDom; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; -use tao::window::Window; -use wry::http::Response; -use wry::{WebContext, WebView, WebViewBuilder}; +use wry::{WebContext, WebViewBuilder}; -pub(crate) fn build( - cfg: &mut Config, +pub struct WebviewHandler { + pub dom: VirtualDom, + pub desktop_context: DesktopContext, + pub waker: Waker, + + // Wry assumes the webcontext is alive for the lifetime of the webview. + // We need to keep the webcontext alive, otherwise the webview will crash + _web_context: WebContext, +} + +pub fn create_new_window( + mut cfg: Config, event_loop: &EventLoopWindowTarget, - proxy: EventLoopProxy, -) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue, Window) { - let mut builder = cfg.window.clone(); + proxy: &EventLoopProxy, + dom: VirtualDom, + queue: &WebviewQueue, + event_handlers: &WindowEventHandlers, + shortcut_manager: ShortcutRegistry, +) -> WebviewHandler { + let window = cfg.window.clone().build(event_loop).unwrap(); - // TODO: restore the menu bar with muda: https://github.com/tauri-apps/muda/blob/dev/examples/wry.rs + // TODO: allow users to specify their own menubars, again :/ if cfg.enable_default_menu_bar { - // builder = builder.with_menu(build_default_menu_bar()); + use crate::menubar::*; + build_menu_bar(build_default_menu_bar(), &window); } - let window = builder.build(event_loop).unwrap(); - let window_id = window.id(); let file_handler = cfg.file_drop_handler.take(); let custom_head = cfg.custom_head.clone(); @@ -49,10 +73,13 @@ pub(crate) fn build( .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() - .with_ipc_handler(move |payload: String| { - // defer the event to the main thread - if let Ok(message) = serde_json::from_str(&payload) { - _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window_id)); + .with_ipc_handler({ + let proxy = proxy.clone(); + move |payload: String| { + // defer the event to the main thread + if let Ok(message) = serde_json::from_str(&payload) { + _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window_id)); + } } }) .with_asynchronous_custom_protocol(String::from("dioxus"), { @@ -101,76 +128,49 @@ pub(crate) fn build( webview = webview.with_custom_protocol(name, move |r| handler(r)) } + const INITIALIZATION_SCRIPT: &str = r#" + if (document.addEventListener) { + document.addEventListener('contextmenu', function(e) { + e.preventDefault(); + }, false); + } else { + document.attachEvent('oncontextmenu', function() { + window.event.returnValue = false; + }); + } + "#; + 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; - }); - } - "#, - ) + webview = webview.with_initialization_script(INITIALIZATION_SCRIPT) } else { // in debug, we are okay with the reload menu showing and dev tool webview = webview.with_devtools(true); } - ( - webview.build().unwrap(), - web_context, - asset_handlers, - edit_queue, + let webview = webview.build().unwrap(); + + let desktop_context = Rc::from(DesktopService::new( window, - ) -} - -/// Builds a standard menu bar depending on the users platform. It may be used as a starting point -/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder). -/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste. -/// > The menu bar differs per platform because of constraints introduced -/// > by [`MenuItem`](tao::menu::MenuItem). -pub fn build_default_menu_bar() -> Menu { - let menu = Menu::new(); - - // since it is uncommon on windows to have an "application menu" - // we add a "window" menu to be more consistent across platforms with the standard menu - let window_menu = Submenu::new("Window", true); - window_menu - .append_items(&[ - &PredefinedMenuItem::fullscreen(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::hide(None), - &PredefinedMenuItem::hide_others(None), - &PredefinedMenuItem::show_all(None), - &PredefinedMenuItem::maximize(None), - &PredefinedMenuItem::minimize(None), - &PredefinedMenuItem::close_window(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::quit(None), - ]) - .unwrap(); - - let edit_menu = Submenu::new("Window", true); - edit_menu - .append_items(&[ - &PredefinedMenuItem::undo(None), - &PredefinedMenuItem::redo(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::cut(None), - &PredefinedMenuItem::copy(None), - &PredefinedMenuItem::paste(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::select_all(None), - ]) - .unwrap(); - - menu.append_items(&[&window_menu, &edit_menu]).unwrap(); - - menu + webview, + proxy.clone(), + event_loop.clone(), + queue.clone(), + event_handlers.clone(), + shortcut_manager, + edit_queue, + asset_handlers, + )); + + dom.base_scope().provide_context(desktop_context.clone()); + + init_eval(dom.base_scope()); + + WebviewHandler { + // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both + waker: tao_waker(proxy.clone(), desktop_context.window.id()), + desktop_context, + dom, + _web_context: web_context, + } } From 6e08703c6c18f5d82cda66825ee76a78900244df Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 17:21:38 -0800 Subject: [PATCH 073/126] Fix compile issue --- packages/desktop/src/app.rs | 12 +++++++----- packages/desktop/src/desktop_context.rs | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 85e4eecbd..fac223ab0 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -334,12 +334,14 @@ impl App

{ let mut cx = std::task::Context::from_waker(&view.waker); loop { - let fut = view.dom.wait_for_work(); - pin_mut!(fut); + { + let fut = view.dom.wait_for_work(); + pin_mut!(fut); - match fut.poll_unpin(&mut cx) { - std::task::Poll::Ready(_) => {} - std::task::Poll::Pending => break, + match fut.poll_unpin(&mut cx) { + std::task::Poll::Ready(_) => {} + std::task::Poll::Pending => break, + } } send_edits(view.dom.render_immediate(), &view.desktop_context); diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index af1204b06..866056558 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -34,6 +34,9 @@ pub fn window() -> DesktopContext { dioxus_core::prelude::consume_context().unwrap() } +/// A handle to the [`DesktopService`] that can be passed around. +pub type DesktopContext = Rc; + /// An imperative interface to the current window. /// /// To get a handle to the current window, use the [`use_window`] hook. @@ -78,9 +81,6 @@ pub struct DesktopService { pub(crate) views: Rc>>, } -/// A handle to the [`DesktopService`] that can be passed around. -pub type DesktopContext = Rc; - /// A smart pointer to the current window. impl std::ops::Deref for DesktopService { type Target = Window; From d47e49478666619d682d0bc9be5d2f43cc019a4d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 17:28:54 -0800 Subject: [PATCH 074/126] More cleaning up of the desktop crate --- packages/desktop/src/app.rs | 11 +++-- packages/desktop/src/desktop_context.rs | 16 +++++++- packages/desktop/src/edits.rs | 54 +++++++------------------ packages/desktop/src/menubar.rs | 8 ++-- 4 files changed, 39 insertions(+), 50 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index fac223ab0..931d239cb 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -2,7 +2,7 @@ pub use crate::assets::{AssetFuture, AssetHandler, AssetRequest, AssetResponse}; pub use crate::cfg::{Config, WindowCloseBehaviour}; pub use crate::desktop_context::DesktopContext; pub use crate::desktop_context::{window, DesktopService, WryEventHandler, WryEventHandlerId}; -use crate::edits::{send_edits, EditQueue, WebviewQueue}; +use crate::edits::{EditQueue, WebviewQueue}; use crate::element::DesktopElement; use crate::eval::init_eval; use crate::events::{IpcMessage, IpcMethod}; @@ -198,7 +198,7 @@ impl App

{ pub fn handle_initialize_msg(&mut self, id: WindowId) { let view = self.webviews.get_mut(&id).unwrap(); - send_edits(view.dom.rebuild(), &view.desktop_context); + view.desktop_context.send_edits(view.dom.rebuild()); view.desktop_context .window .set_visible(self.is_visible_before_start); @@ -271,8 +271,7 @@ impl App

{ }; view.dom.handle_event(&name, as_any, element, bubbles); - - send_edits(view.dom.render_immediate(), &view.desktop_context); + view.desktop_context.send_edits(view.dom.render_immediate()); } #[cfg(all(feature = "hot-reload", debug_assertions))] @@ -319,7 +318,7 @@ impl App

{ view.dom.handle_event(event_name, data, id, event_bubbles); } - send_edits(view.dom.render_immediate(), &view.desktop_context); + view.desktop_context.send_edits(view.dom.render_immediate()); } } @@ -344,7 +343,7 @@ impl App

{ } } - send_edits(view.dom.render_immediate(), &view.desktop_context); + view.desktop_context.send_edits(view.dom.render_immediate()); } } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 866056558..0a2a768e4 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -5,8 +5,8 @@ use crate::Config; use crate::{assets::AssetFuture, edits::WebviewQueue}; use crate::{assets::AssetHandlerRegistry, edits::EditQueue}; use crate::{events::IpcMessage, webview::WebviewHandler}; -use dioxus_core::ScopeState; use dioxus_core::VirtualDom; +use dioxus_core::{Mutations, ScopeState}; use dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; use slab::Slab; @@ -121,6 +121,20 @@ impl DesktopService { } } + /// Send a list of mutations to the webview + pub(crate) fn send_edits(&self, edits: Mutations) { + let mut channel = self.channel.borrow_mut(); + let mut templates = self.templates.borrow_mut(); + if let Some(bytes) = crate::edits::apply_edits( + edits, + &mut channel, + &mut templates, + &self.max_template_count, + ) { + self.edit_queue.add_edits(bytes) + } + } + /// Create a new window using the props and window builder /// /// Returns the webview handle for the new window. diff --git a/packages/desktop/src/edits.rs b/packages/desktop/src/edits.rs index 5a09d1933..fa9306156 100644 --- a/packages/desktop/src/edits.rs +++ b/packages/desktop/src/edits.rs @@ -1,24 +1,21 @@ -use crate::assets::AssetHandlerRegistry; use crate::query::QueryEngine; use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; use crate::AssetHandler; use crate::Config; use crate::{assets::AssetFuture, DesktopContext}; +use crate::{assets::AssetHandlerRegistry, DesktopService}; use crate::{events::IpcMessage, webview::WebviewHandler}; use dioxus_core::{BorrowedAttributeValue, Template, TemplateAttribute, TemplateNode, VirtualDom}; use dioxus_core::{Mutations, ScopeState}; use dioxus_html::event_bubbles; use dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; -use slab::Slab; use std::{ - cell::RefCell, fmt::Debug, fmt::Formatter, rc::Rc, rc::Weak, sync::atomic::AtomicU16, - sync::Arc, sync::Mutex, -}; -use tao::{ - event::Event, - event_loop::{EventLoopProxy, EventLoopWindowTarget}, - window::{Fullscreen as WryFullscreen, Window, WindowId}, + cell::RefCell, + rc::Rc, + sync::atomic::AtomicU16, + sync::Arc, + sync::{atomic::Ordering, Mutex}, }; use wry::{RequestAsyncResponder, WebView}; @@ -33,17 +30,6 @@ pub(crate) struct EditQueue { responder: Arc>>, } -impl Debug for EditQueue { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("EditQueue") - .field("queue", &self.queue) - .field("responder", { - &self.responder.lock().unwrap().as_ref().map(|_| ()) - }) - .finish() - } -} - impl EditQueue { pub fn handle_request(&self, responder: RequestAsyncResponder) { let mut queue = self.queue.lock().unwrap(); @@ -64,33 +50,21 @@ impl EditQueue { } } -/// Send a list of mutations to the webview -pub fn send_edits(edits: Mutations, desktop_context: &DesktopContext) { - let mut channel = desktop_context.channel.borrow_mut(); - let mut templates = desktop_context.templates.borrow_mut(); - if let Some(bytes) = apply_edits( - edits, - &mut channel, - &mut templates, - &desktop_context.max_template_count, - ) { - desktop_context.edit_queue.add_edits(bytes) - } -} - -pub fn apply_edits( +pub(crate) fn apply_edits( mutations: Mutations, channel: &mut Channel, templates: &mut FxHashMap, max_template_count: &AtomicU16, ) -> Option> { - use dioxus_core::Mutation::*; if mutations.templates.is_empty() && mutations.edits.is_empty() { return None; } + for template in mutations.templates { add_template(&template, channel, templates, max_template_count); } + + use dioxus_core::Mutation::*; for edit in mutations.edits { match edit { AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16), @@ -156,19 +130,19 @@ pub fn add_template( templates: &mut FxHashMap, max_template_count: &AtomicU16, ) { - let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed); + let current_max_template_count = max_template_count.load(Ordering::Relaxed); for root in template.roots.iter() { create_template_node(channel, root); templates.insert(template.name.to_owned(), current_max_template_count); } channel.add_templates(current_max_template_count, template.roots.len() as u16); - max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + max_template_count.fetch_add(1, Ordering::Relaxed); } -pub fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { +pub fn create_template_node(channel: &mut Channel, node: &'static TemplateNode<'static>) { use TemplateNode::*; - match v { + match node { Element { tag, namespace, diff --git a/packages/desktop/src/menubar.rs b/packages/desktop/src/menubar.rs index 44cc12c84..2f7f3951a 100644 --- a/packages/desktop/src/menubar.rs +++ b/packages/desktop/src/menubar.rs @@ -1,12 +1,14 @@ -use muda::{Menu, MenuItem, PredefinedMenuItem, Submenu}; +use muda::{Menu, PredefinedMenuItem, Submenu}; use tao::window::Window; +#[allow(unused)] pub fn build_menu_bar(menu: Menu, window: &Window) { #[cfg(target_os = "windows")] - menu.init_for_hwnd(window_hwnd); + menu.init_for_hwnd(window); #[cfg(target_os = "linux")] - menu.init_for_gtk_window(>k_window, Some(&vertical_gtk_box)); + menu.init_for_gtk_window(window, None); + // menu.init_for_gtk_window(window, Some(&vertical_gtk_box)); #[cfg(target_os = "macos")] menu.init_for_nsapp(); From fef7b83b792a5e6db1563c1a9fd70d464acf7e4e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 17:35:58 -0800 Subject: [PATCH 075/126] Rename editqueue type --- packages/desktop/src/protocol.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 6910495f0..77acebad6 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,4 +1,4 @@ -use crate::assets::*; +use crate::{assets::*, edits::EditQueue}; use std::{ borrow::Cow, path::{Path, PathBuf}, @@ -16,7 +16,7 @@ pub(super) async fn desktop_handler( custom_index: Option, root_name: &str, asset_handlers: &AssetHandlerRegistry, - edit_queue: &crate::edits::EditQueue, + edit_queue: &EditQueue, headless: bool, responder: wry::RequestAsyncResponder, ) { From 7444796a85646605e56a063bc4c1116d82fe490f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 18:01:13 -0800 Subject: [PATCH 076/126] Simplify asset code a bit --- packages/desktop/src/app.rs | 8 +-- packages/desktop/src/assets.rs | 6 +- packages/desktop/src/desktop_context.rs | 27 +++----- packages/desktop/src/edits.rs | 22 +++--- packages/desktop/src/hooks.rs | 20 +++++- packages/desktop/src/lib.rs | 6 +- packages/desktop/src/shortcut.rs | 89 ++++++++++--------------- packages/desktop/src/webview.rs | 2 +- 8 files changed, 84 insertions(+), 96 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 931d239cb..4e954d496 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -11,7 +11,7 @@ use crate::hooks::*; use crate::query::QueryResult; use crate::shortcut::GlobalHotKeyEvent; use crate::shortcut::ShortcutRegistry; -pub use crate::shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; +pub use crate::shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError}; use crate::{ desktop_context::{EventData, UserWindowEvent, WindowEventHandlers}, webview::WebviewHandler, @@ -171,9 +171,9 @@ impl App

{ let handler = crate::webview::create_new_window( cfg, + dom, target, &self.proxy, - dom, &self.queue, &self.event_handlers, self.shortcut_manager.clone(), @@ -212,10 +212,6 @@ impl App

{ } } - pub fn handle_poll_msg(&mut self, id: WindowId) { - self.poll_vdom(id); - } - pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) { let Ok(result) = serde_json::from_value::(msg.params()) else { return; diff --git a/packages/desktop/src/assets.rs b/packages/desktop/src/assets.rs index f27dcb504..f80c7542a 100644 --- a/packages/desktop/src/assets.rs +++ b/packages/desktop/src/assets.rs @@ -75,8 +75,10 @@ impl F + Send + Sync + 'static> AssetHan } } -type AssetHandlerRegistryInner = - Slab Pin> + Send + Sync + 'static>>; +type UserAssetHandler = + Box Pin> + Send + Sync + 'static>; + +type AssetHandlerRegistryInner = Slab; #[derive(Clone)] pub struct AssetHandlerRegistry(Arc>); diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 0a2a768e4..240f8ec24 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,26 +1,22 @@ +use crate::events::IpcMessage; use crate::query::QueryEngine; use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; use crate::AssetHandler; use crate::Config; use crate::{assets::AssetFuture, edits::WebviewQueue}; use crate::{assets::AssetHandlerRegistry, edits::EditQueue}; -use crate::{events::IpcMessage, webview::WebviewHandler}; +use dioxus_core::Mutations; use dioxus_core::VirtualDom; -use dioxus_core::{Mutations, ScopeState}; use dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; use slab::Slab; -use std::{ - cell::RefCell, fmt::Debug, fmt::Formatter, rc::Rc, rc::Weak, sync::atomic::AtomicU16, - sync::Arc, sync::Mutex, -}; +use std::{cell::RefCell, fmt::Debug, rc::Rc, rc::Weak, sync::atomic::AtomicU16}; use tao::{ event::Event, event_loop::{EventLoopProxy, EventLoopWindowTarget}, window::{Fullscreen as WryFullscreen, Window, WindowId}, }; - -use wry::{RequestAsyncResponder, WebView}; +use wry::WebView; #[cfg(target_os = "ios")] use tao::platform::ios::WindowExtIOS; @@ -123,16 +119,13 @@ impl DesktopService { /// Send a list of mutations to the webview pub(crate) fn send_edits(&self, edits: Mutations) { - let mut channel = self.channel.borrow_mut(); - let mut templates = self.templates.borrow_mut(); - if let Some(bytes) = crate::edits::apply_edits( + crate::edits::apply_edits( edits, - &mut channel, - &mut templates, + &mut self.channel.borrow_mut(), + &mut self.templates.borrow_mut(), &self.max_template_count, - ) { - self.edit_queue.add_edits(bytes) - } + ) + .map(|bytes| self.edit_queue.add_edits(bytes)); } /// Create a new window using the props and window builder @@ -145,9 +138,9 @@ impl DesktopService { pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak { let window = crate::webview::create_new_window( cfg, + dom, &self.event_loop, &self.proxy, - dom, &self.pending_windows, &self.event_handlers, self.shortcut_manager.clone(), diff --git a/packages/desktop/src/edits.rs b/packages/desktop/src/edits.rs index fa9306156..4eeb48a9b 100644 --- a/packages/desktop/src/edits.rs +++ b/packages/desktop/src/edits.rs @@ -1,12 +1,6 @@ -use crate::query::QueryEngine; -use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; -use crate::AssetHandler; -use crate::Config; -use crate::{assets::AssetFuture, DesktopContext}; -use crate::{assets::AssetHandlerRegistry, DesktopService}; -use crate::{events::IpcMessage, webview::WebviewHandler}; -use dioxus_core::{BorrowedAttributeValue, Template, TemplateAttribute, TemplateNode, VirtualDom}; -use dioxus_core::{Mutations, ScopeState}; +use crate::webview::WebviewHandler; +use dioxus_core::Mutations; +use dioxus_core::{BorrowedAttributeValue, Template, TemplateAttribute, TemplateNode}; use dioxus_html::event_bubbles; use dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; @@ -18,12 +12,16 @@ use std::{ sync::{atomic::Ordering, Mutex}, }; -use wry::{RequestAsyncResponder, WebView}; +use wry::RequestAsyncResponder; pub(crate) type WebviewQueue = Rc>>; -/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events. -/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made. +/// This handles communication between the requests that the webview makes and the interpreter. The interpreter +/// constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like +/// server side events. +/// +/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until +/// a new request is made. #[derive(Default, Clone)] pub(crate) struct EditQueue { queue: Arc>>>, diff --git a/packages/desktop/src/hooks.rs b/packages/desktop/src/hooks.rs index 7ebdf4963..54d114abb 100644 --- a/packages/desktop/src/hooks.rs +++ b/packages/desktop/src/hooks.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use crate::assets::*; +use crate::{assets::*, shortcut::IntoAccelerator, ShortcutHandle, ShortcutRegistryError}; use crate::{desktop_context::UserWindowEvent, window, DesktopContext, WryEventHandler}; use dioxus_core::ScopeState; use tao::{event::Event, event_loop::EventLoopWindowTarget}; @@ -52,3 +52,21 @@ pub fn use_asset_handler( } }) } + +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_global_shortcut( + cx: &ScopeState, + accelerator: impl IntoAccelerator, + handler: impl FnMut() + 'static, +) -> &Result { + cx.use_hook(move || { + let desktop = window(); + + let id = desktop.create_shortcut(accelerator.accelerator(), handler); + + Ok(ShortcutHandle { + desktop, + shortcut_id: id?, + }) + }) +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 3e8608200..ca1e851fb 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -37,9 +37,9 @@ pub use cfg::{Config, WindowCloseBehaviour}; pub use desktop_context::{ window, DesktopContext, DesktopService, WryEventHandler, WryEventHandlerId, }; -pub use hooks::{use_asset_handler, use_window, use_wry_event_handler}; +pub use hooks::{use_asset_handler, use_global_shortcut, use_window, use_wry_event_handler}; pub use menubar::build_default_menu_bar; -pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; +pub use shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError}; #[allow(deprecated)] use desktop_context::{EventData, UserWindowEvent}; @@ -139,7 +139,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) _ => {} }, Event::UserEvent(UserWindowEvent(event, id)) => match event { - EventData::Poll => app.handle_poll_msg(id), + EventData::Poll => app.poll_vdom(id), EventData::NewWindow => app.handle_new_window(), EventData::CloseWindow => app.handle_close_msg(id), EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs index 55d49b405..f67d21a4d 100644 --- a/packages/desktop/src/shortcut.rs +++ b/packages/desktop/src/shortcut.rs @@ -1,11 +1,10 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr}; -use dioxus_core::ScopeState; use dioxus_html::input_data::keyboard_types::Modifiers; use slab::Slab; use tao::keyboard::ModifiersState; -use crate::{desktop_context::DesktopContext, window}; +use crate::desktop_context::DesktopContext; #[cfg(any( target_os = "windows", @@ -26,7 +25,7 @@ pub use crate::mobile_shortcut::*; #[derive(Clone)] pub(crate) struct ShortcutRegistry { - manager: Rc>, + manager: Rc, shortcuts: ShortcutMap, } @@ -55,7 +54,7 @@ impl Shortcut { impl ShortcutRegistry { pub fn new() -> Self { Self { - manager: Rc::new(RefCell::new(GlobalHotKeyManager::new().unwrap())), + manager: Rc::new(GlobalHotKeyManager::new().unwrap()), shortcuts: Rc::new(RefCell::new(HashMap::new())), } } @@ -74,36 +73,36 @@ impl ShortcutRegistry { callback: Box, ) -> Result { let accelerator_id = hotkey.clone().id(); + let mut shortcuts = self.shortcuts.borrow_mut(); - Ok( - if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) { - let id = callbacks.insert(callback); - ShortcutId { - id: accelerator_id, - number: id, - } - } else { - match self.manager.borrow_mut().register(hotkey) { - Ok(_) => { - let mut slab = Slab::new(); - let id = slab.insert(callback); - let shortcut = Shortcut { - shortcut: hotkey, - callbacks: slab, - }; - shortcuts.insert(accelerator_id, shortcut); - ShortcutId { - id: accelerator_id, - number: id, - } - } - Err(HotkeyError::HotKeyParseError(shortcut)) => { - return Err(ShortcutRegistryError::InvalidShortcut(shortcut)) - } - Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))), - } - }, - ) + + if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) { + return Ok(ShortcutId { + id: accelerator_id, + number: callbacks.insert(callback), + }); + }; + + self.manager.register(hotkey).map_err(|e| match e { + HotkeyError::HotKeyParseError(shortcut) => { + ShortcutRegistryError::InvalidShortcut(shortcut) + } + err => ShortcutRegistryError::Other(Box::new(err)), + })?; + + let mut shortcut = Shortcut { + shortcut: hotkey, + callbacks: Slab::new(), + }; + + let id = shortcut.callbacks.insert(callback); + + shortcuts.insert(accelerator_id, shortcut); + + Ok(ShortcutId { + id: accelerator_id, + number: id, + }) } pub(crate) fn remove_shortcut(&self, id: ShortcutId) { @@ -112,7 +111,7 @@ impl ShortcutRegistry { callbacks.remove(id.number); if callbacks.is_empty() { if let Some(_shortcut) = shortcuts.remove(&id.id) { - let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut); + let _ = self.manager.unregister(_shortcut.shortcut); } } } @@ -121,7 +120,7 @@ impl ShortcutRegistry { pub(crate) fn remove_all(&self) { let mut shortcuts = self.shortcuts.borrow_mut(); let hotkeys: Vec<_> = shortcuts.drain().map(|(_, v)| v.shortcut).collect(); - let _ = self.manager.borrow_mut().unregister_all(&hotkeys); + let _ = self.manager.unregister_all(&hotkeys); } } @@ -144,7 +143,7 @@ pub struct ShortcutId { /// A global shortcut. This will be automatically removed when it is dropped. pub struct ShortcutHandle { - desktop: DesktopContext, + pub(crate) desktop: DesktopContext, /// The id of the shortcut pub shortcut_id: ShortcutId, } @@ -177,24 +176,6 @@ impl IntoAccelerator for &str { } } -/// Get a closure that executes any JavaScript in the WebView context. -pub fn use_global_shortcut( - cx: &ScopeState, - accelerator: impl IntoAccelerator, - handler: impl FnMut() + 'static, -) -> &Result { - cx.use_hook(move || { - let desktop = window(); - - let id = desktop.create_shortcut(accelerator.accelerator(), handler); - - Ok(ShortcutHandle { - desktop, - shortcut_id: id?, - }) - }) -} - impl ShortcutHandle { /// Remove the shortcut. pub fn remove(&self) { diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 537b3e241..cd83362b5 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -30,9 +30,9 @@ pub struct WebviewHandler { pub fn create_new_window( mut cfg: Config, + dom: VirtualDom, event_loop: &EventLoopWindowTarget, proxy: &EventLoopProxy, - dom: VirtualDom, queue: &WebviewQueue, event_handlers: &WindowEventHandlers, shortcut_manager: ShortcutRegistry, From 060490892ac6d3ff7d6dc1362d1583b46e3d817e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 18:19:28 -0800 Subject: [PATCH 077/126] Clean up desktop even more --- packages/desktop/src/app.rs | 142 +++++++++--------------- packages/desktop/src/assets.rs | 7 +- packages/desktop/src/desktop_context.rs | 14 +-- packages/desktop/src/eval.rs | 16 ++- packages/desktop/src/file_upload.rs | 125 +++++++++++---------- packages/desktop/src/webview.rs | 11 +- 6 files changed, 137 insertions(+), 178 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 4e954d496..9a45e6c19 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -1,48 +1,25 @@ -pub use crate::assets::{AssetFuture, AssetHandler, AssetRequest, AssetResponse}; -pub use crate::cfg::{Config, WindowCloseBehaviour}; -pub use crate::desktop_context::DesktopContext; -pub use crate::desktop_context::{window, DesktopService, WryEventHandler, WryEventHandlerId}; -use crate::edits::{EditQueue, WebviewQueue}; -use crate::element::DesktopElement; -use crate::eval::init_eval; -use crate::events::{IpcMessage, IpcMethod}; -use crate::file_upload; -use crate::hooks::*; -use crate::query::QueryResult; -use crate::shortcut::GlobalHotKeyEvent; -use crate::shortcut::ShortcutRegistry; -pub use crate::shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError}; use crate::{ + cfg::{Config, WindowCloseBehaviour}, desktop_context::{EventData, UserWindowEvent, WindowEventHandlers}, + edits::WebviewQueue, + element::DesktopElement, + events::IpcMessage, + file_upload::FileDialogRequest, + query::QueryResult, + shortcut::{GlobalHotKeyEvent, ShortcutRegistry}, webview::WebviewHandler, }; -use dioxus_core::*; -use dioxus_html::{event_bubbles, MountedData}; +use dioxus_core::{Component, ElementId, VirtualDom}; +use dioxus_html::MountedData; use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; -use dioxus_interpreter_js::binary_protocol::Channel; use futures_util::{pin_mut, FutureExt}; -use global_hotkey::{ - hotkey::{Code, HotKey, Modifiers}, - GlobalHotKeyManager, -}; -use rustc_hash::FxHashMap; +use std::cell::Cell; use std::rc::Rc; -use std::sync::atomic::AtomicU16; -use std::task::Waker; -use std::{borrow::Borrow, cell::Cell}; use std::{collections::HashMap, sync::Arc}; -pub use tao::dpi::{LogicalSize, PhysicalSize}; +use tao::event_loop::EventLoopBuilder; use tao::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; -pub use tao::window::WindowBuilder; use tao::window::WindowId; -use tao::{ - event::{Event, StartCause, WindowEvent}, - event_loop::ControlFlow, -}; -use tao::{event_loop::EventLoopBuilder, window::Window}; -use tokio::runtime::Builder; -pub use wry; -use wry::WebContext; +use tao::{event::Event, event_loop::ControlFlow}; use wry::WebView; pub struct App

{ @@ -221,24 +198,14 @@ impl App

{ return; }; - view.dom - .base_scope() - .consume_context::() - .unwrap() - .query - .send(result); + view.desktop_context.query.send(result); } pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) { - let params = msg.params(); + let parsed_params = serde_json::from_value(msg.params()) + .map_err(|err| tracing::error!("Error parsing user_event: {:?}", err)); - let evt = match serde_json::from_value::(params) { - Ok(value) => value, - Err(err) => { - tracing::error!("Error parsing user_event: {:?}", err); - return; - } - }; + let Ok(evt) = parsed_params else { return }; let HtmlEvent { element, @@ -250,20 +217,13 @@ impl App

{ let view = self.webviews.get_mut(&id).unwrap(); // check for a mounted event placeholder and replace it with a desktop specific element - let as_any = if let dioxus_html::EventData::Mounted = &data { - let query = view - .dom - .base_scope() - .consume_context::() - .unwrap() - .query - .clone(); - - let element = DesktopElement::new(element, view.desktop_context.clone(), query); - - Rc::new(MountedData::new(element)) - } else { - data.into_any() + let as_any = match data { + dioxus_html::EventData::Mounted => Rc::new(MountedData::new(DesktopElement::new( + element, + view.desktop_context.clone(), + view.desktop_context.query.clone(), + ))), + _ => data.into_any(), }; view.dom.handle_event(&name, as_any, element, bubbles); @@ -278,9 +238,7 @@ impl App

{ webview.dom.replace_template(template); } - let ids = self.webviews.keys().copied().collect::>(); - - for id in ids { + for id in self.webviews.keys().copied().collect::>() { self.poll_vdom(id); } } @@ -291,31 +249,32 @@ impl App

{ } pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) { - if let Ok(file_diolog) = - serde_json::from_value::(msg.params()) - { - let id = ElementId(file_diolog.target); - let event_name = &file_diolog.event; - let event_bubbles = file_diolog.bubbles; - let files = file_upload::get_file_event(&file_diolog); - let data = Rc::new(FormData { - value: Default::default(), - values: Default::default(), - files: Some(Arc::new(NativeFileEngine::new(files))), - }); + let Ok(file_dialog) = serde_json::from_value::(msg.params()) else { + return; + }; - let view = self.webviews.get_mut(&window).unwrap(); + let id = ElementId(file_dialog.target); + let event_name = &file_dialog.event; + let event_bubbles = file_dialog.bubbles; + let files = file_dialog.get_file_event(); - if event_name == "change&input" { - view.dom - .handle_event("input", data.clone(), id, event_bubbles); - view.dom.handle_event("change", data, id, event_bubbles); - } else { - view.dom.handle_event(event_name, data, id, event_bubbles); - } + let data = Rc::new(FormData { + value: Default::default(), + values: Default::default(), + files: Some(Arc::new(NativeFileEngine::new(files))), + }); - view.desktop_context.send_edits(view.dom.render_immediate()); + let view = self.webviews.get_mut(&window).unwrap(); + + if event_name == "change&input" { + view.dom + .handle_event("input", data.clone(), id, event_bubbles); + view.dom.handle_event("change", data, id, event_bubbles); + } else { + view.dom.handle_event(event_name, data, id, event_bubbles); } + + view.desktop_context.send_edits(view.dom.render_immediate()); } /// Poll the virtualdom until it's pending @@ -324,10 +283,15 @@ impl App

{ /// /// All IO is done on the tokio runtime we started earlier pub fn poll_vdom(&mut self, id: WindowId) { - let view = self.webviews.get_mut(&id).unwrap(); + let Some(view) = self.webviews.get_mut(&id) else { + return; + }; let mut cx = std::task::Context::from_waker(&view.waker); + // Continously poll the virtualdom until it's pending + // Wait for work will return Ready when it has edits to be sent to the webview + // It will return Pending when it needs to be polled again - nothing is ready loop { { let fut = view.dom.wait_for_work(); @@ -335,7 +299,7 @@ impl App

{ match fut.poll_unpin(&mut cx) { std::task::Poll::Ready(_) => {} - std::task::Poll::Pending => break, + std::task::Poll::Pending => return, } } diff --git a/packages/desktop/src/assets.rs b/packages/desktop/src/assets.rs index f80c7542a..7f86b6184 100644 --- a/packages/desktop/src/assets.rs +++ b/packages/desktop/src/assets.rs @@ -1,6 +1,4 @@ -use crate::edits::EditQueue; use crate::DesktopContext; -use dioxus_core::ScopeState; use slab::Slab; use std::{ borrow::Cow, @@ -15,10 +13,7 @@ use tokio::{ runtime::Handle, sync::{OnceCell, RwLock}, }; -use wry::{ - http::{status::StatusCode, Request, Response}, - Result, -}; +use wry::http::{Request, Response}; /// An arbitrary asset is an HTTP response containing a binary body. pub type AssetResponse = Response>; diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 240f8ec24..ef2f671b2 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -146,25 +146,19 @@ impl DesktopService { self.shortcut_manager.clone(), ); - let desktop_context = window - .dom - .base_scope() - .consume_context::>() - .unwrap(); - - let id = window.desktop_context.window.id(); + let cx = window.desktop_context.clone(); self.proxy - .send_event(UserWindowEvent(EventData::NewWindow, id)) + .send_event(UserWindowEvent(EventData::NewWindow, cx.id())) .unwrap(); self.proxy - .send_event(UserWindowEvent(EventData::Poll, id)) + .send_event(UserWindowEvent(EventData::Poll, cx.id())) .unwrap(); self.pending_windows.borrow_mut().push(window); - Rc::downgrade(&desktop_context) + Rc::downgrade(&cx) } /// trigger the drag-window event diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index e187be450..e9b0f036b 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -1,21 +1,19 @@ #![allow(clippy::await_holding_refcell_ref)] use async_trait::async_trait; -use dioxus_core::ScopeState; use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; use std::{cell::RefCell, rc::Rc}; use crate::{query::Query, DesktopContext}; -/// Provides the DesktopEvalProvider through [`cx.provide_context`]. -pub fn init_eval(cx: &ScopeState) { - let desktop_ctx = cx.consume_context::().unwrap(); - let provider: Rc = Rc::new(DesktopEvalProvider { desktop_ctx }); - cx.provide_context(provider); -} - /// Reprents the desktop-target's provider of evaluators. pub struct DesktopEvalProvider { - desktop_ctx: DesktopContext, + pub(crate) desktop_ctx: DesktopContext, +} + +impl DesktopEvalProvider { + pub fn new(desktop_ctx: DesktopContext) -> Self { + Self { desktop_ctx } + } } impl EvalProvider for DesktopEvalProvider { diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index 7b5f08fc3..98dbec437 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -14,74 +14,77 @@ pub(crate) struct FileDialogRequest { pub bubbles: bool, } -#[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -)))] -pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec { - vec![] -} +#[allow(unused)] +impl FileDialogRequest { + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + )))] + pub(crate) fn get_file_event(&self) -> Vec { + vec![] + } -#[cfg(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec { - fn get_file_event_for_folder( - request: &FileDialogRequest, - dialog: rfd::FileDialog, - ) -> Vec { - if request.multiple { - dialog.pick_folders().into_iter().flatten().collect() - } else { - dialog.pick_folder().into_iter().collect() + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub(crate) fn get_file_event(&self) -> Vec { + fn get_file_event_for_folder( + request: &FileDialogRequest, + dialog: rfd::FileDialog, + ) -> Vec { + if request.multiple { + dialog.pick_folders().into_iter().flatten().collect() + } else { + dialog.pick_folder().into_iter().collect() + } } - } - fn get_file_event_for_file( - request: &FileDialogRequest, - mut dialog: rfd::FileDialog, - ) -> Vec { - let filters: Vec<_> = request - .accept - .as_deref() - .unwrap_or_default() - .split(',') - .filter_map(|s| Filters::from_str(s).ok()) - .collect(); + fn get_file_event_for_file( + request: &FileDialogRequest, + mut dialog: rfd::FileDialog, + ) -> Vec { + let filters: Vec<_> = request + .accept + .as_deref() + .unwrap_or_default() + .split(',') + .filter_map(|s| Filters::from_str(s).ok()) + .collect(); - let file_extensions: Vec<_> = filters - .iter() - .flat_map(|f| f.as_extensions().into_iter()) - .collect(); + let file_extensions: Vec<_> = filters + .iter() + .flat_map(|f| f.as_extensions().into_iter()) + .collect(); - dialog = dialog.add_filter("name", file_extensions.as_slice()); + dialog = dialog.add_filter("name", file_extensions.as_slice()); - let files: Vec<_> = if request.multiple { - dialog.pick_files().into_iter().flatten().collect() + let files: Vec<_> = if request.multiple { + dialog.pick_files().into_iter().flatten().collect() + } else { + dialog.pick_file().into_iter().collect() + }; + + files + } + + let dialog = rfd::FileDialog::new(); + + if self.directory { + get_file_event_for_folder(self, dialog) } else { - dialog.pick_file().into_iter().collect() - }; - - files - } - - let dialog = rfd::FileDialog::new(); - - if request.directory { - get_file_event_for_folder(request, dialog) - } else { - get_file_event_for_file(request, dialog) + get_file_event_for_file(self, dialog) + } } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index cd83362b5..6eac25b62 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,15 +1,17 @@ use std::{rc::Rc, task::Waker}; -use crate::edits::{EditQueue, WebviewQueue}; use crate::{ assets::AssetHandlerRegistry, desktop_context::UserWindowEvent, waker::tao_waker, Config, DesktopContext, }; use crate::{ desktop_context::{EventData, WindowEventHandlers}, - eval::init_eval, shortcut::ShortcutRegistry, }; +use crate::{ + edits::{EditQueue, WebviewQueue}, + eval::DesktopEvalProvider, +}; use crate::{ protocol::{self}, DesktopService, @@ -162,9 +164,12 @@ pub fn create_new_window( asset_handlers, )); + // Provide the desktop context to the virtualdom dom.base_scope().provide_context(desktop_context.clone()); - init_eval(dom.base_scope()); + // Also set up its eval provider + dom.base_scope() + .provide_context(Rc::new(DesktopEvalProvider::new(desktop_context.clone()))); WebviewHandler { // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both From 73d5069a2056c0a2ea4a0d88e00c91a7e0d12185 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 18:31:22 -0800 Subject: [PATCH 078/126] Move launch functions into their own file --- packages/desktop/src/launch.rs | 120 ++++++++++++++++++++++++++++++ packages/desktop/src/lib.rs | 130 ++------------------------------- 2 files changed, 126 insertions(+), 124 deletions(-) create mode 100644 packages/desktop/src/launch.rs diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs new file mode 100644 index 000000000..8af51e137 --- /dev/null +++ b/packages/desktop/src/launch.rs @@ -0,0 +1,120 @@ +use crate::{ + app::App, + desktop_context::{EventData, UserWindowEvent}, + events::IpcMethod, + Config, +}; +use dioxus_core::*; +use tao::event::{Event, StartCause, WindowEvent}; +use tokio::runtime::Builder; + +/// Launch the WebView and run the event loop. +/// +/// This function will start a multithreaded Tokio runtime as well the WebView event loop. +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// +/// fn main() { +/// dioxus_desktop::launch(app); +/// } +/// +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello world!"} +/// }) +/// } +/// ``` +pub fn launch(root: Component) { + launch_with_props(root, (), Config::default()) +} + +/// Launch the WebView and run the event loop, with configuration. +/// +/// This function will start a multithreaded Tokio runtime as well the WebView event loop. +/// +/// You can configure the WebView window with a configuration closure +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// use dioxus_desktop::*; +/// +/// fn main() { +/// dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App"))); +/// } +/// +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello world!"} +/// }) +/// } +/// ``` +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. +/// +/// 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 +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// use dioxus_desktop::Config; +/// +/// fn main() { +/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); +/// } +/// +/// struct AppProps { +/// name: &'static str +/// } +/// +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello {cx.props.name}!"} +/// }) +/// } +/// ``` +pub fn launch_with_props(root: Component

, props: P, cfg: Config) { + // We start the tokio runtime *on this thread* + // Any future we poll later will use this runtime to spawn tasks and for IO + // I would love to just allow dioxus to work with any runtime... but tokio is weird + let rt = Builder::new_multi_thread().enable_all().build().unwrap(); + let _guard = rt.enter(); + + let (event_loop, mut app) = App::new(cfg, props, root); + + event_loop.run(move |window_event, event_loop, control_flow| { + app.tick(&window_event, event_loop); + + match window_event { + Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(event_loop), + Event::WindowEvent { + event, window_id, .. + } => match event { + WindowEvent::CloseRequested => app.handle_close_requested(window_id), + WindowEvent::Destroyed { .. } => app.window_destroyed(window_id), + _ => {} + }, + Event::UserEvent(UserWindowEvent(event, id)) => match event { + EventData::Poll => app.poll_vdom(id), + EventData::NewWindow => app.handle_new_window(), + EventData::CloseWindow => app.handle_close_msg(id), + EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), + EventData::Ipc(msg) => match msg.method() { + IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), + IpcMethod::UserEvent => app.handle_user_event_msg(msg, id), + IpcMethod::Query => app.handle_query_msg(msg, id), + IpcMethod::BrowserOpen => app.handle_browser_open(msg), + IpcMethod::Initialize => app.handle_initialize_msg(id), + IpcMethod::Other(_) => {} + }, + }, + _ => {} + } + + *control_flow = app.control_flow; + }) +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index ca1e851fb..818605dc5 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -23,12 +23,12 @@ mod shortcut; mod waker; mod webview; -// Re-exports -pub use tao::{ - self, - dpi::{LogicalSize, PhysicalSize}, - window::WindowBuilder, -}; +// The main entrypoint for this crate +pub use launch::*; +mod launch; + +// Reexport tao and wry, might want to re-export other important things +pub use tao; pub use wry; // Public exports @@ -40,121 +40,3 @@ pub use desktop_context::{ pub use hooks::{use_asset_handler, use_global_shortcut, use_window, use_wry_event_handler}; pub use menubar::build_default_menu_bar; pub use shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError}; - -#[allow(deprecated)] -use desktop_context::{EventData, UserWindowEvent}; -use dioxus_core::*; -use events::IpcMethod; -use tao::event::{Event, StartCause, WindowEvent}; -use tokio::runtime::Builder; - -/// Launch the WebView and run the event loop. -/// -/// This function will start a multithreaded Tokio runtime as well the WebView event loop. -/// -/// ```rust, no_run -/// use dioxus::prelude::*; -/// -/// fn main() { -/// dioxus_desktop::launch(app); -/// } -/// -/// fn app(cx: Scope) -> Element { -/// cx.render(rsx!{ -/// h1 {"hello world!"} -/// }) -/// } -/// ``` -pub fn launch(root: Component) { - launch_with_props(root, (), Config::default()) -} - -/// Launch the WebView and run the event loop, with configuration. -/// -/// This function will start a multithreaded Tokio runtime as well the WebView event loop. -/// -/// You can configure the WebView window with a configuration closure -/// -/// ```rust, no_run -/// use dioxus::prelude::*; -/// use dioxus_desktop::*; -/// -/// fn main() { -/// dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App"))); -/// } -/// -/// fn app(cx: Scope) -> Element { -/// cx.render(rsx!{ -/// h1 {"hello world!"} -/// }) -/// } -/// ``` -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. -/// -/// 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 -/// -/// ```rust, no_run -/// use dioxus::prelude::*; -/// use dioxus_desktop::Config; -/// -/// fn main() { -/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); -/// } -/// -/// struct AppProps { -/// name: &'static str -/// } -/// -/// fn app(cx: Scope) -> Element { -/// cx.render(rsx!{ -/// h1 {"hello {cx.props.name}!"} -/// }) -/// } -/// ``` -pub fn launch_with_props(root: Component

, props: P, cfg: Config) { - // We start the tokio runtime *on this thread* - // Any future we poll later will use this runtime to spawn tasks and for IO - // I would love to just allow dioxus to work with any runtime... but tokio is weird - let rt = &Builder::new_multi_thread().enable_all().build().unwrap(); - let _guard = rt.enter(); - - let (event_loop, mut app) = app::App::new(cfg, props, root); - - event_loop.run(move |window_event, event_loop, control_flow| { - app.tick(&window_event, event_loop); - - match window_event { - Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(event_loop), - Event::WindowEvent { - event, window_id, .. - } => match event { - WindowEvent::CloseRequested => app.handle_close_requested(window_id), - WindowEvent::Destroyed { .. } => app.window_destroyed(window_id), - _ => {} - }, - Event::UserEvent(UserWindowEvent(event, id)) => match event { - EventData::Poll => app.poll_vdom(id), - EventData::NewWindow => app.handle_new_window(), - EventData::CloseWindow => app.handle_close_msg(id), - EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), - EventData::Ipc(msg) => match msg.method() { - IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), - IpcMethod::UserEvent => app.handle_user_event_msg(msg, id), - IpcMethod::Query => app.handle_query_msg(msg, id), - IpcMethod::BrowserOpen => app.handle_browser_open(msg), - IpcMethod::Initialize => app.handle_initialize_msg(id), - IpcMethod::Other(_) => {} - }, - }, - _ => {} - } - - *control_flow = app.control_flow; - }) -} From 38216fa2729a8f07fcb055d60dc66bebdd6db908 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 19:09:59 -0800 Subject: [PATCH 079/126] Make the tokio runtime optional --- examples/clock.rs | 5 +++-- packages/desktop/src/app.rs | 24 +++++++++----------- packages/desktop/src/cfg.rs | 15 +++++++++++++ packages/desktop/src/desktop_context.rs | 2 +- packages/desktop/src/{events.rs => ipc.rs} | 0 packages/desktop/src/launch.rs | 26 ++++++++++++++++------ packages/desktop/src/lib.rs | 7 +++--- packages/desktop/src/protocol.rs | 9 ++++++-- 8 files changed, 60 insertions(+), 28 deletions(-) rename packages/desktop/src/{events.rs => ipc.rs} (100%) diff --git a/examples/clock.rs b/examples/clock.rs index f9c1892d9..4b7be3d3b 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -1,7 +1,8 @@ use dioxus::prelude::*; use dioxus_signals::use_signal; -fn main() { +#[tokio::main] +async fn main() { dioxus_desktop::launch(app); } @@ -10,7 +11,7 @@ fn app(cx: Scope) -> Element { use_future!(cx, || async move { loop { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + tokio::time::sleep(std::time::Duration::from_millis(10)).await; count += 1; println!("current: {count}"); } diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 9a45e6c19..c8ca8a92a 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -3,30 +3,28 @@ use crate::{ desktop_context::{EventData, UserWindowEvent, WindowEventHandlers}, edits::WebviewQueue, element::DesktopElement, - events::IpcMessage, file_upload::FileDialogRequest, + ipc::IpcMessage, query::QueryResult, shortcut::{GlobalHotKeyEvent, ShortcutRegistry}, webview::WebviewHandler, }; use dioxus_core::{Component, ElementId, VirtualDom}; -use dioxus_html::MountedData; -use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; +use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent, MountedData}; use futures_util::{pin_mut, FutureExt}; -use std::cell::Cell; -use std::rc::Rc; -use std::{collections::HashMap, sync::Arc}; -use tao::event_loop::EventLoopBuilder; -use tao::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; -use tao::window::WindowId; -use tao::{event::Event, event_loop::ControlFlow}; -use wry::WebView; +use std::{cell::Cell, collections::HashMap, rc::Rc, sync::Arc}; +use tao::{ + event::Event, + event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, + window::WindowId, +}; -pub struct App

{ +pub(crate) struct App

{ // move the props into a cell so we can pop it out later to create the first window // iOS panics if we create a window before the event loop is started pub(crate) props: Rc>>, pub(crate) cfg: Rc>>, + pub(crate) root: Component

, pub(crate) webviews: HashMap, pub(crate) event_handlers: WindowEventHandlers, @@ -310,7 +308,7 @@ impl App

{ /// Different hide implementations per platform #[allow(unused)] -pub fn hide_app_window(webview: &WebView) { +pub fn hide_app_window(webview: &wry::WebView) { #[cfg(target_os = "windows")] { use wry::application::platform::windows::WindowExtWindows; diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 4fb7f6ecb..a5d17b541 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::path::PathBuf; +use dioxus_core::prelude::Component; use tao::window::{Icon, WindowBuilder, WindowId}; use wry::{ http::{Request as HttpRequest, Response as HttpResponse}, @@ -68,6 +69,20 @@ impl Config { } } + /// Launch a Dioxus app using the given component and config + /// + /// See the [`crate::launch::launch`] function for more details. + pub fn launch(self, root: Component<()>) { + crate::launch::launch_cfg(root, self) + } + + /// Launch a Dioxus app using the given component, config, and props + /// + /// See the [`crate::launch::launch_with_props`] function for more details. + pub fn launch_with_props(self, root: Component

, props: P) { + crate::launch::launch_with_props(root, props, self) + } + /// Set whether the default menu bar should be enabled. /// /// > Note: `enable` is `true` by default. To disable the default menu bar pass `false`. diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index ef2f671b2..e1ed1f07a 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,4 +1,4 @@ -use crate::events::IpcMessage; +use crate::ipc::IpcMessage; use crate::query::QueryEngine; use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; use crate::AssetHandler; diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/ipc.rs similarity index 100% rename from packages/desktop/src/events.rs rename to packages/desktop/src/ipc.rs diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index 8af51e137..8ee939494 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -1,12 +1,11 @@ use crate::{ app::App, desktop_context::{EventData, UserWindowEvent}, - events::IpcMethod, + ipc::IpcMethod, Config, }; use dioxus_core::*; use tao::event::{Event, StartCause, WindowEvent}; -use tokio::runtime::Builder; /// Launch the WebView and run the event loop. /// @@ -55,6 +54,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// Launch the WebView and run the event loop, with configuration and root props. /// +/// If the [`tokio`] feature is enabled, this will also startup and block a tokio runtime using the unconstrained task. /// 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 @@ -78,12 +78,24 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// } /// ``` pub fn launch_with_props(root: Component

, props: P, cfg: Config) { - // We start the tokio runtime *on this thread* - // Any future we poll later will use this runtime to spawn tasks and for IO - // I would love to just allow dioxus to work with any runtime... but tokio is weird - let rt = Builder::new_multi_thread().enable_all().build().unwrap(); - let _guard = rt.enter(); + #[cfg(feature = "tokio")] + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(tokio::task::unconstrained(async move { + launch_with_props_blocking(root, props, cfg); + })); + #[cfg(not(feature = "tokio"))] + launch_with_props_blocking(root, props, cfg); +} + +/// Launch the WebView and run the event loop, with configuration and root props. +/// +/// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime +/// and is equivalent to calling launch_with_props with the tokio feature disabled. +pub fn launch_with_props_blocking(root: Component

, props: P, cfg: Config) { let (event_loop, mut app) = App::new(cfg, props, root); event_loop.run(move |window_event, event_loop, control_flow| { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 818605dc5..3f8d9d3ce 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -11,18 +11,19 @@ mod edits; mod element; mod escape; mod eval; -mod events; mod file_upload; mod hooks; +mod ipc; mod menubar; -#[cfg(any(target_os = "ios", target_os = "android"))] -mod mobile_shortcut; mod protocol; mod query; mod shortcut; mod waker; mod webview; +#[cfg(any(target_os = "ios", target_os = "android"))] +mod mobile_shortcut; + // The main entrypoint for this crate pub use launch::*; mod launch; diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 77acebad6..bdcefa473 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -57,7 +57,8 @@ pub(super) async fn desktop_handler( .body(Cow::from(body)) { Ok(response) => { - return responder.respond(response); + responder.respond(response); + return; } Err(err) => tracing::error!("error building response: {}", err), } @@ -103,7 +104,10 @@ pub(super) async fn desktop_handler( .header("Content-Type", content_type) .body(Cow::from(asset)) { - Ok(response) => return responder.respond(response), + Ok(response) => { + responder.respond(response); + return; + } Err(err) => tracing::error!("error building response: {}", err), } } @@ -114,6 +118,7 @@ pub(super) async fn desktop_handler( { Ok(response) => { responder.respond(response); + return; } Err(err) => tracing::error!("error building response: {}", err), } From 2e9737ef57ad37a8eb7444ebe3b32aa6c3fbf75f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 19:52:49 -0800 Subject: [PATCH 080/126] Clean up and document the protocol handler --- examples/clock.rs | 3 +- packages/desktop/src/index.html | 18 +-- packages/desktop/src/protocol.rs | 199 ++++++++++++++----------------- 3 files changed, 99 insertions(+), 121 deletions(-) diff --git a/examples/clock.rs b/examples/clock.rs index 4b7be3d3b..ba47d994a 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -1,8 +1,7 @@ use dioxus::prelude::*; use dioxus_signals::use_signal; -#[tokio::main] -async fn main() { +fn main() { dioxus_desktop::launch(app); } diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 0704fd0f0..45926e3f1 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -1,12 +1,12 @@ - - Dioxus app - - - - -

- - + + Dioxus app + + + + +
+ + diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index bdcefa473..be3c12231 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -5,10 +5,11 @@ use std::{ }; use wry::{ http::{status::StatusCode, Request, Response}, - Result, + RequestAsyncResponder, Result, }; static MINIFIED: &str = include_str!("./minified.js"); +static DEFAULT_INDEX: &str = include_str!("./index.html"); pub(super) async fn desktop_handler( request: Request>, @@ -18,120 +19,110 @@ pub(super) async fn desktop_handler( asset_handlers: &AssetHandlerRegistry, edit_queue: &EditQueue, headless: bool, - responder: wry::RequestAsyncResponder, + responder: RequestAsyncResponder, ) { let request = AssetRequest::from(request); // If the request is for the root, we'll serve the index.html file. if request.uri().path() == "/" { - // If a custom index is provided, just defer to that, expecting the user to know what they're doing. - // we'll look for the closing tag and insert our little module loader there. - let body = match custom_index { - Some(custom_index) => custom_index - .replace( - "", - &format!("{}", module_loader(root_name, headless)), - ) - .into_bytes(), - - None => { - // Otherwise, we'll serve the default index.html and apply a custom head if that's specified. - let mut template = include_str!("./index.html").to_string(); - - if let Some(custom_head) = custom_head { - template = template.replace("", &custom_head); - } - - template - .replace( - "", - &module_loader(root_name, headless), - ) - .into_bytes() - } - }; - - match Response::builder() - .header("Content-Type", "text/html") - .header("Access-Control-Allow-Origin", "*") - .body(Cow::from(body)) - { - Ok(response) => { - responder.respond(response); - return; - } - Err(err) => tracing::error!("error building response: {}", err), + match build_index_file(custom_index, custom_head, root_name, headless) { + Ok(response) => return responder.respond(response), + Err(err) => return tracing::error!("error building response: {}", err), } - } else if request.uri().path().trim_matches('/') == "edits" { - edit_queue.handle_request(responder); - return; } - // If the user provided a custom asset handler, then call it and return the response - // if the request was handled. + // If the request is asking for edits (ie binary protocol streaming, do that) + if request.uri().path().trim_matches('/') == "edits" { + return edit_queue.handle_request(responder); + } + + // If the user provided a custom asset handler, then call it and return the response if the request was handled. if let Some(response) = asset_handlers.try_handlers(&request).await { - responder.respond(response); - return; + return responder.respond(response); } // Else, try to serve a file from the filesystem. + match serve_from_fs(request) { + Ok(res) => responder.respond(res), + Err(e) => tracing::error!("Error serving request from filesystem {}", e), + } +} +fn serve_from_fs(request: AssetRequest) -> Result { // If the path is relative, we'll try to serve it from the assets directory. let mut asset = get_asset_root() .unwrap_or_else(|| Path::new(".").to_path_buf()) .join(&request.path); + // If we can't find it, make it absolute and try again if !asset.exists() { asset = PathBuf::from("/").join(request.path); } if asset.exists() { - let content_type = match get_mime_from_path(&asset) { - Ok(content_type) => content_type, - Err(err) => { - tracing::error!("error getting mime type: {}", err); - return; - } - }; - let asset = match std::fs::read(asset) { - Ok(asset) => asset, - Err(err) => { - tracing::error!("error reading asset: {}", err); - return; - } - }; - match Response::builder() - .header("Content-Type", content_type) - .body(Cow::from(asset)) - { - Ok(response) => { - responder.respond(response); - return; - } - Err(err) => tracing::error!("error building response: {}", err), - } - } + let content_type = get_mime_from_path(&asset)?; + let asset = std::fs::read(asset)?; - match Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Cow::from(String::from("Not Found").into_bytes())) - { - Ok(response) => { - responder.respond(response); - return; - } - Err(err) => tracing::error!("error building response: {}", err), + Ok(Response::builder() + .header("Content-Type", content_type) + .body(Cow::from(asset))?) + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Cow::from(String::from("Not Found").into_bytes()))?) } } -fn module_loader(root_name: &str, headless: bool) -> String { +/// Build the index.html file we use for bootstrapping a new app +/// +/// We use wry/webview by building a special index.html that forms a bridge between the webview and your rust code +/// +/// This is similar to tauri, except we give more power to your rust code and less power to your frontend code. +/// This lets us skip a build/bundle step - your code just works - but limits how your Rust code can actually +/// mess with UI elements. We make this decision since other renderers like LiveView are very separate and can +/// never properly bridge the gap. Eventually of course, the idea is to build a custom CSS/HTML renderer where you +/// *do* have native control over elements, but that still won't work with liveview. +fn build_index_file( + custom_index: Option, + custom_head: Option, + root_name: &str, + headless: bool, +) -> std::result::Result>, wry::http::Error> { + // Load a custom index file if provided + let mut index = custom_index.unwrap_or_else(|| DEFAULT_INDEX.to_string()); + + // Insert a custom head if provided + // We look just for the closing head tag. If a user provided a custom index with weird syntax, this might fail + if let Some(head) = custom_head { + index.insert_str(index.find("").expect("Head element to exist"), &head); + } + + // Inject our module loader + index.insert_str( + index.find("").expect("Body element to exist"), + &module_loader(root_name, headless), + ); + + Response::builder() + .header("Content-Type", "text/html") + .header("Access-Control-Allow-Origin", "*") + .body(Cow::from(index.into_bytes())) +} + +/// Construct the inline script that boots up the page and bridges the webview with rust code. +/// +/// The arguments here: +/// - root_name: the root element (by Id) that we stream edits into +/// - headless: is this page being loaded but invisible? Important because not all windows are visible and the +/// interpreter can't connect until the window is ready. +fn module_loader(root_id: &str, headless: bool) -> String { format!( r#"