diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index cb5a1f1bb..7b23c6111 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -43,7 +43,7 @@ jobs: # args: --path packages/cli - name: Run Playwright tests run: npx playwright test - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report diff --git a/.gitignore b/.gitignore index b8025753e..364e8d0fb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /dist Cargo.lock .DS_Store +/examples/assets/test_video.mp4 .vscode/* !.vscode/settings.json diff --git a/Cargo.toml b/Cargo.toml index 1772c1f47..29ae2d30d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,3 +133,4 @@ fern = { version = "0.6.0", features = ["colored"] } env_logger = "0.10.0" simple_logger = "4.0.0" thiserror = { workspace = true } +http-range = "0.1.5" diff --git a/README.md b/README.md index d1ec98019..65cd08263 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ So... Dioxus is great, but why won't it work for me? ## Contributing - Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing). - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). -- Join the discord and ask questions! +- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions! diff --git a/examples/all_events.rs b/examples/all_events.rs index 594a736d5..3f57ec747 100644 --- a/examples/all_events.rs +++ b/examples/all_events.rs @@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element { }; cx.render(rsx! ( - div { - style: "{CONTAINER_STYLE}", + div { style: "{CONTAINER_STYLE}", div { style: "{RECT_STYLE}", // focusing is necessary to catch keyboard events @@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element { onmousemove: move |event| log_event(Event::MouseMove(event)), onclick: move |event| log_event(Event::MouseClick(event)), - ondblclick: move |event| log_event(Event::MouseDoubleClick(event)), + ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event)), onmousedown: move |event| log_event(Event::MouseDown(event)), onmouseup: move |event| log_event(Event::MouseUp(event)), @@ -77,9 +76,7 @@ fn app(cx: Scope) -> Element { "Hover, click, type or scroll to see the info down below" } - div { - events.read().iter().map(|event| rsx!( div { "{event:?}" } )) - }, - }, + div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) } + } )) } diff --git a/examples/compose.rs b/examples/compose.rs index 3ee6d36ae..4b6a7450c 100644 --- a/examples/compose.rs +++ b/examples/compose.rs @@ -27,13 +27,7 @@ fn app(cx: Scope) -> Element { button { onclick: move |_| { - let dom = VirtualDom::new_with_props(compose, ComposeProps { - app_tx: tx.clone() - }); - - // this returns a weak reference to the other window - // Be careful not to keep a strong reference to the other window or it will never be dropped - // and the window will never close. + let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() }); window.new_window(dom, Default::default()); }, "Click to compose a new email" diff --git a/examples/dynamic_asset.rs b/examples/dynamic_asset.rs new file mode 100644 index 000000000..1c004e015 --- /dev/null +++ b/examples/dynamic_asset.rs @@ -0,0 +1,29 @@ +use dioxus::prelude::*; +use dioxus_desktop::wry::http::Response; +use dioxus_desktop::{use_asset_handler, AssetRequest}; +use std::path::Path; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + use_asset_handler(cx, |request: &AssetRequest| { + let path = request.path().to_path_buf(); + async move { + if path != Path::new("logo.png") { + return None; + } + let image_data: &[u8] = include_bytes!("./assets/logo.png"); + Some(Response::new(image_data.into())) + } + }); + + cx.render(rsx! { + div { + img { + src: "logo.png" + } + } + }) +} diff --git a/examples/mobile_demo/Cargo.toml b/examples/mobile_demo/Cargo.toml index ba2af2312..a90ff4f36 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.31.0" +wry = "0.34.0" dioxus = { path = "../../packages/dioxus" } dioxus-desktop = { path = "../../packages/desktop", features = [ "tokio_runtime", diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 9ec2dc228..0f3fada71 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -48,11 +48,8 @@ pub fn app(cx: Scope<()>) -> Element { cx.render(rsx! { section { class: "todoapp", style { include_str!("./assets/todomvc.css") } - TodoHeader { - todos: todos, - } - section { - class: "main", + TodoHeader { todos: todos } + section { class: "main", if !todos.is_empty() { rsx! { input { @@ -103,31 +100,34 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element { cx.render(rsx! { header { class: "header", - h1 {"todos"} - input { - class: "new-todo", - placeholder: "What needs to be done?", - value: "{draft}", - autofocus: "true", - oninput: move |evt| { - draft.set(evt.value.clone()); - }, - onkeydown: move |evt| { - if evt.key() == Key::Enter && !draft.is_empty() { - cx.props.todos.make_mut().insert( - **todo_id, - TodoItem { - id: **todo_id, - checked: false, - contents: draft.to_string(), - }, - ); - *todo_id.make_mut() += 1; - draft.set("".to_string()); + h1 { "todos" } + input { + class: "new-todo", + placeholder: "What needs to be done?", + value: "{draft}", + autofocus: "true", + oninput: move |evt| { + draft.set(evt.value.clone()); + }, + onkeydown: move |evt| { + if evt.key() == Key::Enter && !draft.is_empty() { + cx.props + .todos + .make_mut() + .insert( + **todo_id, + TodoItem { + id: **todo_id, + checked: false, + contents: draft.to_string(), + }, + ); + *todo_id.make_mut() += 1; + draft.set("".to_string()); + } } } } - } }) } @@ -146,8 +146,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { let editing = if **is_editing { "editing" } else { "" }; cx.render(rsx!{ - li { - class: "{completed} {editing}", + li { class: "{completed} {editing}", div { class: "view", input { class: "toggle", @@ -160,14 +159,16 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { } label { r#for: "cbg-{todo.id}", - ondblclick: move |_| is_editing.set(true), + ondoubleclick: move |_| is_editing.set(true), prevent_default: "onclick", "{todo.contents}" } button { class: "destroy", - onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); }, - prevent_default: "onclick", + onclick: move |_| { + cx.props.todos.make_mut().remove(&todo.id); + }, + prevent_default: "onclick" } } is_editing.then(|| rsx!{ @@ -213,15 +214,15 @@ pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element { cx.render(rsx! { footer { class: "footer", span { class: "todo-count", - strong {"{active_todo_count} "} - span {"{active_todo_text} left"} + strong { "{active_todo_count} " } + span { "{active_todo_text} left" } } ul { class: "filters", - for (state, state_text, url) in [ - (FilterState::All, "All", "#/"), - (FilterState::Active, "Active", "#/active"), - (FilterState::Completed, "Completed", "#/completed"), - ] { + for (state , state_text , url) in [ + (FilterState::All, "All", "#/"), + (FilterState::Active, "Active", "#/active"), + (FilterState::Completed, "Completed", "#/completed"), +] { li { a { href: url, @@ -250,8 +251,14 @@ pub fn PageFooter(cx: Scope) -> Element { cx.render(rsx! { footer { class: "info", p { "Double-click to edit a todo" } - p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }} - p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }} + p { + "Created by " + a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" } + } + p { + "Part of " + a { href: "http://todomvc.com", "TodoMVC" } + } } }) } diff --git a/examples/video_stream.rs b/examples/video_stream.rs new file mode 100644 index 000000000..f495299f4 --- /dev/null +++ b/examples/video_stream.rs @@ -0,0 +1,184 @@ +use dioxus::prelude::*; +use dioxus_desktop::wry::http; +use dioxus_desktop::wry::http::Response; +use dioxus_desktop::{use_asset_handler, AssetRequest}; +use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode}; +use std::borrow::Cow; +use std::{io::SeekFrom, path::PathBuf}; +use tokio::io::AsyncReadExt; +use tokio::io::AsyncSeekExt; +use tokio::io::AsyncWriteExt; + +const VIDEO_PATH: &str = "./examples/assets/test_video.mp4"; + +fn main() { + let video_file = PathBuf::from(VIDEO_PATH); + if !video_file.exists() { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + println!("Downloading video file..."); + let video_url = + "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; + let mut response = reqwest::get(video_url).await.unwrap(); + let mut file = tokio::fs::File::create(&video_file).await.unwrap(); + while let Some(chunk) = response.chunk().await.unwrap() { + file.write_all(&chunk).await.unwrap(); + } + }); + } + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + use_asset_handler(cx, move |request: &AssetRequest| { + let request = request.clone(); + async move { + let video_file = PathBuf::from(VIDEO_PATH); + let mut file = tokio::fs::File::open(&video_file).await.unwrap(); + let response: Option>> = + match get_stream_response(&mut file, &request).await { + Ok(response) => Some(response.map(Cow::Owned)), + Err(err) => { + eprintln!("Error: {}", err); + None + } + }; + response + } + }); + + render! { + div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } } + } +} + +async fn get_stream_response( + asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync), + request: &AssetRequest, +) -> Result>, Box> { + // get stream length + let len = { + let old_pos = asset.stream_position().await?; + let len = asset.seek(SeekFrom::End(0)).await?; + asset.seek(SeekFrom::Start(old_pos)).await?; + len + }; + + let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4"); + + // if the webview sent a range header, we need to send a 206 in return + // Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers. + let http_response = if let Some(range_header) = request.headers().get("range") { + let not_satisfiable = || { + ResponseBuilder::new() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(CONTENT_RANGE, format!("bytes */{len}")) + .body(vec![]) + }; + + // parse range header + let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) { + ranges + .iter() + // map the output back to spec range , example: 0-499 + .map(|r| (r.start, r.start + r.length - 1)) + .collect::>() + } else { + return Ok(not_satisfiable()?); + }; + + /// The Maximum bytes we send in one range + const MAX_LEN: u64 = 1000 * 1024; + + if ranges.len() == 1 { + let &(start, mut end) = ranges.first().unwrap(); + + // check if a range is not satisfiable + // + // this should be already taken care of by HttpRange::parse + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + return Ok(not_satisfiable()?); + } + + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + + // calculate number of bytes needed to be read + let bytes_to_read = end + 1 - start; + + // allocate a buf with a suitable capacity + let mut buf = Vec::with_capacity(bytes_to_read as usize); + // seek the file to the starting byte + asset.seek(SeekFrom::Start(start)).await?; + // read the needed bytes + asset.take(bytes_to_read).read_to_end(&mut buf).await?; + + resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); + resp = resp.header(CONTENT_LENGTH, end + 1 - start); + resp = resp.status(StatusCode::PARTIAL_CONTENT); + resp.body(buf) + } else { + let mut buf = Vec::new(); + let ranges = ranges + .iter() + .filter_map(|&(start, mut end)| { + // filter out unsatisfiable ranges + // + // this should be already taken care of by HttpRange::parse + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + None + } else { + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + Some((start, end)) + } + }) + .collect::>(); + + let boundary = format!("{:x}", rand::random::()); + let boundary_sep = format!("\r\n--{boundary}\r\n"); + let boundary_closer = format!("\r\n--{boundary}\r\n"); + + resp = resp.header( + CONTENT_TYPE, + format!("multipart/byteranges; boundary={boundary}"), + ); + + for (end, start) in ranges { + // a new range is being written, write the range boundary + buf.write_all(boundary_sep.as_bytes()).await?; + + // write the needed headers `Content-Type` and `Content-Range` + buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes()) + .await?; + buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes()) + .await?; + + // write the separator to indicate the start of the range body + buf.write_all("\r\n".as_bytes()).await?; + + // calculate number of bytes needed to be read + let bytes_to_read = end + 1 - start; + + let mut local_buf = vec![0_u8; bytes_to_read as usize]; + asset.seek(SeekFrom::Start(start)).await?; + asset.read_exact(&mut local_buf).await?; + buf.extend_from_slice(&local_buf); + } + // all ranges have been written, write the closing boundary + buf.write_all(boundary_closer.as_bytes()).await?; + + resp.body(buf) + } + } else { + resp = resp.header(CONTENT_LENGTH, len); + let mut buf = Vec::with_capacity(len as usize); + asset.read_to_end(&mut buf).await?; + resp.body(buf) + }; + + http_response.map_err(Into::into) +} diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 128133602..4c115184e 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -83,6 +83,7 @@ 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" } +gitignore = "1.0.8" [features] default = [] diff --git a/packages/cli/README.md b/packages/cli/README.md index 8379f22cc..2325d6784 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) -``` +```shell 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. -``` +```shell 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 -``` +```shell 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: -``` +```shell dx create hello --template gh:dioxuslabs/dioxus-template ``` diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index f7b6a3071..a5721d7a6 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -93,6 +93,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { cmd }; + let cmd = cmd.args(&config.cargo_args); + let cmd = match executable { ExecutableType::Binary(name) => cmd.arg("--bin").arg(name), ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), @@ -265,7 +267,6 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result Result Result cmd.arg("--bin").arg(name), crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), @@ -302,12 +313,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result { file_name = name.clone(); - config.target_dir.join(release_type).join(name) + config + .target_dir + .join(target_platform) + .join(release_type) + .join(name) } crate::ExecutableType::Example(name) => { file_name = name.clone(); config .target_dir + .join(target_platform) .join(release_type) .join("examples") .join(name) diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index 6c1ce04a1..812a4c325 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -27,15 +27,17 @@ pub struct Autoformat { impl Autoformat { // Todo: autoformat the entire crate pub async fn autoformat(self) -> Result<()> { + let Autoformat { check, raw, file } = self; + // Default to formatting the project - if self.raw.is_none() && self.file.is_none() { - if let Err(e) = autoformat_project(self.check).await { + if raw.is_none() && file.is_none() { + if let Err(e) = autoformat_project(check).await { eprintln!("error formatting project: {}", e); exit(1); } } - if let Some(raw) = self.raw { + if let Some(raw) = raw { let indent = indentation_for(".")?; if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) { println!("{}", inner); @@ -47,47 +49,90 @@ impl Autoformat { } // Format single file - if let Some(file) = self.file { - let file_content; - let indent; - if file == "-" { - indent = indentation_for(".")?; - let mut contents = String::new(); - std::io::stdin().read_to_string(&mut contents)?; - file_content = Ok(contents); - } else { - indent = indentation_for(".")?; - file_content = fs::read_to_string(&file); - }; - - match file_content { - Ok(s) => { - let edits = dioxus_autofmt::fmt_file(&s, indent); - let out = dioxus_autofmt::apply_formats(&s, edits); - if file == "-" { - print!("{}", out); - } else { - match fs::write(&file, out) { - Ok(_) => { - println!("formatted {}", file); - } - Err(e) => { - eprintln!("failed to write formatted content to file: {}", e); - } - } - } - } - Err(e) => { - eprintln!("failed to open file: {}", e); - exit(1); - } - } + if let Some(file) = file { + refactor_file(file)?; } Ok(()) } } +fn refactor_file(file: String) -> Result<(), Error> { + let indent = indentation_for(".")?; + let file_content = if file == "-" { + let mut contents = String::new(); + std::io::stdin().read_to_string(&mut contents)?; + Ok(contents) + } else { + fs::read_to_string(&file) + }; + let Ok(s) = file_content else { + eprintln!("failed to open file: {}", file_content.unwrap_err()); + exit(1); + }; + let edits = dioxus_autofmt::fmt_file(&s, indent); + let out = dioxus_autofmt::apply_formats(&s, edits); + + if file == "-" { + print!("{}", out); + } else if let Err(e) = fs::write(&file, out) { + eprintln!("failed to write formatted content to file: {e}",); + } else { + println!("formatted {}", file); + } + + Ok(()) +} + +fn get_project_files(config: &CrateConfig) -> Vec { + let mut files = vec![]; + + let gitignore_path = config.crate_dir.join(".gitignore"); + if gitignore_path.is_file() { + let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap(); + if let Ok(git_files) = gitigno.included_files() { + let git_files = git_files + .into_iter() + .filter(|f| f.ends_with(".rs") && !is_target_dir(f)); + files.extend(git_files) + }; + } else { + collect_rs_files(&config.crate_dir, &mut files); + } + + files +} + +fn is_target_dir(file: &Path) -> bool { + let stripped = if let Ok(cwd) = std::env::current_dir() { + file.strip_prefix(cwd).unwrap_or(file) + } else { + file + }; + if let Some(first) = stripped.components().next() { + first.as_os_str() == "target" + } else { + false + } +} + +async fn format_file( + path: impl AsRef, + indent: IndentOptions, +) -> Result { + let contents = tokio::fs::read_to_string(&path).await?; + + let edits = dioxus_autofmt::fmt_file(&contents, indent); + let len = edits.len(); + + if !edits.is_empty() { + let out = dioxus_autofmt::apply_formats(&contents, edits); + tokio::fs::write(path, out).await?; + } + + Ok(len) +} + /// Read every .rs file accessible when considering the .gitignore and try to format it /// /// Runs using Tokio for multithreading, so it should be really really fast @@ -96,8 +141,7 @@ impl Autoformat { async fn autoformat_project(check: bool) -> Result<()> { let crate_config = crate::CrateConfig::new(None)?; - let mut files_to_format = vec![]; - collect_rs_files(&crate_config.crate_dir, &mut files_to_format); + let files_to_format = get_project_files(&crate_config); if files_to_format.is_empty() { return Ok(()); @@ -107,38 +151,17 @@ async fn autoformat_project(check: bool) -> Result<()> { let counts = files_to_format .into_iter() - .filter(|file| { - if file.components().any(|f| f.as_os_str() == "target") { - return false; - } - - true - }) .map(|path| async { - let _path = path.clone(); - let _indent = indent.clone(); - let res = tokio::spawn(async move { - let contents = tokio::fs::read_to_string(&path).await?; - - let edits = dioxus_autofmt::fmt_file(&contents, _indent.clone()); - let len = edits.len(); - - if !edits.is_empty() { - let out = dioxus_autofmt::apply_formats(&contents, edits); - tokio::fs::write(&path, out).await?; - } - - Ok(len) as Result - }) - .await; + let path_clone = path.clone(); + let res = tokio::spawn(format_file(path, indent.clone())).await; match res { Err(err) => { - eprintln!("error formatting file: {}\n{err}", _path.display()); + eprintln!("error formatting file: {}\n{err}", path_clone.display()); None } Ok(Err(err)) => { - eprintln!("error formatting file: {}\n{err}", _path.display()); + eprintln!("error formatting file: {}\n{err}", path_clone.display()); None } Ok(Ok(res)) => Some(res), @@ -148,13 +171,7 @@ async fn autoformat_project(check: bool) -> Result<()> { .collect::>() .await; - let files_formatted: usize = counts - .into_iter() - .map(|f| match f { - Some(res) => res, - _ => 0, - }) - .sum(); + let files_formatted: usize = counts.into_iter().flatten().sum(); if files_formatted > 0 && check { eprintln!("{} files needed formatting", files_formatted); @@ -207,26 +224,24 @@ fn indentation_for(file_or_dir: impl AsRef) -> Result { )) } -fn collect_rs_files(folder: &Path, files: &mut Vec) { - let Ok(folder) = folder.read_dir() else { +fn collect_rs_files(folder: &impl AsRef, files: &mut Vec) { + if is_target_dir(folder.as_ref()) { + return; + } + let Ok(folder) = folder.as_ref().read_dir() else { return; }; - // load the gitignore - for entry in folder { let Ok(entry) = entry else { continue; }; - let path = entry.path(); - if path.is_dir() { collect_rs_files(&path, files); } - if let Some(ext) = path.extension() { - if ext == "rs" { + if ext == "rs" && !is_target_dir(&path) { files.push(path); } } diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 63a2969e0..0deeb38ca 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -37,6 +37,12 @@ impl Build { .platform .unwrap_or(crate_config.dioxus_config.application.default_platform); + if let Some(target) = self.build.target { + crate_config.set_target(target); + } + + crate_config.set_cargo_args(self.build.cargo_args); + // #[cfg(feature = "plugin")] // let _ = PluginManager::on_build_start(&crate_config, &platform); diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index 80b52cf2f..fd97930ed 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -76,6 +76,12 @@ impl Bundle { crate_config.set_profile(self.build.profile.unwrap()); } + if let Some(target) = &self.build.target { + crate_config.set_target(target.to_string()); + } + + crate_config.set_cargo_args(self.build.cargo_args); + // build the desktop app build_desktop(&crate_config, false)?; @@ -148,6 +154,11 @@ impl Bundle { .collect(), ); } + + if let Some(target) = &self.build.target { + settings = settings.target(target.to_string()); + } + let settings = settings.build(); // on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567) @@ -156,9 +167,9 @@ impl Bundle { tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{ #[cfg(target_os = "macos")] - panic!("Failed to bundle project: {}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err); + panic!("Failed to bundle project: {:#?}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err); #[cfg(not(target_os = "macos"))] - panic!("Failed to bundle project: {}", err); + panic!("Failed to bundle project: {:#?}", err); }); Ok(()) diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs index 2a441a1c8..0d2a5e212 100644 --- a/packages/cli/src/cli/cfg.rs +++ b/packages/cli/src/cli/cfg.rs @@ -6,10 +6,6 @@ use super::*; /// Config options for the build system. #[derive(Clone, Debug, Default, Deserialize, Parser)] pub struct ConfigOptsBuild { - /// The index HTML file to drive the bundling process [default: index.html] - #[arg(long)] - pub target: Option, - /// Build in release mode [default: false] #[clap(long)] #[serde(default)] @@ -35,14 +31,18 @@ pub struct ConfigOptsBuild { /// Space separated list of features to activate #[clap(long)] pub features: Option>, + + /// Rustc platform triple + #[clap(long)] + pub target: Option, + + /// Extra arguments passed to cargo build + #[clap(last = true)] + pub cargo_args: Vec, } #[derive(Clone, Debug, Default, Deserialize, Parser)] pub struct ConfigOptsServe { - /// The index HTML file to drive the bundling process [default: index.html] - #[arg(short, long)] - pub target: Option, - /// Port of dev server #[clap(long)] #[clap(default_value_t = 8080)] @@ -89,6 +89,14 @@ pub struct ConfigOptsServe { /// Space separated list of features to activate #[clap(long)] pub features: Option>, + + /// Rustc platform triple + #[clap(long)] + pub target: Option, + + /// Extra arguments passed to cargo build + #[clap(last = true)] + pub cargo_args: Vec, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] @@ -129,4 +137,12 @@ pub struct ConfigOptsBundle { /// Space separated list of features to activate #[clap(long)] pub features: Option>, + + /// Rustc platform triple + #[clap(long)] + pub target: Option, + + /// Extra arguments passed to cargo build + #[clap(last = true)] + pub cargo_args: Vec, } diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index 433d611b7..e08b02339 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -34,6 +34,12 @@ impl Serve { // Subdirectories don't work with the server crate_config.dioxus_config.web.app.base_path = None; + if let Some(target) = self.serve.target { + crate_config.set_target(target); + } + + crate_config.set_cargo_args(self.serve.cargo_args); + let platform = self .serve .platform diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 3abf47f0c..727504e6a 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -211,6 +211,8 @@ pub struct CrateConfig { pub verbose: bool, pub custom_profile: Option, pub features: Option>, + pub target: Option, + pub cargo_args: Vec, } #[derive(Debug, Clone)] @@ -278,6 +280,8 @@ impl CrateConfig { let verbose = false; let custom_profile = None; let features = None; + let target = None; + let cargo_args = vec![]; Ok(Self { out_dir, @@ -294,6 +298,8 @@ impl CrateConfig { custom_profile, features, verbose, + target, + cargo_args, }) } @@ -331,6 +337,16 @@ impl CrateConfig { self.features = Some(features); self } + + pub fn set_target(&mut self, target: String) -> &mut Self { + self.target = Some(target); + self + } + + pub fn set_cargo_args(&mut self, cargo_args: Vec) -> &mut Self { + self.cargo_args = cargo_args; + self + } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 7ca1b2c9a..2358551f2 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -42,34 +42,36 @@ async fn main() -> anyhow::Result<()> { set_up_logging(); - let bin = get_bin(args.bin)?; + let bin = get_bin(args.bin); - let _dioxus_config = DioxusConfig::load(Some(bin.clone())) + if let Ok(bin) = &bin { + let _dioxus_config = DioxusConfig::load(Some(bin.clone())) .map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))? .unwrap_or_else(|| { log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config"); DioxusConfig::default() }); - #[cfg(feature = "plugin")] - PluginManager::init(_dioxus_config.plugin) - .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?; + #[cfg(feature = "plugin")] + PluginManager::init(_dioxus_config.plugin) + .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?; + } match args.action { Translate(opts) => opts .translate() .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)), - Build(opts) => opts - .build(Some(bin.clone())) + Build(opts) if bin.is_ok() => opts + .build(Some(bin.unwrap().clone())) .map_err(|e| anyhow!("🚫 Building project failed: {}", e)), - Clean(opts) => opts - .clean(Some(bin.clone())) + Clean(opts) if bin.is_ok() => opts + .clean(Some(bin.unwrap().clone())) .map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)), - Serve(opts) => opts - .serve(Some(bin.clone())) + Serve(opts) if bin.is_ok() => opts + .serve(Some(bin.unwrap().clone())) .await .map_err(|e| anyhow!("🚫 Serving project failed: {}", e)), @@ -81,8 +83,8 @@ async fn main() -> anyhow::Result<()> { .config() .map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)), - Bundle(opts) => opts - .bundle(Some(bin.clone())) + Bundle(opts) if bin.is_ok() => opts + .bundle(Some(bin.unwrap().clone())) .map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)), #[cfg(feature = "plugin")] @@ -107,5 +109,6 @@ async fn main() -> anyhow::Result<()> { Ok(()) } + _ => Err(anyhow::anyhow!(bin.unwrap_err())), } } diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index fec1e5a99..ec21fa4c0 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -154,7 +154,11 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<() println!("Connected to hot reloading πŸš€"); } Err(err) => { - if err.kind() != std::io::ErrorKind::WouldBlock { + let error_string = err.to_string(); + // Filter out any error messages about a operation that may block and an error message that triggers on some operating systems that says "Waiting for a process to open the other end of the pipe" without WouldBlock being set + let display_error = err.kind() != std::io::ErrorKind::WouldBlock + && !error_string.contains("Waiting for a process"); + if display_error { println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err); } } diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 0e1413597..98ff1e078 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -11,6 +11,7 @@ use axum::{ body::{Full, HttpBody}, extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, http::{ + self, header::{HeaderName, HeaderValue}, Method, Response, StatusCode, }, @@ -262,7 +263,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 + let mut response = if file_service_config .dioxus_config .web .watcher @@ -290,6 +291,13 @@ async fn setup_router( } else { response.map(|body| body.boxed()) }; + let headers = response.headers_mut(); + headers.insert( + http::header::CACHE_CONTROL, + HeaderValue::from_static("no-cache"), + ); + headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache")); + headers.insert(http::header::EXPIRES, HeaderValue::from_static("0")); Ok(response) }, ) diff --git a/packages/core-macro/src/component_body_deserializers/component.rs b/packages/core-macro/src/component_body_deserializers/component.rs index 36cda850c..994e51709 100644 --- a/packages/core-macro/src/component_body_deserializers/component.rs +++ b/packages/core-macro/src/component_body_deserializers/component.rs @@ -58,8 +58,11 @@ impl ToTokens for ComponentDeserializerOutput { fn to_tokens(&self, tokens: &mut TokenStream2) { let comp_fn = &self.comp_fn; let props_struct = &self.props_struct; + let fn_ident = &comp_fn.sig.ident; + let doc = format!("Properties for the [`{fn_ident}`] component."); tokens.append_all(quote! { + #[doc = #doc] #props_struct #[allow(non_snake_case)] #comp_fn diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 785d4b174..ffdd4656b 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -13,62 +13,50 @@ use crate::{ #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct ElementId(pub usize); -pub(crate) struct ElementRef { +/// An Element that can be bubbled to's unique identifier. +/// +/// `BubbleId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is +/// unmounted, then the `BubbleId` will be reused for a new component. +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct VNodeId(pub usize); + +#[derive(Debug, Clone, Copy)] +pub struct ElementRef { // the pathway of the real element inside the template - pub path: ElementPath, + pub(crate) path: ElementPath, // The actual template - pub template: Option>>, + pub(crate) template: VNodeId, // The scope the element belongs to - pub scope: ScopeId, + pub(crate) scope: ScopeId, } #[derive(Clone, Copy, Debug)] -pub enum ElementPath { - Deep(&'static [u8]), - Root(usize), -} - -impl ElementRef { - pub(crate) fn none() -> Self { - Self { - template: None, - path: ElementPath::Root(0), - scope: ScopeId::ROOT, - } - } +pub struct ElementPath { + pub(crate) path: &'static [u8], } impl VirtualDom { - pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { - self.next_reference(template, ElementPath::Deep(path)) + pub(crate) fn next_element(&mut self) -> ElementId { + ElementId(self.elements.insert(None)) } - pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { - self.next_reference(template, ElementPath::Root(path)) - } + pub(crate) fn next_vnode_ref(&mut self, vnode: &VNode) -> VNodeId { + let new_id = VNodeId(self.element_refs.insert(Some(unsafe { + std::mem::transmute::, _>(vnode.into()) + }))); - pub(crate) fn next_null(&mut self) -> ElementId { - let entry = self.elements.vacant_entry(); - let id = entry.key(); + // Set this id to be dropped when the scope is rerun + if let Some(scope) = self.runtime.current_scope_id() { + self.scopes[scope.0] + .element_refs_to_drop + .borrow_mut() + .push(new_id); + } - entry.insert(ElementRef::none()); - ElementId(id) - } - - fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId { - let entry = self.elements.vacant_entry(); - let id = entry.key(); - let scope = self.runtime.current_scope_id().unwrap_or(ScopeId::ROOT); - - entry.insert(ElementRef { - // We know this is non-null because it comes from a reference - template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }), - path, - scope, - }); - ElementId(id) + new_id } pub(crate) fn reclaim(&mut self, el: ElementId) { @@ -76,7 +64,7 @@ impl VirtualDom { .unwrap_or_else(|| panic!("cannot reclaim {:?}", el)); } - pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option { + pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> { if el.0 == 0 { panic!( "Cannot reclaim the root element - {:#?}", @@ -84,12 +72,12 @@ impl VirtualDom { ); } - self.elements.try_remove(el.0) + self.elements.try_remove(el.0).map(|_| ()) } - pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) { - let node: *const VNode = node as *const _; - self.elements[el.0].template = unsafe { std::mem::transmute(node) }; + pub(crate) fn set_template(&mut self, id: VNodeId, vnode: &VNode) { + self.element_refs[id.0] = + Some(unsafe { std::mem::transmute::, _>(vnode.into()) }); } // Drop a scope and all its children @@ -101,6 +89,15 @@ impl VirtualDom { id, }); + // Remove all VNode ids from the scope + for id in self.scopes[id.0] + .element_refs_to_drop + .borrow_mut() + .drain(..) + { + self.element_refs.try_remove(id.0); + } + self.ensure_drop_safety(id); if recursive { @@ -145,14 +142,25 @@ impl VirtualDom { } /// Descend through the tree, removing any borrowed props and listeners - pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { + pub(crate) fn ensure_drop_safety(&mut self, scope_id: ScopeId) { let scope = &self.scopes[scope_id.0]; + { + // Drop all element refs that could be invalidated when the component was rerun + let mut element_refs = self.scopes[scope_id.0].element_refs_to_drop.borrow_mut(); + let element_refs_slab = &mut self.element_refs; + for element_ref in element_refs.drain(..) { + if let Some(element_ref) = element_refs_slab.get_mut(element_ref.0) { + *element_ref = None; + } + } + } + // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we // run the hooks (which hold an &mut Reference) // recursively call ensure_drop_safety on all children - let mut props = scope.borrowed_props.borrow_mut(); - props.drain(..).for_each(|comp| { + let props = { scope.borrowed_props.borrow_mut().clone() }; + for comp in props { let comp = unsafe { &*comp }; match comp.scope.get() { Some(child) if child != scope_id => self.ensure_drop_safety(child), @@ -161,7 +169,9 @@ impl VirtualDom { if let Ok(mut props) = comp.props.try_borrow_mut() { *props = None; } - }); + } + let scope = &self.scopes[scope_id.0]; + scope.borrowed_props.borrow_mut().clear(); // Now that all the references are gone, we can safely drop our own references in our listeners. let mut listeners = scope.attributes_to_drop_before_render.borrow_mut(); @@ -176,18 +186,12 @@ impl VirtualDom { impl ElementPath { pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool { - match *self { - ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()], - ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8, - } + small.len() <= self.path.len() && *small == &self.path[..small.len()] } } impl PartialEq<&[u8]> for ElementPath { fn eq(&self, other: &&[u8]) -> bool { - match *self { - ElementPath::Deep(deep) => deep.eq(*other), - ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8, - } + self.path.eq(*other) } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index a61a8cace..f3b89ae9e 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::{ + BorrowedAttributeValue, ElementPath, ElementRef, VComponent, VPlaceholder, VText, +}; use crate::mutations::Mutation; use crate::mutations::Mutation::*; use crate::nodes::VNode; @@ -94,6 +96,9 @@ impl<'b> VirtualDom { nodes_mut.resize(len, ElementId::default()); }; + // Set this node id + node.stable_id.set(Some(self.next_vnode_ref(node))); + // The best renderers will have templates prehydrated and registered // Just in case, let's create the template using instructions anyways self.register_template(node.template.get()); @@ -181,15 +186,30 @@ impl<'b> VirtualDom { use DynamicNode::*; match &template.dynamic_nodes[idx] { node @ Component { .. } | node @ Fragment(_) => { - self.create_dynamic_node(template, node, idx) + let template_ref = ElementRef { + path: ElementPath { + path: template.template.get().node_paths[idx], + }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + self.create_dynamic_node(template_ref, node) } - Placeholder(VPlaceholder { id }) => { - let id = self.set_slot(template, id, idx); + Placeholder(VPlaceholder { id, parent }) => { + let template_ref = ElementRef { + path: ElementPath { + path: template.template.get().node_paths[idx], + }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + parent.set(Some(template_ref)); + let id = self.set_slot(id); self.mutations.push(CreatePlaceholder { id }); 1 } Text(VText { id, value }) => { - let id = self.set_slot(template, id, idx); + let id = self.set_slot(id); self.create_static_text(value, id); 1 } @@ -205,7 +225,7 @@ impl<'b> VirtualDom { }); } - /// We write all the descndent data for this element + /// We write all the descendent data for this element /// /// Elements can contain other nodes - and those nodes can be dynamic or static /// @@ -265,7 +285,14 @@ impl<'b> VirtualDom { .map(|sorted_index| dynamic_nodes[sorted_index].0); for idx in reversed_iter { - let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx); + let boundary_ref = ElementRef { + path: ElementPath { + path: template.template.get().node_paths[idx], + }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + let m = self.create_dynamic_node(boundary_ref, &template.dynamic_nodes[idx]); if m > 0 { // The path is one shorter because the top node is the root let path = &template.template.get().node_paths[idx][1..]; @@ -279,15 +306,15 @@ impl<'b> VirtualDom { attrs: &mut Peekable>, root_idx: u8, root: ElementId, - node: &VNode, + node: &'b VNode<'b>, ) { while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx)) { - let id = self.assign_static_node_as_dynamic(path, root, node, attr_id); + let id = self.assign_static_node_as_dynamic(path, root); loop { - self.write_attribute(&node.dynamic_attrs[attr_id], id); + self.write_attribute(node, attr_id, &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) { @@ -298,7 +325,13 @@ impl<'b> VirtualDom { } } - fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) { + fn write_attribute( + &mut self, + template: &'b VNode<'b>, + idx: usize, + attribute: &'b crate::Attribute<'b>, + id: ElementId, + ) { // Make sure we set the attribute's associated id attribute.mounted_element.set(id); @@ -307,6 +340,13 @@ impl<'b> VirtualDom { match &attribute.value { AttributeValue::Listener(_) => { + let path = &template.template.get().attr_paths[idx]; + let element_ref = ElementRef { + path: ElementPath { path }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + self.elements[id.0] = Some(element_ref); self.mutations.push(NewEventListener { // all listeners start with "on" name: &unbounded_name[2..], @@ -330,7 +370,7 @@ impl<'b> VirtualDom { fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId { // Get an ID for this root since it's a real root - let this_id = self.next_root(template, root_idx); + let this_id = self.next_element(); template.root_ids.borrow_mut()[root_idx] = this_id; self.mutations.push(LoadTemplate { @@ -353,8 +393,6 @@ impl<'b> VirtualDom { &mut self, path: &'static [u8], this_id: ElementId, - template: &VNode, - attr_id: usize, ) -> ElementId { if path.len() == 1 { return this_id; @@ -362,7 +400,7 @@ impl<'b> VirtualDom { // if attribute is on a root node, then we've already created the element // Else, it's deep in the template and we should create a new id for it - let id = self.next_element(template, template.template.get().attr_paths[attr_id]); + let id = self.next_element(); self.mutations.push(Mutation::AssignId { path: &path[1..], @@ -405,6 +443,7 @@ impl<'b> VirtualDom { #[allow(unused_mut)] pub(crate) fn register_template(&mut self, mut template: Template<'static>) { let (path, byte_index) = template.name.rsplit_once(':').unwrap(); + let byte_index = byte_index.parse::().unwrap(); // First, check if we've already seen this template if self @@ -439,27 +478,21 @@ impl<'b> VirtualDom { pub(crate) fn create_dynamic_node( &mut self, - template: &'b VNode<'b>, + parent: ElementRef, node: &'b DynamicNode<'b>, - idx: usize, ) -> usize { use DynamicNode::*; match node { - Text(text) => self.create_dynamic_text(template, text, idx), - Placeholder(place) => self.create_placeholder(place, template, idx), - Component(component) => self.create_component_node(template, component), - Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(), + Text(text) => self.create_dynamic_text(parent, text), + Placeholder(place) => self.create_placeholder(place, parent), + Component(component) => self.create_component_node(Some(parent), component), + Fragment(frag) => self.create_children(*frag, Some(parent)), } } - fn create_dynamic_text( - &mut self, - template: &'b VNode<'b>, - text: &'b VText<'b>, - idx: usize, - ) -> usize { + fn create_dynamic_text(&mut self, parent: ElementRef, text: &'b VText<'b>) -> usize { // Allocate a dynamic element reference for this text node - let new_id = self.next_element(template, template.template.get().node_paths[idx]); + let new_id = self.next_element(); // Make sure the text node is assigned to the correct element text.id.set(Some(new_id)); @@ -470,7 +503,7 @@ impl<'b> VirtualDom { // Add the mutation to the list self.mutations.push(HydrateText { id: new_id, - path: &template.template.get().node_paths[idx][1..], + path: &parent.path.path[1..], value, }); @@ -481,18 +514,20 @@ impl<'b> VirtualDom { pub(crate) fn create_placeholder( &mut self, placeholder: &VPlaceholder, - template: &'b VNode<'b>, - idx: usize, + parent: ElementRef, ) -> usize { // Allocate a dynamic element reference for this text node - let id = self.next_element(template, template.template.get().node_paths[idx]); + let id = self.next_element(); // Make sure the text node is assigned to the correct element placeholder.id.set(Some(id)); + // Assign the placeholder's parent + placeholder.parent.set(Some(parent)); + // Assign the ID to the existing node in the template self.mutations.push(AssignId { - path: &template.template.get().node_paths[idx][1..], + path: &parent.path.path[1..], id, }); @@ -502,7 +537,7 @@ impl<'b> VirtualDom { pub(super) fn create_component_node( &mut self, - template: &'b VNode<'b>, + parent: Option, component: &'b VComponent<'b>, ) -> usize { use RenderReturn::*; @@ -514,8 +549,11 @@ impl<'b> VirtualDom { match unsafe { self.run_scope(scope).extend_lifetime_ref() } { // Create the component's root element - Ready(t) => self.create_scope(scope, t), - Aborted(t) => self.mount_aborted(template, t), + Ready(t) => { + self.assign_boundary_ref(parent, t); + self.create_scope(scope, t) + } + Aborted(t) => self.mount_aborted(t, parent), } } @@ -531,20 +569,17 @@ impl<'b> VirtualDom { .unwrap_or_else(|| component.scope.get().unwrap()) } - fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize { - let id = self.next_element(parent, &[]); + fn mount_aborted(&mut self, placeholder: &VPlaceholder, parent: Option) -> usize { + let id = self.next_element(); self.mutations.push(Mutation::CreatePlaceholder { id }); placeholder.id.set(Some(id)); + placeholder.parent.set(parent); + 1 } - fn set_slot( - &mut self, - template: &'b VNode<'b>, - slot: &'b Cell>, - id: usize, - ) -> ElementId { - let id = self.next_element(template, template.template.get().node_paths[id]); + fn set_slot(&mut self, slot: &'b Cell>) -> ElementId { + let id = self.next_element(); slot.set(Some(id)); id } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 2de862218..58bd934b9 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,7 +1,10 @@ use crate::{ any_props::AnyProps, arena::ElementId, - innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText}, + innerlude::{ + BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent, VPlaceholder, + VText, + }, mutations::Mutation, nodes::RenderReturn, nodes::{DynamicNode, VNode}, @@ -39,19 +42,27 @@ impl<'b> VirtualDom { (Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p), // Just move over the placeholder - (Aborted(l), Aborted(r)) => r.id.set(l.id.get()), + (Aborted(l), Aborted(r)) => { + r.id.set(l.id.get()); + r.parent.set(l.parent.get()) + } // Placeholder becomes something // We should also clear the error now - (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), + (Aborted(l), Ready(r)) => self.replace_placeholder( + l, + [r], + l.parent.get().expect("root node should not be none"), + ), }; } self.runtime.scope_stack.borrow_mut().pop(); } fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { - let id = self.next_null(); + let id = self.next_element(); p.id.set(Some(id)); + p.parent.set(l.parent.get()); self.mutations.push(Mutation::CreatePlaceholder { id }); let pre_edits = self.mutations.edits.len(); @@ -81,12 +92,24 @@ impl<'b> VirtualDom { if let Some(&template) = map.get(&byte_index) { right_template.template.set(template); if template != left_template.template.get() { - return self.replace(left_template, [right_template]); + let parent = left_template.parent.take(); + return self.replace(left_template, [right_template], parent); } } } } + // Copy over the parent + { + right_template.parent.set(left_template.parent.get()); + } + + // Update the bubble id pointer + right_template.stable_id.set(left_template.stable_id.get()); + if let Some(bubble_id) = right_template.stable_id.get() { + self.set_template(bubble_id, right_template); + } + // If the templates are the same, we don't need to do anything, nor do we want to if templates_are_the_same(left_template, right_template) { return; @@ -105,12 +128,8 @@ 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()); - - // We want to make sure anything that gets pulled is valid - self.update_template(left_attr.mounted_element.get(), right_template); + let mounted_element = left_attr.mounted_element.get(); + right_attr.mounted_element.set(mounted_element); // If the attributes are different (or volatile), we need to update them if left_attr.value != right_attr.value || left_attr.volatile { @@ -123,8 +142,16 @@ impl<'b> VirtualDom { .dynamic_nodes .iter() .zip(right_template.dynamic_nodes.iter()) - .for_each(|(left_node, right_node)| { - self.diff_dynamic_node(left_node, right_node, right_template); + .enumerate() + .for_each(|(dyn_node_idx, (left_node, right_node))| { + let current_ref = ElementRef { + template: right_template.stable_id().unwrap(), + path: ElementPath { + path: left_template.template.get().node_paths[dyn_node_idx], + }, + scope: self.runtime.scope_stack.borrow().last().copied().unwrap(), + }; + self.diff_dynamic_node(left_node, right_node, current_ref); }); // Make sure the roots get transferred over while we're here @@ -135,30 +162,24 @@ impl<'b> VirtualDom { right.push(element); } } - - let root_ids = right_template.root_ids.borrow(); - - // Update the node refs - for i in 0..root_ids.len() { - if let Some(root_id) = root_ids.get(i) { - self.update_template(*root_id, right_template); - } - } } fn diff_dynamic_node( &mut self, left_node: &'b DynamicNode<'b>, right_node: &'b DynamicNode<'b>, - node: &'b VNode<'b>, + parent: ElementRef, ) { match (left_node, right_node) { - (Text(left), Text(right)) => self.diff_vtext(left, right, node), - (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), - (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), - (Component(left), Component(right)) => self.diff_vcomponent(left, right, node), - (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right), - (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), + (Text(left), Text(right)) => self.diff_vtext(left, right), + (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right, parent), + (Placeholder(left), Placeholder(right)) => { + right.id.set(left.id.get()); + right.parent.set(left.parent.get()); + }, + (Component(left), Component(right)) => self.diff_vcomponent(left, right, Some(parent)), + (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right, parent), + (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right, parent), _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), }; } @@ -179,7 +200,7 @@ impl<'b> VirtualDom { &mut self, left: &'b VComponent<'b>, right: &'b VComponent<'b>, - right_template: &'b VNode<'b>, + parent: Option, ) { if std::ptr::eq(left, right) { return; @@ -187,7 +208,7 @@ impl<'b> VirtualDom { // Replace components that have different render fns if left.render_fn != right.render_fn { - return self.replace_vcomponent(right_template, right, left); + return self.replace_vcomponent(right, left, parent); } // Make sure the new vcomponent has the right scopeid associated to it @@ -228,11 +249,11 @@ impl<'b> VirtualDom { fn replace_vcomponent( &mut self, - right_template: &'b VNode<'b>, right: &'b VComponent<'b>, left: &'b VComponent<'b>, + parent: Option, ) { - let m = self.create_component_node(right_template, right); + let m = self.create_component_node(parent, right); let pre_edits = self.mutations.edits.len(); @@ -287,11 +308,12 @@ impl<'b> VirtualDom { /// } /// ``` fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { + let parent = left.parent.take(); match matching_components(left, right) { - None => self.replace(left, [right]), + None => self.replace(left, [right], parent), Some(components) => components .into_iter() - .for_each(|(l, r)| self.diff_vcomponent(l, r, right)), + .for_each(|(l, r)| self.diff_vcomponent(l, r, parent)), } } @@ -299,11 +321,8 @@ impl<'b> VirtualDom { /// /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's /// different. - fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) { - let id = left - .id - .get() - .unwrap_or_else(|| self.next_element(node, &[0])); + fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) { + let id = left.id.get().unwrap_or_else(|| self.next_element()); right.id.set(Some(id)); if left.value != right.value { @@ -312,7 +331,12 @@ impl<'b> VirtualDom { } } - fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_non_empty_fragment( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { let new_is_keyed = new[0].key.is_some(); let old_is_keyed = old[0].key.is_some(); debug_assert!( @@ -325,9 +349,9 @@ impl<'b> VirtualDom { ); if new_is_keyed && old_is_keyed { - self.diff_keyed_children(old, new); + self.diff_keyed_children(old, new, parent); } else { - self.diff_non_keyed_children(old, new); + self.diff_non_keyed_children(old, new, parent); } } @@ -339,7 +363,12 @@ impl<'b> VirtualDom { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_non_keyed_children( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { use std::cmp::Ordering; // Handled these cases in `diff_children` before calling this function. @@ -348,7 +377,9 @@ impl<'b> VirtualDom { match old.len().cmp(&new.len()) { Ordering::Greater => self.remove_nodes(&old[new.len()..]), - Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), + Ordering::Less => { + self.create_and_insert_after(&new[old.len()..], old.last().unwrap(), parent) + } Ordering::Equal => {} } @@ -373,7 +404,12 @@ impl<'b> VirtualDom { // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 // // The stack is empty upon entry. - fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_keyed_children( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { if cfg!(debug_assertions) { let mut keys = rustc_hash::FxHashSet::default(); let mut assert_unique_keys = |children: &'b [VNode<'b>]| { @@ -401,7 +437,7 @@ impl<'b> VirtualDom { // // `shared_prefix_count` is the count of how many nodes at the start of // `new` and `old` share the same keys. - let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { + let (left_offset, right_offset) = match self.diff_keyed_ends(old, new, parent) { Some(count) => count, None => return, }; @@ -427,18 +463,18 @@ impl<'b> VirtualDom { if left_offset == 0 { // insert at the beginning of the old list let foothold = &old[old.len() - right_offset]; - self.create_and_insert_before(new_middle, foothold); + self.create_and_insert_before(new_middle, foothold, parent); } else if right_offset == 0 { // insert at the end the old list let foothold = old.last().unwrap(); - self.create_and_insert_after(new_middle, foothold); + self.create_and_insert_after(new_middle, foothold, parent); } else { // inserting in the middle let foothold = &old[left_offset - 1]; - self.create_and_insert_after(new_middle, foothold); + self.create_and_insert_after(new_middle, foothold, parent); } } else { - self.diff_keyed_middle(old_middle, new_middle); + self.diff_keyed_middle(old_middle, new_middle, parent); } } @@ -451,6 +487,7 @@ impl<'b> VirtualDom { &mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>], + parent: ElementRef, ) -> Option<(usize, usize)> { let mut left_offset = 0; @@ -466,7 +503,7 @@ impl<'b> VirtualDom { // If that was all of the old children, then create and append the remaining // new children and we're finished. if left_offset == old.len() { - self.create_and_insert_after(&new[left_offset..], old.last().unwrap()); + self.create_and_insert_after(&new[left_offset..], old.last().unwrap(), parent); return None; } @@ -505,7 +542,12 @@ impl<'b> VirtualDom { // // Upon exit from this function, it will be restored to that same self. #[allow(clippy::too_many_lines)] - fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_keyed_middle( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { /* 1. Map the old keys into a numerical ordering based on indices. 2. Create a map of old key to its index @@ -562,7 +604,7 @@ impl<'b> VirtualDom { if shared_keys.is_empty() { if old.first().is_some() { self.remove_nodes(&old[1..]); - self.replace(&old[0], new); + self.replace(&old[0], new, Some(parent)); } else { // I think this is wrong - why are we appending? // only valid of the if there are no trailing elements @@ -739,20 +781,38 @@ impl<'b> VirtualDom { .sum() } - fn create_children(&mut self, nodes: impl IntoIterator>) -> usize { + pub(crate) fn create_children( + &mut self, + nodes: impl IntoIterator>, + parent: Option, + ) -> usize { nodes .into_iter() - .fold(0, |acc, child| acc + self.create(child)) + .map(|child| { + self.assign_boundary_ref(parent, child); + self.create(child) + }) + .sum() } - fn create_and_insert_before(&mut self, new: &'b [VNode<'b>], before: &'b VNode<'b>) { - let m = self.create_children(new); + fn create_and_insert_before( + &mut self, + new: &'b [VNode<'b>], + before: &'b VNode<'b>, + parent: ElementRef, + ) { + let m = self.create_children(new, Some(parent)); let id = self.find_first_element(before); self.mutations.push(Mutation::InsertBefore { id, m }) } - fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) { - let m = self.create_children(new); + fn create_and_insert_after( + &mut self, + new: &'b [VNode<'b>], + after: &'b VNode<'b>, + parent: ElementRef, + ) { + let m = self.create_children(new, Some(parent)); let id = self.find_last_element(after); self.mutations.push(Mutation::InsertAfter { id, m }) } @@ -762,15 +822,21 @@ impl<'b> VirtualDom { &mut self, l: &'b VPlaceholder, r: impl IntoIterator>, + parent: ElementRef, ) { - let m = self.create_children(r); + let m = self.create_children(r, Some(parent)); let id = l.id.get().unwrap(); self.mutations.push(Mutation::ReplaceWith { id, m }); self.reclaim(id); } - fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator>) { - let m = self.create_children(right); + fn replace( + &mut self, + left: &'b VNode<'b>, + right: impl IntoIterator>, + parent: Option, + ) { + let m = self.create_children(right, parent); let pre_edits = self.mutations.edits.len(); @@ -789,11 +855,12 @@ impl<'b> VirtualDom { }; } - fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) { + fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder, parent: ElementRef) { // Create the placeholder first, ensuring we get a dedicated ID for the placeholder - let placeholder = self.next_element(&l[0], &[]); + let placeholder = self.next_element(); r.id.set(Some(placeholder)); + r.parent.set(Some(parent)); self.mutations .push(Mutation::CreatePlaceholder { id: placeholder }); @@ -831,6 +898,16 @@ impl<'b> VirtualDom { // Clean up the roots, assuming we need to generate mutations for these // This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim) self.reclaim_roots(node, gen_muts); + + // Clean up the vnode id + self.reclaim_vnode_id(node); + } + + fn reclaim_vnode_id(&mut self, node: &'b VNode<'b>) { + // Clean up the vnode id + if let Some(id) = node.stable_id() { + self.element_refs.remove(id.0); + } } fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) { @@ -989,6 +1066,13 @@ impl<'b> VirtualDom { } } } + + pub(crate) fn assign_boundary_ref(&mut self, parent: Option, child: &'b VNode<'b>) { + if let Some(parent) = parent { + // assign the parent of the child + child.parent.set(Some(parent)); + } + } } /// Are the templates the same? diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index a9e919bfa..dbc8e8131 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -30,7 +30,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { let children = cx.props.0.as_ref()?; Some(VNode { key: children.key, - parent: children.parent, + parent: children.parent.clone(), + stable_id: children.stable_id.clone(), template: children.template.clone(), root_ids: children.root_ids.clone(), dynamic_nodes: children.dynamic_nodes, diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 3189086b4..ae1bd2109 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -18,7 +18,7 @@ use crate::{innerlude::VNode, ScopeState}; /// A concrete type provider for closures that build [`VNode`] structures. /// -/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over +/// This struct wraps lazy structs that build [`VNode`] trees. Normally, we cannot perform a blanket implementation over /// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching. /// /// diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 78868d29f..9f3ce04a2 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,3 +1,4 @@ +use crate::innerlude::{ElementRef, VNodeId}; use crate::{ any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState, }; @@ -47,7 +48,10 @@ pub struct VNode<'a> { pub key: Option<&'a str>, /// When rendered, this template will be linked to its parent manually - pub parent: Option, + pub(crate) parent: Cell>, + + /// The bubble id assigned to the child that we need to update and drop when diffing happens + pub(crate) stable_id: Cell>, /// The static nodes and static descriptor of the template pub template: Cell>, @@ -68,7 +72,8 @@ impl<'a> VNode<'a> { pub fn empty(cx: &'a ScopeState) -> Element<'a> { Some(VNode { key: None, - parent: None, + parent: Default::default(), + stable_id: Default::default(), root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())), dynamic_nodes: &[], dynamic_attrs: &[], @@ -81,6 +86,30 @@ impl<'a> VNode<'a> { }) } + /// Create a new VNode + pub fn new( + key: Option<&'a str>, + template: Template<'static>, + root_ids: bumpalo::collections::Vec<'a, ElementId>, + dynamic_nodes: &'a [DynamicNode<'a>], + dynamic_attrs: &'a [Attribute<'a>], + ) -> Self { + Self { + key, + parent: Cell::new(None), + stable_id: Cell::new(None), + template: Cell::new(template), + root_ids: RefCell::new(root_ids), + dynamic_nodes, + dynamic_attrs, + } + } + + /// Get the stable id of this node used for bubbling events + pub(crate) fn stable_id(&self) -> Option { + self.stable_id.get() + } + /// Load a dynamic root at the given index /// /// Returns [`None`] if the root is actually a static node (Element/Text) @@ -319,7 +348,7 @@ pub struct VComponent<'a> { /// The function pointer of the component, known at compile time /// - /// It is possible that components get folded at comppile time, so these shouldn't be really used as a key + /// It is possible that components get folded at compile time, so these shouldn't be really used as a key pub(crate) render_fn: *const (), pub(crate) props: RefCell + 'a>>>, @@ -372,6 +401,8 @@ impl<'a> VText<'a> { pub struct VPlaceholder { /// The ID of this node in the real DOM pub(crate) id: Cell>, + /// The parent of this node + pub(crate) parent: Cell>, } impl VPlaceholder { @@ -722,7 +753,8 @@ impl<'b> IntoDynNode<'b> for Arguments<'_> { impl<'a> IntoDynNode<'a> for &'a VNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(_cx.bump().alloc([VNode { - parent: self.parent, + parent: self.parent.clone(), + stable_id: self.stable_id.clone(), template: self.template.clone(), root_ids: self.root_ids.clone(), key: self.key, diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index fc668b364..d2e9135e6 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -19,7 +19,7 @@ pub struct TaskId(pub usize); /// the task itself is the waker pub(crate) struct LocalTask { pub scope: ScopeId, - pub(super) task: RefCell + 'static>>>, + pub task: RefCell + 'static>>>, pub waker: Waker, } @@ -48,11 +48,15 @@ impl Scheduler { })), }; - entry.insert(task); + let mut cx = std::task::Context::from_waker(&task.waker); - self.sender - .unbounded_send(SchedulerMsg::TaskNotified(task_id)) - .expect("Scheduler should exist"); + if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { + self.sender + .unbounded_send(SchedulerMsg::TaskNotified(task_id)) + .expect("Scheduler should exist"); + } + + entry.insert(task); task_id } @@ -60,8 +64,8 @@ impl Scheduler { /// Drop the future with the given TaskId /// /// This does not abort the task, so you'll want to wrap it in an aborthandle if that's important to you - pub fn remove(&self, id: TaskId) { - self.tasks.borrow_mut().try_remove(id.0); + pub fn remove(&self, id: TaskId) -> Option { + self.tasks.borrow_mut().try_remove(id.0) } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 397f5328e..962a55e86 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -36,6 +36,7 @@ impl VirtualDom { borrowed_props: Default::default(), attributes_to_drop_before_render: Default::default(), + element_refs_to_drop: Default::default(), })); let context = diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 59bed3b1f..eeede6a81 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -230,17 +230,7 @@ impl ScopeContext { /// This is good for tasks that need to be run after the component has been dropped. pub fn spawn_forever(&self, fut: impl Future + 'static) -> TaskId { // The root scope will never be unmounted so we can just add the task at the top of the app - let id = self.tasks.spawn(ScopeId::ROOT, fut); - - // wake up the scheduler if it is sleeping - self.tasks - .sender - .unbounded_send(SchedulerMsg::TaskNotified(id)) - .expect("Scheduler should exist"); - - self.spawned_tasks.borrow_mut().insert(id); - - id + self.tasks.spawn(ScopeId::ROOT, fut) } /// Informs the scheduler that this task is no longer needed and should be removed. diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index f0bbd776c..40e6a91de 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -3,7 +3,7 @@ use crate::{ any_props::VProps, bump_frame::BumpFrame, innerlude::ErrorBoundary, - innerlude::{DynamicNode, EventHandler, VComponent, VText}, + innerlude::{DynamicNode, EventHandler, VComponent, VNodeId, VText}, lazynodes::LazyNodes, nodes::{IntoAttributeValue, IntoDynNode, RenderReturn}, runtime::Runtime, @@ -94,6 +94,7 @@ pub struct ScopeState { pub(crate) hook_idx: Cell, pub(crate) borrowed_props: RefCell>>, + pub(crate) element_refs_to_drop: RefCell>, pub(crate) attributes_to_drop_before_render: RefCell>>, pub(crate) props: Option>>, @@ -466,7 +467,7 @@ impl<'src> ScopeState { render_fn: component as *const (), static_props: P::IS_STATIC, props: RefCell::new(Some(extended)), - scope: Cell::new(None), + scope: Default::default(), }) } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 2d83ba63a..d1951b388 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -4,19 +4,19 @@ use crate::{ any_props::VProps, - arena::{ElementId, ElementRef}, - innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg}, + arena::ElementId, + innerlude::{DirtyScope, ElementRef, ErrorBoundary, Mutations, Scheduler, SchedulerMsg}, mutations::Mutation, nodes::RenderReturn, nodes::{Template, TemplateId}, runtime::{Runtime, RuntimeGuard}, scopes::{ScopeId, ScopeState}, - AttributeValue, Element, Event, Scope, + AttributeValue, Element, Event, Scope, VNode, }; use futures_util::{pin_mut, StreamExt}; use rustc_hash::{FxHashMap, FxHashSet}; use slab::Slab; -use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; +use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -186,7 +186,10 @@ pub struct VirtualDom { pub(crate) templates: FxHashMap>>, // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template - pub(crate) elements: Slab, + pub(crate) element_refs: Slab>>>, + + // The element ids that are used in the renderer + pub(crate) elements: Slab>, pub(crate) mutations: Mutations<'static>, @@ -263,6 +266,7 @@ impl VirtualDom { dirty_scopes: Default::default(), templates: Default::default(), elements: Default::default(), + element_refs: Default::default(), mutations: Mutations::default(), suspended_scopes: Default::default(), }; @@ -276,7 +280,7 @@ impl VirtualDom { root.provide_context(Rc::new(ErrorBoundary::new(ScopeId::ROOT))); // the root element is always given element ID 0 since it's the container for the entire tree - dom.elements.insert(ElementRef::none()); + dom.elements.insert(None); dom } @@ -314,9 +318,9 @@ impl VirtualDom { } } - /// Call a listener inside the VirtualDom with data from outside the VirtualDom. + /// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an dynamic element, not a static node or a text node.** /// - /// This method will identify the appropriate element. The data must match up with the listener delcared. Note that + /// This method will identify the appropriate element. The data must match up with the listener declared. Note that /// this method does not give any indication as to the success of the listener call. If the listener is not found, /// nothing will happen. /// @@ -353,7 +357,15 @@ impl VirtualDom { | | | <-- no, broke early | <-- no, broke early */ - let mut parent_path = self.elements.get(element.0); + let parent_path = match self.elements.get(element.0) { + Some(Some(el)) => el, + _ => return, + }; + let mut parent_node = self + .element_refs + .get(parent_path.template.0) + .cloned() + .map(|el| (*parent_path, el)); let mut listeners = vec![]; // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required @@ -365,36 +377,72 @@ impl VirtualDom { // If the event bubbles, we traverse through the tree until we find the target element. if bubbles { // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent. - while let Some(el_ref) = parent_path { + while let Some((path, el_ref)) = parent_node { // safety: we maintain references of all vnodes in the element slab - if let Some(template) = el_ref.template { - let template = unsafe { template.as_ref() }; - let node_template = template.template.get(); - let target_path = el_ref.path; + let template = unsafe { el_ref.unwrap().as_ref() }; + let node_template = template.template.get(); + let target_path = path.path; - for (idx, attr) in template.dynamic_attrs.iter().enumerate() { - let this_path = node_template.attr_paths[idx]; + 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); + // 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); - // 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 - if target_path == this_path { - break; - } + // 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 + if target_path == this_path { + break; } } + } - // Now that we've accumulated all the parent attributes for the target element, call them in reverse order - // We check the bubble state between each call to see if the event has been stopped from bubbling - for listener in listeners.drain(..).rev() { - if let AttributeValue::Listener(listener) = listener { - let origin = el_ref.scope; + // Now that we've accumulated all the parent attributes for the target element, call them in reverse order + // We check the bubble state between each call to see if the event has been stopped from bubbling + for listener in listeners.drain(..).rev() { + if let AttributeValue::Listener(listener) = listener { + let origin = path.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); + + if !uievent.propagates.get() { + return; + } + } + } + + parent_node = template.parent.get().and_then(|element_ref| { + self.element_refs + .get(element_ref.template.0) + .cloned() + .map(|el| (element_ref, el)) + }); + } + } else { + // Otherwise, we just call the listener on the target element + if let Some((path, el_ref)) = parent_node { + // safety: we maintain references of all vnodes in the element slab + let template = unsafe { el_ref.unwrap().as_ref() }; + let node_template = template.template.get(); + let target_path = path.path; + + 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 = path.scope; self.runtime.scope_stack.borrow_mut().push(origin); self.runtime.rendering.set(false); if let Some(cb) = listener.borrow_mut().as_deref_mut() { @@ -403,44 +451,7 @@ impl VirtualDom { self.runtime.scope_stack.borrow_mut().pop(); self.runtime.rendering.set(true); - if !uievent.propagates.get() { - return; - } - } - } - - parent_path = template.parent.and_then(|id| self.elements.get(id.0)); - } else { - break; - } - } - } else { - // Otherwise, we just call the listener on the target element - if let Some(el_ref) = parent_path { - // safety: we maintain references of all vnodes in the element slab - if let Some(template) = el_ref.template { - let template = unsafe { template.as_ref() }; - let node_template = template.template.get(); - let target_path = el_ref.path; - - 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); - - break; - } + break; } } } @@ -563,7 +574,7 @@ impl VirtualDom { // If an error occurs, we should try to render the default error component and context where the error occured RenderReturn::Aborted(placeholder) => { tracing::debug!("Ran into suspended or aborted scope during rebuild"); - let id = self.next_null(); + let id = self.next_element(); placeholder.id.set(Some(id)); self.mutations.push(Mutation::CreatePlaceholder { id }); } @@ -595,15 +606,12 @@ impl VirtualDom { /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content pub async fn wait_for_suspense(&mut self) { loop { - // println!("waiting for suspense {:?}", self.suspended_scopes); if self.suspended_scopes.is_empty() { return; } - // println!("waiting for suspense"); self.wait_for_work().await; - // println!("Rendered immediately"); _ = self.render_immediate(); } } diff --git a/packages/core/tests/event_propagation.rs b/packages/core/tests/event_propagation.rs new file mode 100644 index 000000000..d694b1ff1 --- /dev/null +++ b/packages/core/tests/event_propagation.rs @@ -0,0 +1,69 @@ +use dioxus::prelude::*; +use dioxus_core::ElementId; +use std::{rc::Rc, sync::Mutex}; + +static CLICKS: Mutex = Mutex::new(0); + +#[test] +fn events_propagate() { + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + // Top-level click is registered + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true); + assert_eq!(*CLICKS.lock().unwrap(), 1); + + // break reference.... + for _ in 0..5 { + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + } + + // Lower click is registered + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + assert_eq!(*CLICKS.lock().unwrap(), 3); + + // break reference.... + for _ in 0..5 { + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + } + + // Stop propagation occurs + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + assert_eq!(*CLICKS.lock().unwrap(), 3); +} + +fn app(cx: Scope) -> Element { + render! { + div { + onclick: move |_| { + println!("top clicked"); + *CLICKS.lock().unwrap() += 1; + }, + + vec![ + render! { + problematic_child {} + } + ].into_iter() + } + } +} + +fn problematic_child(cx: Scope) -> Element { + render! { + button { + onclick: move |evt| { + println!("bottom clicked"); + let mut clicks = CLICKS.lock().unwrap(); + + if *clicks == 3 { + evt.stop_propagation(); + } else { + *clicks += 1; + } + } + } + } +} diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index 1523c4e22..65d131eb8 100644 --- a/packages/core/tests/fuzzing.rs +++ b/packages/core/tests/fuzzing.rs @@ -2,7 +2,7 @@ use dioxus::prelude::Props; use dioxus_core::*; -use std::{cell::Cell, collections::HashSet}; +use std::{cfg, collections::HashSet}; fn random_ns() -> Option<&'static str> { let namespace = rand::random::() % 2; @@ -170,22 +170,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { let range = if depth > 5 { 1 } else { 4 }; match rand::random::() % range { 0 => DynamicNode::Placeholder(Default::default()), - 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| VNode { - key: None, - parent: Default::default(), - template: Cell::new(Template { - name: concat!(file!(), ":", line!(), ":", column!(), ":0"), - roots: &[TemplateNode::Dynamic { id: 0 }], - node_paths: &[&[0]], - attr_paths: &[], - }), - root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(), - dynamic_nodes: cx.bump().alloc([cx.component( - create_random_element, - DepthProps { depth, root: false }, - "create_random_element", - )]), - dynamic_attrs: &[], + 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| { + VNode::new( + None, + Template { + name: concat!(file!(), ":", line!(), ":", column!(), ":0"), + roots: &[TemplateNode::Dynamic { id: 0 }], + node_paths: &[&[0]], + attr_paths: &[], + }, + bumpalo::collections::Vec::new_in(cx.bump()), + cx.bump().alloc([cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + )]), + &[], + ) })), 2 => cx.component( create_random_element, @@ -271,13 +272,11 @@ fn create_random_element(cx: Scope) -> Element { ) .into_boxed_str(), )); - // println!("{template:#?}"); - let node = VNode { - key: None, - parent: None, - template: Cell::new(template), - root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(), - dynamic_nodes: { + let node = VNode::new( + None, + template, + bumpalo::collections::Vec::new_in(cx.bump()), + { let dynamic_nodes: Vec<_> = dynamic_node_types .iter() .map(|ty| match ty { @@ -291,12 +290,12 @@ fn create_random_element(cx: Scope) -> Element { .collect(); cx.bump().alloc(dynamic_nodes) }, - dynamic_attrs: cx.bump().alloc( + cx.bump().alloc( (0..template.attr_paths.len()) .map(|_| create_random_dynamic_attr(cx)) .collect::>(), ), - }; + ); Some(node) } _ => None, @@ -306,10 +305,10 @@ fn create_random_element(cx: Scope) -> Element { } // test for panics when creating random nodes and templates -#[cfg(not(miri))] #[test] fn create() { - for _ in 0..1000 { + let repeat_count = if cfg!(miri) { 100 } else { 1000 }; + for _ in 0..repeat_count { let mut vdom = VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); let _ = vdom.rebuild(); @@ -318,10 +317,10 @@ fn create() { // test for panics when diffing random nodes // This test will change the template every render which is not very realistic, but it helps stress the system -#[cfg(not(miri))] #[test] fn diff() { - for _ in 0..100000 { + let repeat_count = if cfg!(miri) { 100 } else { 1000 }; + for _ in 0..repeat_count { let mut vdom = VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); let _ = vdom.rebuild(); diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 2290e3c95..3d8c2d31b 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -18,8 +18,8 @@ dioxus-hot-reload = { workspace = true, optional = true } serde = "1.0.136" serde_json = "1.0.79" thiserror = { workspace = true } -wry = { version = "0.33.0", default-features = false, features = ["protocol", "file-drop", "tao"] } tracing = { workspace = true } +wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] } futures-channel = { workspace = true } tokio = { workspace = true, features = [ "sync", @@ -42,7 +42,7 @@ crossbeam-channel = "0.5.8" [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.11.3" +rfd = "0.12" global-hotkey = { git = "https://github.com/tauri-apps/global-hotkey" } [target.'cfg(target_os = "ios")'.dependencies] diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index 646b951e1..b0aef5f79 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -232,6 +232,12 @@ fn app(cx: Scope) -> Element { 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 { id: "mouse_move_div", @@ -240,7 +246,7 @@ fn app(cx: Scope) -> Element { assert!(event.data.modifiers().is_empty()); assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); received_events.modify(|x| *x + 1) - }, + } } div { id: "mouse_click_div", @@ -250,11 +256,11 @@ fn app(cx: Scope) -> Element { assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); received_events.modify(|x| *x + 1) - }, + } } - div{ + div { id: "mouse_dblclick_div", - ondblclick: move |event| { + ondoubleclick: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); @@ -263,7 +269,7 @@ fn app(cx: Scope) -> Element { received_events.modify(|x| *x + 1) } } - div{ + div { id: "mouse_down_div", onmousedown: move |event| { println!("{:?}", event.data); @@ -273,7 +279,7 @@ fn app(cx: Scope) -> Element { received_events.modify(|x| *x + 1) } } - div{ + div { id: "mouse_up_div", onmouseup: move |event| { println!("{:?}", event.data); @@ -283,21 +289,20 @@ fn app(cx: Scope) -> Element { received_events.modify(|x| *x + 1) } } - div{ + div { id: "wheel_div", width: "100px", height: "100px", background_color: "red", onwheel: move |event| { println!("{:?}", event.data); - let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{ - panic!("Expected delta to be in pixels") - }; + let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else { + panic!("Expected delta to be in pixels") }; assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0)); received_events.modify(|x| *x + 1) } } - input{ + input { id: "key_down_div", onkeydown: move |event| { println!("{:?}", event.data); @@ -306,11 +311,11 @@ fn app(cx: Scope) -> Element { assert_eq!(event.data.code().to_string(), "KeyA"); assert_eq!(event.data.location, 0); assert!(event.data.is_auto_repeating()); - received_events.modify(|x| *x + 1) + } } - input{ + input { id: "key_up_div", onkeyup: move |event| { println!("{:?}", event.data); @@ -319,11 +324,10 @@ fn app(cx: Scope) -> Element { assert_eq!(event.data.code().to_string(), "KeyA"); assert_eq!(event.data.location, 0); assert!(!event.data.is_auto_repeating()); - received_events.modify(|x| *x + 1) } } - input{ + input { id: "key_press_div", onkeypress: move |event| { println!("{:?}", event.data); @@ -332,18 +336,17 @@ fn app(cx: Scope) -> Element { assert_eq!(event.data.code().to_string(), "KeyA"); assert_eq!(event.data.location, 0); assert!(!event.data.is_auto_repeating()); - received_events.modify(|x| *x + 1) } } - input{ + input { id: "focus_in_div", onfocusin: move |event| { println!("{:?}", event.data); received_events.modify(|x| *x + 1) } } - input{ + input { id: "focus_out_div", onfocusout: move |event| { println!("{:?}", event.data); diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 00f4847c4..596b21938 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -35,6 +35,7 @@ pub struct Config { pub(crate) root_name: String, pub(crate) background_color: Option<(u8, u8, u8, u8)>, pub(crate) last_window_close_behaviour: WindowCloseBehaviour, + pub(crate) enable_default_menu_bar: bool, } type DropHandler = Box bool>; @@ -64,9 +65,18 @@ impl Config { root_name: "main".to_string(), background_color: None, last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp, + enable_default_menu_bar: true, } } + /// Set whether the default menu bar should be enabled. + /// + /// > Note: `enable` is `true` by default. To disable the default menu bar pass `false`. + pub fn with_default_menu_bar(mut self, enable: bool) -> Self { + self.enable_default_menu_bar = enable; + self + } + /// set the directory from which assets will be searched in release mode pub fn with_resource_directory(mut self, path: impl Into) -> Self { self.resource_dir = Some(path.into()); diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index de5eefc89..adf358f84 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,7 +1,10 @@ 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 dioxus_core::ScopeState; @@ -114,6 +117,7 @@ pub struct DesktopService { pub(crate) max_template_count: AtomicU16, pub(crate) channel: RefCell, + pub(crate) asset_handlers: AssetHandlerRegistry, #[cfg(target_os = "ios")] pub(crate) views: Rc>>, @@ -140,6 +144,7 @@ impl DesktopService { event_handlers: WindowEventHandlers, shortcut_manager: ShortcutRegistry, edit_queue: EditQueue, + asset_handlers: AssetHandlerRegistry, ) -> Self { Self { webview: Rc::new(webview), @@ -153,6 +158,7 @@ impl DesktopService { templates: Default::default(), max_template_count: Default::default(), channel: Default::default(), + asset_handlers, #[cfg(target_os = "ios")] views: Default::default(), } @@ -303,6 +309,20 @@ impl DesktopService { self.shortcut_manager.remove_all() } + /// Provide a callback to handle asset loading yourself. + /// + /// See [`use_asset_handle`](crate::use_asset_handle) for a convenient hook. + pub async fn register_asset_handler(&self, f: impl AssetHandler) -> usize { + self.asset_handlers.register_handler(f).await + } + + /// Removes an asset handler by its identifier. + /// + /// Returns `None` if the handler did not exist. + pub async fn remove_asset_handler(&self, id: usize) -> Option<()> { + self.asset_handlers.remove_handler(id).await + } + /// Push an objc view to the window #[cfg(target_os = "ios")] pub fn push_view(&self, view: objc_id::ShareId) { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index d394635ed..50138921c 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -34,6 +34,7 @@ use element::DesktopElement; use eval::init_eval; use futures_util::{pin_mut, FutureExt}; use rustc_hash::FxHashMap; +pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse}; use shortcut::ShortcutRegistry; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; use std::cell::Cell; @@ -48,6 +49,7 @@ use tao::{ event::{Event, StartCause, WindowEvent}, event_loop::ControlFlow, }; +pub use webview::build_default_menu_bar; pub use wry; pub use wry::application as tao; use wry::application::event_loop::EventLoopBuilder; @@ -401,7 +403,8 @@ fn create_new_window( event_handlers: &WindowEventHandlers, shortcut_manager: ShortcutRegistry, ) -> WebviewHandler { - let (webview, web_context, edit_queue) = webview::build(&mut cfg, event_loop, proxy.clone()); + 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(), @@ -409,6 +412,7 @@ fn create_new_window( queue.clone(), event_handlers.clone(), shortcut_manager, + asset_handlers, edit_queue, )); diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index d8ed0478c..f00221d85 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,18 +1,58 @@ +use dioxus_core::ScopeState; +use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS}; +use slab::Slab; use std::{ borrow::Cow, + future::Future, + ops::Deref, path::{Path, PathBuf}, + pin::Pin, + rc::Rc, + sync::Arc, +}; +use tokio::{ + runtime::Handle, + sync::{OnceCell, RwLock}, }; use wry::{ http::{status::StatusCode, Request, Response}, webview::RequestAsyncResponder, Result, }; +use crate::{use_window, DesktopContext}; 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#"