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/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 d22269a0c..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.28.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/src/builder.rs b/packages/cli/src/builder.rs index 759be7dec..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), @@ -286,6 +288,14 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result cmd.arg("--bin").arg(name), crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), @@ -303,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/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/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/create.rs b/packages/core/src/create.rs index 892cb26ab..f3b89ae9e 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -225,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 /// @@ -443,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 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/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/desktop/Cargo.toml b/packages/desktop/Cargo.toml index f0ea9e189..cdb7021ed 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.28.0", default-features = false, features = ["protocol", "file-drop"] } 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", @@ -37,10 +37,12 @@ slab = { workspace = true } futures-util = { workspace = true } urlencoding = "2.1.2" async-trait = "0.1.68" +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] objc = "0.2.7" @@ -56,8 +58,6 @@ tokio_runtime = ["tokio"] fullscreen = ["wry/fullscreen"] transparent = ["wry/transparent"] devtools = ["wry/devtools"] -tray = ["wry/tray"] -dox = ["wry/dox"] hot-reload = ["dioxus-hot-reload"] gnu = [] diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index ac62cd37b..6d29b6ffa 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -229,75 +229,111 @@ fn app(cx: Scope) -> Element { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert!(event.data.held_buttons().is_empty()); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); recieved_events.modify(|x| *x + 1) - }, + } } div { id: "mouse_move_div", onmousemove: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); recieved_events.modify(|x| *x + 1) - }, + } } div { id: "mouse_click_div", onclick: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); - 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)); - recieved_events.modify(|x| *x + 1) - }, - } - div{ - id: "mouse_dblclick_div", - ondblclick: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); - 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)); + 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), + ); recieved_events.modify(|x| *x + 1) } } - div{ + div { + id: "mouse_dblclick_div", + ondoubleclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!( + event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary), + ); + 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), + ); + recieved_events.modify(|x| *x + 1) + } + } + div { id: "mouse_down_div", onmousedown: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); - 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)); + 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), + ); recieved_events.modify(|x| *x + 1) } } - div{ + div { id: "mouse_up_div", onmouseup: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert!(event.data.held_buttons().is_empty()); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); recieved_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)); recieved_events.modify(|x| *x + 1) } } - input{ + input { id: "key_down_div", onkeydown: move |event| { println!("{:?}", event.data); @@ -306,11 +342,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()); - recieved_events.modify(|x| *x + 1) } } - input{ + input { id: "key_up_div", onkeyup: move |event| { println!("{:?}", event.data); @@ -319,11 +354,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()); - recieved_events.modify(|x| *x + 1) } } - input{ + input { id: "key_press_div", onkeypress: move |event| { println!("{:?}", event.data); @@ -332,18 +366,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()); - recieved_events.modify(|x| *x + 1) } } - input{ + input { id: "focus_in_div", onfocusin: move |event| { println!("{:?}", event.data); recieved_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 07281e364..3b0409ae8 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -36,6 +36,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>; @@ -65,9 +66,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 897f8eba8..fb0615900 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -4,10 +4,11 @@ use std::rc::Weak; use crate::create_new_window; use crate::events::IpcMessage; +use crate::protocol::AssetFuture; +use crate::protocol::AssetHandlerRegistry; use crate::query::QueryEngine; -use crate::shortcut::ShortcutId; -use crate::shortcut::ShortcutRegistry; -use crate::shortcut::ShortcutRegistryError; +use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; +use crate::AssetHandler; use crate::Config; use crate::WebviewHandler; use dioxus_core::ScopeState; @@ -15,7 +16,6 @@ use dioxus_core::VirtualDom; #[cfg(all(feature = "hot-reload", debug_assertions))] use dioxus_hot_reload::HotReloadMsg; use slab::Slab; -use wry::application::accelerator::Accelerator; use wry::application::event::Event; use wry::application::event_loop::EventLoopProxy; use wry::application::event_loop::EventLoopWindowTarget; @@ -67,6 +67,8 @@ pub struct DesktopService { pub(crate) shortcut_manager: ShortcutRegistry, + pub(crate) asset_handlers: AssetHandlerRegistry, + #[cfg(target_os = "ios")] pub(crate) views: Rc>>, } @@ -91,6 +93,7 @@ impl DesktopService { webviews: WebviewQueue, event_handlers: WindowEventHandlers, shortcut_manager: ShortcutRegistry, + asset_handlers: AssetHandlerRegistry, ) -> Self { Self { webview: Rc::new(webview), @@ -100,6 +103,7 @@ impl DesktopService { pending_windows: webviews, event_handlers, shortcut_manager, + asset_handlers, #[cfg(target_os = "ios")] views: Default::default(), } @@ -233,11 +237,11 @@ impl DesktopService { /// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information. pub fn create_shortcut( &self, - accelerator: Accelerator, + hotkey: HotKey, callback: impl FnMut() + 'static, ) -> Result { self.shortcut_manager - .add_shortcut(accelerator, Box::new(callback)) + .add_shortcut(hotkey, Box::new(callback)) } /// Remove a global shortcut @@ -250,6 +254,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) { @@ -369,17 +387,10 @@ impl WryWindowEventHandlerInner { target: &EventLoopWindowTarget, ) { // if this event does not apply to the window this listener cares about, return - match event { - Event::WindowEvent { window_id, .. } - | Event::MenuEvent { - window_id: Some(window_id), - .. - } => { - if *window_id != self.window_id { - return; - } + if let Event::WindowEvent { window_id, .. } = event { + if *window_id != self.window_id { + return; } - _ => (), } (self.handler)(event, target) } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 63a8914f3..105bf0019 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -10,16 +10,16 @@ mod escape; mod eval; mod events; mod file_upload; +#[cfg(any(target_os = "ios", target_os = "android"))] +mod mobile_shortcut; mod protocol; mod query; mod shortcut; mod waker; mod webview; -#[cfg(any(target_os = "ios", target_os = "android"))] -mod mobile_shortcut; - use crate::query::QueryResult; +use crate::shortcut::GlobalHotKeyEvent; pub use cfg::{Config, WindowCloseBehaviour}; pub use desktop_context::DesktopContext; pub use desktop_context::{ @@ -32,6 +32,7 @@ use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; use element::DesktopElement; use eval::init_eval; use futures_util::{pin_mut, FutureExt}; +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; @@ -43,10 +44,12 @@ use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use tao::window::WindowBuilder; use tao::{ event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, + 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; use wry::webview::WebView; use wry::{application::window::WindowId, webview::WebContext}; @@ -120,7 +123,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// } /// ``` pub fn launch_with_props(root: Component

, props: P, cfg: Config) { - let event_loop = EventLoop::::with_user_event(); + let event_loop = EventLoopBuilder::::with_user_event().build(); let proxy = event_loop.create_proxy(); @@ -157,7 +160,8 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) let queue = WebviewQueue::default(); - let shortcut_manager = ShortcutRegistry::new(&event_loop); + let shortcut_manager = ShortcutRegistry::new(); + let global_hotkey_channel = GlobalHotKeyEvent::receiver(); // move the props into a cell so we can pop it out later to create the first window // iOS panics if we create a window before the event loop is started @@ -166,10 +170,14 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) let mut is_visible_before_start = true; event_loop.run(move |window_event, event_loop, control_flow| { - *control_flow = ControlFlow::Wait; + *control_flow = ControlFlow::Poll; event_handlers.apply_event(&window_event, event_loop); + if let Ok(event) = global_hotkey_channel.try_recv() { + shortcut_manager.call_handlers(event); + } + match window_event { Event::WindowEvent { event, window_id, .. @@ -375,7 +383,6 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) _ => {} }, - Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id), _ => {} } }) @@ -390,7 +397,8 @@ fn create_new_window( event_handlers: &WindowEventHandlers, shortcut_manager: ShortcutRegistry, ) -> WebviewHandler { - let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone()); + let (webview, web_context, asset_handlers) = + webview::build(&mut cfg, event_loop, proxy.clone()); let desktop_context = Rc::from(DesktopService::new( webview, proxy.clone(), @@ -398,6 +406,7 @@ fn create_new_window( queue.clone(), event_handlers.clone(), shortcut_manager, + asset_handlers, )); let cx = dom.base_scope(); diff --git a/packages/desktop/src/mobile_shortcut.rs b/packages/desktop/src/mobile_shortcut.rs index 29149b55b..c6144344b 100644 --- a/packages/desktop/src/mobile_shortcut.rs +++ b/packages/desktop/src/mobile_shortcut.rs @@ -1,29 +1,51 @@ #![allow(unused)] use super::*; -use wry::application::accelerator::Accelerator; +use std::str::FromStr; use wry::application::event_loop::EventLoopWindowTarget; -pub struct GlobalShortcut(); -pub struct ShortcutManager(); +use dioxus_html::input_data::keyboard_types::Modifiers; -impl ShortcutManager { - pub fn new(target: &EventLoopWindowTarget) -> Self { - Self() +#[derive(Clone, Debug)] +pub struct Accelerator; + +#[derive(Clone, Copy)] +pub struct HotKey; + +impl HotKey { + pub fn new(mods: Option, key: Code) -> Self { + Self } - pub fn register( - &mut self, - accelerator: Accelerator, - ) -> Result { - Ok(GlobalShortcut()) + pub fn id(&self) -> u32 { + 0 + } +} + +impl FromStr for HotKey { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(HotKey) + } +} + +pub struct GlobalHotKeyManager(); + +impl GlobalHotKeyManager { + pub fn new() -> Result { + Ok(Self()) } - pub fn unregister(&mut self, id: ShortcutId) -> Result<(), ShortcutManagerError> { + pub fn register(&mut self, accelerator: HotKey) -> Result { + Ok(HotKey) + } + + pub fn unregister(&mut self, id: HotKey) -> Result<(), HotkeyError> { Ok(()) } - pub fn unregister_all(&mut self) -> Result<(), ShortcutManagerError> { + pub fn unregister_all(&mut self, _: &[HotKey]) -> Result<(), HotkeyError> { Ok(()) } } @@ -33,23 +55,35 @@ use std::{error, fmt}; /// An error whose cause the `ShortcutManager` to fail. #[non_exhaustive] #[derive(Debug)] -pub enum ShortcutManagerError { +pub enum HotkeyError { AcceleratorAlreadyRegistered(Accelerator), AcceleratorNotRegistered(Accelerator), - InvalidAccelerator(String), + HotKeyParseError(String), } -impl error::Error for ShortcutManagerError {} -impl fmt::Display for ShortcutManagerError { +impl error::Error for HotkeyError {} +impl fmt::Display for HotkeyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { - ShortcutManagerError::AcceleratorAlreadyRegistered(e) => { + HotkeyError::AcceleratorAlreadyRegistered(e) => { f.pad(&format!("hotkey already registered: {:?}", e)) } - ShortcutManagerError::AcceleratorNotRegistered(e) => { + HotkeyError::AcceleratorNotRegistered(e) => { f.pad(&format!("hotkey not registered: {:?}", e)) } - ShortcutManagerError::InvalidAccelerator(e) => e.fmt(f), + HotkeyError::HotKeyParseError(e) => e.fmt(f), } } } + +pub struct GlobalHotKeyEvent { + pub id: u32, +} + +impl GlobalHotKeyEvent { + pub fn receiver() -> crossbeam_channel::Receiver { + crossbeam_channel::unbounded().1 + } +} + +pub(crate) type Code = dioxus_html::input_data::keyboard_types::Code; diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 2f660b7a2..46be1b942 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,13 +1,26 @@ +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}, Result, }; +use crate::{use_window, DesktopContext}; + fn module_loader(root_name: &str) -> String { let js = INTERPRETER_JS.replace( "/*POST_HANDLE_EDITS*/", @@ -51,12 +64,156 @@ fn module_loader(root_name: &str) -> String { ) } -pub(super) fn desktop_handler( - request: &Request>, +/// An arbitrary asset is an HTTP response containing a binary body. +pub type AssetResponse = Response>; + +/// A future that returns an [`AssetResponse`]. This future may be spawned in a new thread, +/// so it must be [`Send`], [`Sync`], and `'static`. +pub trait AssetFuture: Future> + Send + Sync + 'static {} +impl> + Send + Sync + 'static> AssetFuture for T {} + +#[derive(Debug, Clone)] +/// A request for an asset. This is a wrapper around [`Request>`] that provides methods specific to asset requests. +pub struct AssetRequest { + path: PathBuf, + request: Arc>>, +} + +impl AssetRequest { + /// Get the path the asset request is for + pub fn path(&self) -> &Path { + &self.path + } +} + +impl From>> for AssetRequest { + fn from(request: Request>) -> Self { + let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/')) + .expect("expected URL to be UTF-8 encoded"); + let path = PathBuf::from(&*decoded); + Self { + request: Arc::new(request), + path, + } + } +} + +impl Deref for AssetRequest { + type Target = Request>; + + fn deref(&self) -> &Self::Target { + &self.request + } +} + +/// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`. +/// This handler is stashed indefinitely in a context object, so it must be `'static`. +pub trait AssetHandler: Send + Sync + 'static { + /// Handle an asset request, returning a future that either loads the asset, or returns `None` + fn handle_request(&self, request: &AssetRequest) -> F; +} + +impl F + Send + Sync + 'static> AssetHandler for T { + fn handle_request(&self, request: &AssetRequest) -> F { + self(request) + } +} + +type AssetHandlerRegistryInner = + Slab Pin> + Send + Sync + 'static>>; + +#[derive(Clone)] +pub struct AssetHandlerRegistry(Arc>); + +impl AssetHandlerRegistry { + pub fn new() -> Self { + AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new()))) + } + + pub async fn register_handler(&self, f: impl AssetHandler) -> usize { + let mut registry = self.0.write().await; + registry.insert(Box::new(move |req| Box::pin(f.handle_request(req)))) + } + + pub async fn remove_handler(&self, id: usize) -> Option<()> { + let mut registry = self.0.write().await; + registry.try_remove(id).map(|_| ()) + } + + pub async fn try_handlers(&self, req: &AssetRequest) -> Option { + let registry = self.0.read().await; + for (_, handler) in registry.iter() { + if let Some(response) = handler(req).await { + return Some(response); + } + } + None + } +} + +/// A handle to a registered asset handler. +pub struct AssetHandlerHandle { + desktop: DesktopContext, + handler_id: Rc>, +} + +impl AssetHandlerHandle { + /// Returns the ID for this handle. + /// + /// Because registering an ID is asynchronous, this may return `None` if the + /// registration has not completed yet. + pub fn handler_id(&self) -> Option { + self.handler_id.get().copied() + } +} + +impl Drop for AssetHandlerHandle { + fn drop(&mut self) { + let cell = Rc::clone(&self.handler_id); + let desktop = Rc::clone(&self.desktop); + tokio::task::block_in_place(move || { + Handle::current().block_on(async move { + if let Some(id) = cell.get() { + desktop.asset_handlers.remove_handler(*id).await; + } + }) + }); + } +} + +/// Provide a callback to handle asset loading yourself. +/// +/// The callback takes a path as requested by the web view, and it should return `Some(response)` +/// if you want to load the asset, and `None` if you want to fallback on the default behavior. +pub fn use_asset_handler( + cx: &ScopeState, + handler: impl AssetHandler, +) -> &AssetHandlerHandle { + let desktop = Rc::clone(use_window(cx)); + cx.use_hook(|| { + let handler_id = Rc::new(OnceCell::new()); + let handler_id_ref = Rc::clone(&handler_id); + let desktop_ref = Rc::clone(&desktop); + cx.push_future(async move { + let id = desktop.asset_handlers.register_handler(handler).await; + handler_id.set(id).unwrap(); + }); + AssetHandlerHandle { + desktop: desktop_ref, + handler_id: handler_id_ref, + } + }) +} + +pub(super) async fn desktop_handler( + request: Request>, custom_head: Option, custom_index: Option, root_name: &str, -) -> Result>> { + asset_handlers: &AssetHandlerRegistry, +) -> Result { + let request = AssetRequest::from(request); + // If the request is for the root, we'll serve the index.html file. if request.uri().path() == "/" { // If a custom index is provided, just defer to that, expecting the user to know what they're doing. @@ -91,18 +248,21 @@ pub(super) fn desktop_handler( .map_err(From::from); } + // If the user provided a custom asset handler, then call it and return the response + // if the request was handled. + if let Some(response) = asset_handlers.try_handlers(&request).await { + return Ok(response); + } + // Else, try to serve a file from the filesystem. - let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/')) - .expect("expected URL to be UTF-8 encoded"); - let path = PathBuf::from(&*decoded); // If the path is relative, we'll try to serve it from the assets directory. let mut asset = get_asset_root() .unwrap_or_else(|| Path::new(".").to_path_buf()) - .join(&path); + .join(&request.path); if !asset.exists() { - asset = PathBuf::from("/").join(path); + asset = PathBuf::from("/").join(request.path); } if asset.exists() { diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs index ffc64a232..66ab4ba7a 100644 --- a/packages/desktop/src/shortcut.rs +++ b/packages/desktop/src/shortcut.rs @@ -3,11 +3,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr}; use dioxus_core::ScopeState; use dioxus_html::input_data::keyboard_types::Modifiers; use slab::Slab; -use wry::application::{ - accelerator::{Accelerator, AcceleratorId}, - event_loop::EventLoopWindowTarget, - keyboard::{KeyCode, ModifiersState}, -}; +use wry::application::keyboard::ModifiersState; use crate::{desktop_context::DesktopContext, use_window}; @@ -20,22 +16,25 @@ use crate::{desktop_context::DesktopContext, use_window}; target_os = "netbsd", target_os = "openbsd" ))] -use wry::application::global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError}; +pub use global_hotkey::{ + hotkey::{Code, HotKey}, + Error as HotkeyError, GlobalHotKeyEvent, GlobalHotKeyManager, +}; #[cfg(any(target_os = "ios", target_os = "android"))] pub use crate::mobile_shortcut::*; #[derive(Clone)] pub(crate) struct ShortcutRegistry { - manager: Rc>, + manager: Rc>, shortcuts: ShortcutMap, } -type ShortcutMap = Rc>>; +type ShortcutMap = Rc>>; struct Shortcut { #[allow(unused)] - shortcut: GlobalShortcut, + shortcut: HotKey, callbacks: Slab>, } @@ -54,15 +53,15 @@ impl Shortcut { } impl ShortcutRegistry { - pub fn new(target: &EventLoopWindowTarget) -> Self { + pub fn new() -> Self { Self { - manager: Rc::new(RefCell::new(ShortcutManager::new(target))), + manager: Rc::new(RefCell::new(GlobalHotKeyManager::new().unwrap())), shortcuts: Rc::new(RefCell::new(HashMap::new())), } } - pub(crate) fn call_handlers(&self, id: AcceleratorId) { - if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id) { + pub(crate) fn call_handlers(&self, id: GlobalHotKeyEvent) { + if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id.id) { for (_, callback) in callbacks.iter_mut() { (callback)(); } @@ -71,10 +70,10 @@ impl ShortcutRegistry { pub(crate) fn add_shortcut( &self, - accelerator: Accelerator, + hotkey: HotKey, callback: Box, ) -> Result { - let accelerator_id = accelerator.clone().id(); + let accelerator_id = hotkey.clone().id(); let mut shortcuts = self.shortcuts.borrow_mut(); Ok( if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) { @@ -84,12 +83,12 @@ impl ShortcutRegistry { number: id, } } else { - match self.manager.borrow_mut().register(accelerator) { - Ok(global_shortcut) => { + match self.manager.borrow_mut().register(hotkey) { + Ok(_) => { let mut slab = Slab::new(); let id = slab.insert(callback); let shortcut = Shortcut { - shortcut: global_shortcut, + shortcut: hotkey, callbacks: slab, }; shortcuts.insert(accelerator_id, shortcut); @@ -98,7 +97,7 @@ impl ShortcutRegistry { number: id, } } - Err(ShortcutManagerError::InvalidAccelerator(shortcut)) => { + Err(HotkeyError::HotKeyParseError(shortcut)) => { return Err(ShortcutRegistryError::InvalidShortcut(shortcut)) } Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))), @@ -113,15 +112,6 @@ impl ShortcutRegistry { callbacks.remove(id.number); if callbacks.is_empty() { if let Some(_shortcut) = shortcuts.remove(&id.id) { - #[cfg(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut); } } @@ -130,8 +120,8 @@ impl ShortcutRegistry { pub(crate) fn remove_all(&self) { let mut shortcuts = self.shortcuts.borrow_mut(); - shortcuts.clear(); - let _ = self.manager.borrow_mut().unregister_all(); + let hotkeys: Vec<_> = shortcuts.drain().map(|(_, v)| v.shortcut).collect(); + let _ = self.manager.borrow_mut().unregister_all(&hotkeys); } } @@ -148,7 +138,7 @@ pub enum ShortcutRegistryError { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// An global id for a shortcut. pub struct ShortcutId { - id: AcceleratorId, + id: u32, number: usize, } @@ -160,30 +150,30 @@ pub struct ShortcutHandle { } pub trait IntoAccelerator { - fn accelerator(&self) -> Accelerator; + fn accelerator(&self) -> HotKey; } impl IntoAccelerator for (dioxus_html::KeyCode, ModifiersState) { - fn accelerator(&self) -> Accelerator { - Accelerator::new(Some(self.1), self.0.into_key_code()) + fn accelerator(&self) -> HotKey { + HotKey::new(Some(self.1.into_modifiers_state()), self.0.into_key_code()) } } impl IntoAccelerator for (ModifiersState, dioxus_html::KeyCode) { - fn accelerator(&self) -> Accelerator { - Accelerator::new(Some(self.0), self.1.into_key_code()) + fn accelerator(&self) -> HotKey { + HotKey::new(Some(self.0.into_modifiers_state()), self.1.into_key_code()) } } impl IntoAccelerator for dioxus_html::KeyCode { - fn accelerator(&self) -> Accelerator { - Accelerator::new(None, self.into_key_code()) + fn accelerator(&self) -> HotKey { + HotKey::new(None, self.into_key_code()) } } impl IntoAccelerator for &str { - fn accelerator(&self) -> Accelerator { - Accelerator::from_str(self).unwrap() + fn accelerator(&self) -> HotKey { + HotKey::from_str(self).unwrap() } } @@ -220,143 +210,144 @@ impl Drop for ShortcutHandle { } pub trait IntoModifersState { - fn into_modifiers_state(self) -> ModifiersState; + fn into_modifiers_state(self) -> Modifiers; } impl IntoModifersState for ModifiersState { - fn into_modifiers_state(self) -> ModifiersState { - self + fn into_modifiers_state(self) -> Modifiers { + let mut modifiers = Modifiers::default(); + if self.shift_key() { + modifiers |= Modifiers::SHIFT; + } + if self.control_key() { + modifiers |= Modifiers::CONTROL; + } + if self.alt_key() { + modifiers |= Modifiers::ALT; + } + if self.super_key() { + modifiers |= Modifiers::META; + } + + modifiers } } impl IntoModifersState for Modifiers { - fn into_modifiers_state(self) -> ModifiersState { - let mut state = ModifiersState::empty(); - if self.contains(Modifiers::SHIFT) { - state |= ModifiersState::SHIFT - } - if self.contains(Modifiers::CONTROL) { - state |= ModifiersState::CONTROL - } - if self.contains(Modifiers::ALT) { - state |= ModifiersState::ALT - } - if self.contains(Modifiers::META) || self.contains(Modifiers::SUPER) { - state |= ModifiersState::SUPER - } - state + fn into_modifiers_state(self) -> Modifiers { + self } } pub trait IntoKeyCode { - fn into_key_code(self) -> KeyCode; + fn into_key_code(self) -> Code; } -impl IntoKeyCode for KeyCode { - fn into_key_code(self) -> KeyCode { +impl IntoKeyCode for Code { + fn into_key_code(self) -> Code { self } } impl IntoKeyCode for dioxus_html::KeyCode { - fn into_key_code(self) -> KeyCode { + fn into_key_code(self) -> Code { match self { - dioxus_html::KeyCode::Backspace => KeyCode::Backspace, - dioxus_html::KeyCode::Tab => KeyCode::Tab, - dioxus_html::KeyCode::Clear => KeyCode::NumpadClear, - dioxus_html::KeyCode::Enter => KeyCode::Enter, - dioxus_html::KeyCode::Shift => KeyCode::ShiftLeft, - dioxus_html::KeyCode::Ctrl => KeyCode::ControlLeft, - dioxus_html::KeyCode::Alt => KeyCode::AltLeft, - dioxus_html::KeyCode::Pause => KeyCode::Pause, - dioxus_html::KeyCode::CapsLock => KeyCode::CapsLock, - dioxus_html::KeyCode::Escape => KeyCode::Escape, - dioxus_html::KeyCode::Space => KeyCode::Space, - dioxus_html::KeyCode::PageUp => KeyCode::PageUp, - dioxus_html::KeyCode::PageDown => KeyCode::PageDown, - dioxus_html::KeyCode::End => KeyCode::End, - dioxus_html::KeyCode::Home => KeyCode::Home, - dioxus_html::KeyCode::LeftArrow => KeyCode::ArrowLeft, - dioxus_html::KeyCode::UpArrow => KeyCode::ArrowUp, - dioxus_html::KeyCode::RightArrow => KeyCode::ArrowRight, - dioxus_html::KeyCode::DownArrow => KeyCode::ArrowDown, - dioxus_html::KeyCode::Insert => KeyCode::Insert, - dioxus_html::KeyCode::Delete => KeyCode::Delete, - dioxus_html::KeyCode::Num0 => KeyCode::Numpad0, - dioxus_html::KeyCode::Num1 => KeyCode::Numpad1, - dioxus_html::KeyCode::Num2 => KeyCode::Numpad2, - dioxus_html::KeyCode::Num3 => KeyCode::Numpad3, - dioxus_html::KeyCode::Num4 => KeyCode::Numpad4, - dioxus_html::KeyCode::Num5 => KeyCode::Numpad5, - dioxus_html::KeyCode::Num6 => KeyCode::Numpad6, - dioxus_html::KeyCode::Num7 => KeyCode::Numpad7, - dioxus_html::KeyCode::Num8 => KeyCode::Numpad8, - dioxus_html::KeyCode::Num9 => KeyCode::Numpad9, - dioxus_html::KeyCode::A => KeyCode::KeyA, - dioxus_html::KeyCode::B => KeyCode::KeyB, - dioxus_html::KeyCode::C => KeyCode::KeyC, - dioxus_html::KeyCode::D => KeyCode::KeyD, - dioxus_html::KeyCode::E => KeyCode::KeyE, - dioxus_html::KeyCode::F => KeyCode::KeyF, - dioxus_html::KeyCode::G => KeyCode::KeyG, - dioxus_html::KeyCode::H => KeyCode::KeyH, - dioxus_html::KeyCode::I => KeyCode::KeyI, - dioxus_html::KeyCode::J => KeyCode::KeyJ, - dioxus_html::KeyCode::K => KeyCode::KeyK, - dioxus_html::KeyCode::L => KeyCode::KeyL, - dioxus_html::KeyCode::M => KeyCode::KeyM, - dioxus_html::KeyCode::N => KeyCode::KeyN, - dioxus_html::KeyCode::O => KeyCode::KeyO, - dioxus_html::KeyCode::P => KeyCode::KeyP, - dioxus_html::KeyCode::Q => KeyCode::KeyQ, - dioxus_html::KeyCode::R => KeyCode::KeyR, - dioxus_html::KeyCode::S => KeyCode::KeyS, - dioxus_html::KeyCode::T => KeyCode::KeyT, - dioxus_html::KeyCode::U => KeyCode::KeyU, - dioxus_html::KeyCode::V => KeyCode::KeyV, - dioxus_html::KeyCode::W => KeyCode::KeyW, - dioxus_html::KeyCode::X => KeyCode::KeyX, - dioxus_html::KeyCode::Y => KeyCode::KeyY, - dioxus_html::KeyCode::Z => KeyCode::KeyZ, - dioxus_html::KeyCode::Numpad0 => KeyCode::Numpad0, - dioxus_html::KeyCode::Numpad1 => KeyCode::Numpad1, - dioxus_html::KeyCode::Numpad2 => KeyCode::Numpad2, - dioxus_html::KeyCode::Numpad3 => KeyCode::Numpad3, - dioxus_html::KeyCode::Numpad4 => KeyCode::Numpad4, - dioxus_html::KeyCode::Numpad5 => KeyCode::Numpad5, - dioxus_html::KeyCode::Numpad6 => KeyCode::Numpad6, - dioxus_html::KeyCode::Numpad7 => KeyCode::Numpad7, - dioxus_html::KeyCode::Numpad8 => KeyCode::Numpad8, - dioxus_html::KeyCode::Numpad9 => KeyCode::Numpad9, - dioxus_html::KeyCode::Multiply => KeyCode::NumpadMultiply, - dioxus_html::KeyCode::Add => KeyCode::NumpadAdd, - dioxus_html::KeyCode::Subtract => KeyCode::NumpadSubtract, - dioxus_html::KeyCode::DecimalPoint => KeyCode::NumpadDecimal, - dioxus_html::KeyCode::Divide => KeyCode::NumpadDivide, - dioxus_html::KeyCode::F1 => KeyCode::F1, - dioxus_html::KeyCode::F2 => KeyCode::F2, - dioxus_html::KeyCode::F3 => KeyCode::F3, - dioxus_html::KeyCode::F4 => KeyCode::F4, - dioxus_html::KeyCode::F5 => KeyCode::F5, - dioxus_html::KeyCode::F6 => KeyCode::F6, - dioxus_html::KeyCode::F7 => KeyCode::F7, - dioxus_html::KeyCode::F8 => KeyCode::F8, - dioxus_html::KeyCode::F9 => KeyCode::F9, - dioxus_html::KeyCode::F10 => KeyCode::F10, - dioxus_html::KeyCode::F11 => KeyCode::F11, - dioxus_html::KeyCode::F12 => KeyCode::F12, - dioxus_html::KeyCode::NumLock => KeyCode::NumLock, - dioxus_html::KeyCode::ScrollLock => KeyCode::ScrollLock, - dioxus_html::KeyCode::Semicolon => KeyCode::Semicolon, - dioxus_html::KeyCode::EqualSign => KeyCode::Equal, - dioxus_html::KeyCode::Comma => KeyCode::Comma, - dioxus_html::KeyCode::Period => KeyCode::Period, - dioxus_html::KeyCode::ForwardSlash => KeyCode::Slash, - dioxus_html::KeyCode::GraveAccent => KeyCode::Backquote, - dioxus_html::KeyCode::OpenBracket => KeyCode::BracketLeft, - dioxus_html::KeyCode::BackSlash => KeyCode::Backslash, - dioxus_html::KeyCode::CloseBraket => KeyCode::BracketRight, - dioxus_html::KeyCode::SingleQuote => KeyCode::Quote, + dioxus_html::KeyCode::Backspace => Code::Backspace, + dioxus_html::KeyCode::Tab => Code::Tab, + dioxus_html::KeyCode::Clear => Code::NumpadClear, + dioxus_html::KeyCode::Enter => Code::Enter, + dioxus_html::KeyCode::Shift => Code::ShiftLeft, + dioxus_html::KeyCode::Ctrl => Code::ControlLeft, + dioxus_html::KeyCode::Alt => Code::AltLeft, + dioxus_html::KeyCode::Pause => Code::Pause, + dioxus_html::KeyCode::CapsLock => Code::CapsLock, + dioxus_html::KeyCode::Escape => Code::Escape, + dioxus_html::KeyCode::Space => Code::Space, + dioxus_html::KeyCode::PageUp => Code::PageUp, + dioxus_html::KeyCode::PageDown => Code::PageDown, + dioxus_html::KeyCode::End => Code::End, + dioxus_html::KeyCode::Home => Code::Home, + dioxus_html::KeyCode::LeftArrow => Code::ArrowLeft, + dioxus_html::KeyCode::UpArrow => Code::ArrowUp, + dioxus_html::KeyCode::RightArrow => Code::ArrowRight, + dioxus_html::KeyCode::DownArrow => Code::ArrowDown, + dioxus_html::KeyCode::Insert => Code::Insert, + dioxus_html::KeyCode::Delete => Code::Delete, + dioxus_html::KeyCode::Num0 => Code::Numpad0, + dioxus_html::KeyCode::Num1 => Code::Numpad1, + dioxus_html::KeyCode::Num2 => Code::Numpad2, + dioxus_html::KeyCode::Num3 => Code::Numpad3, + dioxus_html::KeyCode::Num4 => Code::Numpad4, + dioxus_html::KeyCode::Num5 => Code::Numpad5, + dioxus_html::KeyCode::Num6 => Code::Numpad6, + dioxus_html::KeyCode::Num7 => Code::Numpad7, + dioxus_html::KeyCode::Num8 => Code::Numpad8, + dioxus_html::KeyCode::Num9 => Code::Numpad9, + dioxus_html::KeyCode::A => Code::KeyA, + dioxus_html::KeyCode::B => Code::KeyB, + dioxus_html::KeyCode::C => Code::KeyC, + dioxus_html::KeyCode::D => Code::KeyD, + dioxus_html::KeyCode::E => Code::KeyE, + dioxus_html::KeyCode::F => Code::KeyF, + dioxus_html::KeyCode::G => Code::KeyG, + dioxus_html::KeyCode::H => Code::KeyH, + dioxus_html::KeyCode::I => Code::KeyI, + dioxus_html::KeyCode::J => Code::KeyJ, + dioxus_html::KeyCode::K => Code::KeyK, + dioxus_html::KeyCode::L => Code::KeyL, + dioxus_html::KeyCode::M => Code::KeyM, + dioxus_html::KeyCode::N => Code::KeyN, + dioxus_html::KeyCode::O => Code::KeyO, + dioxus_html::KeyCode::P => Code::KeyP, + dioxus_html::KeyCode::Q => Code::KeyQ, + dioxus_html::KeyCode::R => Code::KeyR, + dioxus_html::KeyCode::S => Code::KeyS, + dioxus_html::KeyCode::T => Code::KeyT, + dioxus_html::KeyCode::U => Code::KeyU, + dioxus_html::KeyCode::V => Code::KeyV, + dioxus_html::KeyCode::W => Code::KeyW, + dioxus_html::KeyCode::X => Code::KeyX, + dioxus_html::KeyCode::Y => Code::KeyY, + dioxus_html::KeyCode::Z => Code::KeyZ, + dioxus_html::KeyCode::Numpad0 => Code::Numpad0, + dioxus_html::KeyCode::Numpad1 => Code::Numpad1, + dioxus_html::KeyCode::Numpad2 => Code::Numpad2, + dioxus_html::KeyCode::Numpad3 => Code::Numpad3, + dioxus_html::KeyCode::Numpad4 => Code::Numpad4, + dioxus_html::KeyCode::Numpad5 => Code::Numpad5, + dioxus_html::KeyCode::Numpad6 => Code::Numpad6, + dioxus_html::KeyCode::Numpad7 => Code::Numpad7, + dioxus_html::KeyCode::Numpad8 => Code::Numpad8, + dioxus_html::KeyCode::Numpad9 => Code::Numpad9, + dioxus_html::KeyCode::Multiply => Code::NumpadMultiply, + dioxus_html::KeyCode::Add => Code::NumpadAdd, + dioxus_html::KeyCode::Subtract => Code::NumpadSubtract, + dioxus_html::KeyCode::DecimalPoint => Code::NumpadDecimal, + dioxus_html::KeyCode::Divide => Code::NumpadDivide, + dioxus_html::KeyCode::F1 => Code::F1, + dioxus_html::KeyCode::F2 => Code::F2, + dioxus_html::KeyCode::F3 => Code::F3, + dioxus_html::KeyCode::F4 => Code::F4, + dioxus_html::KeyCode::F5 => Code::F5, + dioxus_html::KeyCode::F6 => Code::F6, + dioxus_html::KeyCode::F7 => Code::F7, + dioxus_html::KeyCode::F8 => Code::F8, + dioxus_html::KeyCode::F9 => Code::F9, + dioxus_html::KeyCode::F10 => Code::F10, + dioxus_html::KeyCode::F11 => Code::F11, + dioxus_html::KeyCode::F12 => Code::F12, + dioxus_html::KeyCode::NumLock => Code::NumLock, + dioxus_html::KeyCode::ScrollLock => Code::ScrollLock, + dioxus_html::KeyCode::Semicolon => Code::Semicolon, + dioxus_html::KeyCode::EqualSign => Code::Equal, + dioxus_html::KeyCode::Comma => Code::Comma, + dioxus_html::KeyCode::Period => Code::Period, + dioxus_html::KeyCode::ForwardSlash => Code::Slash, + dioxus_html::KeyCode::GraveAccent => Code::Backquote, + dioxus_html::KeyCode::OpenBracket => Code::BracketLeft, + dioxus_html::KeyCode::BackSlash => Code::Backslash, + dioxus_html::KeyCode::CloseBraket => Code::BracketRight, + dioxus_html::KeyCode::SingleQuote => Code::Quote, key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key), } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 6e6d26b17..024e040a8 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,24 +1,31 @@ use crate::desktop_context::EventData; -use crate::protocol; +use crate::protocol::{self, AssetHandlerRegistry}; use crate::{desktop_context::UserWindowEvent, Config}; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use wry; pub use wry::application as tao; +use wry::application::menu::{MenuBar, MenuItem}; use wry::application::window::Window; +use wry::http::Response; use wry::webview::{WebContext, WebView, WebViewBuilder}; pub fn build( cfg: &mut Config, event_loop: &EventLoopWindowTarget, proxy: EventLoopProxy, -) -> (WebView, WebContext) { - let builder = cfg.window.clone(); +) -> (WebView, WebContext, AssetHandlerRegistry) { let window = builder.with_visible(false).build(event_loop).unwrap(); let file_handler = cfg.file_drop_handler.take(); let custom_head = cfg.custom_head.clone(); let index_file = cfg.custom_index.clone(); let root_name = cfg.root_name.clone(); + if cfg.enable_default_menu_bar { + builder = builder.with_menu(build_default_menu_bar()); + } + + let window = builder.with_visible(false).build(event_loop).unwrap(); + // We assume that if the icon is None in cfg, then the user just didnt set it if cfg.window.window.window_icon.is_none() { window.set_window_icon(Some( @@ -32,6 +39,8 @@ pub fn build( } let mut web_context = WebContext::new(cfg.data_dir.clone()); + let asset_handlers = AssetHandlerRegistry::new(); + let asset_handlers_ref = asset_handlers.clone(); let mut webview = WebViewBuilder::new(window) .unwrap() @@ -44,8 +53,22 @@ pub fn build( _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id())); } }) - .with_custom_protocol(String::from("dioxus"), move |r| { - protocol::desktop_handler(r, custom_head.clone(), index_file.clone(), &root_name) + .with_asynchronous_custom_protocol(String::from("dioxus"), move |request, responder| { + let custom_head = custom_head.clone(); + let index_file = index_file.clone(); + let root_name = root_name.clone(); + let asset_handlers_ref = asset_handlers_ref.clone(); + tokio::spawn(async move { + let response_res = protocol::desktop_handler( + request, + custom_head.clone(), + index_file.clone(), + &root_name, + &asset_handlers_ref, + ) + .await; + responder.respond(response); + }); }) .with_file_drop_handler(move |window, evet| { file_handler @@ -71,7 +94,16 @@ pub fn build( // .with_web_context(&mut web_context); for (name, handler) in cfg.protocols.drain(..) { - webview = webview.with_custom_protocol(name, handler) + webview = webview.with_custom_protocol(name, move |r| match handler(&r) { + Ok(response) => response, + Err(err) => { + tracing::error!("Error: {}", err); + Response::builder() + .status(500) + .body(err.to_string().into_bytes().into()) + .unwrap() + } + }) } if cfg.disable_context_menu { @@ -94,5 +126,63 @@ pub fn build( webview = webview.with_devtools(true); } - (webview.build().unwrap(), web_context) + (webview.build().unwrap(), web_context, asset_handlers) +} + +/// Builds a standard menu bar depending on the users platform. It may be used as a starting point +/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder). +/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste. +/// > The menu bar differs per platform because of constraints introduced +/// > by [`MenuItem`](tao::menu::MenuItem). +pub fn build_default_menu_bar() -> MenuBar { + let mut menu_bar = MenuBar::new(); + + // since it is uncommon on windows to have an "application menu" + // we add a "window" menu to be more consistent across platforms with the standard menu + let mut window_menu = MenuBar::new(); + #[cfg(target_os = "macos")] + { + window_menu.add_native_item(MenuItem::EnterFullScreen); + window_menu.add_native_item(MenuItem::Zoom); + window_menu.add_native_item(MenuItem::Separator); + } + + window_menu.add_native_item(MenuItem::Hide); + + #[cfg(target_os = "macos")] + { + window_menu.add_native_item(MenuItem::HideOthers); + window_menu.add_native_item(MenuItem::ShowAll); + } + + window_menu.add_native_item(MenuItem::Minimize); + window_menu.add_native_item(MenuItem::CloseWindow); + window_menu.add_native_item(MenuItem::Separator); + window_menu.add_native_item(MenuItem::Quit); + menu_bar.add_submenu("Window", true, window_menu); + + // since tao supports none of the below items on linux we should only add them on macos/windows + #[cfg(not(target_os = "linux"))] + { + let mut edit_menu = MenuBar::new(); + #[cfg(target_os = "macos")] + { + edit_menu.add_native_item(MenuItem::Undo); + edit_menu.add_native_item(MenuItem::Redo); + edit_menu.add_native_item(MenuItem::Separator); + } + + edit_menu.add_native_item(MenuItem::Cut); + edit_menu.add_native_item(MenuItem::Copy); + edit_menu.add_native_item(MenuItem::Paste); + + #[cfg(target_os = "macos")] + { + edit_menu.add_native_item(MenuItem::Separator); + edit_menu.add_native_item(MenuItem::SelectAll); + } + menu_bar.add_submenu("Edit", true, edit_menu); + } + + menu_bar } diff --git a/packages/dioxus-tui/examples/all_terminal_events.rs b/packages/dioxus-tui/examples/all_terminal_events.rs index 483e9afdf..004578807 100644 --- a/packages/dioxus-tui/examples/all_terminal_events.rs +++ b/packages/dioxus-tui/examples/all_terminal_events.rs @@ -37,7 +37,7 @@ fn app(cx: Scope) -> Element { // todo: remove let mut trimmed = format!("{event:?}"); trimmed.truncate(200); - rsx!(p { "{trimmed}" }) + rsx!( p { "{trimmed}" } ) }); let log_event = move |event: Event| { @@ -45,10 +45,7 @@ fn app(cx: Scope) -> Element { }; cx.render(rsx! { - div { - width: "100%", - height: "100%", - flex_direction: "column", + div { width: "100%", height: "100%", flex_direction: "column", div { width: "80%", height: "50%", @@ -59,7 +56,7 @@ fn app(cx: Scope) -> Element { onmousemove: move |event| log_event(Event::MouseMove(event.inner().clone())), onclick: move |event| log_event(Event::MouseClick(event.inner().clone())), - ondblclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())), + ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())), onmousedown: move |event| log_event(Event::MouseDown(event.inner().clone())), onmouseup: move |event| log_event(Event::MouseUp(event.inner().clone())), @@ -73,13 +70,8 @@ fn app(cx: Scope) -> Element { onfocusout: move |event| log_event(Event::FocusOut(event.inner().clone())), "Hover, click, type or scroll to see the info down below" - }, - div { - width: "80%", - height: "50%", - flex_direction: "column", - events_rendered, - }, - }, + } + div { width: "80%", height: "50%", flex_direction: "column", events_rendered } + } }) } diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 8de27a9b1..2f3eca420 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -26,7 +26,7 @@ tower = { version = "0.4.13", features = ["util"], optional = true } axum-macros = "0.3.7" # salvo -salvo = { version = "0.46.0", optional = true, features = ["serve-static", "websocket", "compression"] } +salvo = { version = "0.63.0", optional = true, features = ["serve-static", "websocket", "compression"] } serde = "1.0.159" # Dioxus + SSR diff --git a/packages/fullstack/examples/salvo-hello-world/Cargo.toml b/packages/fullstack/examples/salvo-hello-world/Cargo.toml index ca0d85ecf..3d4e74c5b 100644 --- a/packages/fullstack/examples/salvo-hello-world/Cargo.toml +++ b/packages/fullstack/examples/salvo-hello-world/Cargo.toml @@ -12,7 +12,7 @@ dioxus = { workspace = true } dioxus-fullstack = { workspace = true } tokio = { workspace = true, features = ["full"], optional = true } serde = "1.0.159" -salvo = { version = "0.37.9", optional = true } +salvo = { version = "0.63.0", optional = true } execute = "0.2.12" reqwest = "0.11.18" simple_logger = "4.2.0" diff --git a/packages/generational-box/src/lib.rs b/packages/generational-box/src/lib.rs index f56904ede..57a02a5d7 100644 --- a/packages/generational-box/src/lib.rs +++ b/packages/generational-box/src/lib.rs @@ -700,14 +700,16 @@ impl Owner { /// Creates an invalid handle. This is useful for creating a handle that will be filled in later. If you use this before the value is filled in, you will get may get a panic or an out of date value. pub fn invalid(&self) -> GenerationalBox { let location = self.store.claim(); - GenerationalBox { + let key = GenerationalBox { raw: location, #[cfg(any(debug_assertions, feature = "check_generation"))] generation: location.0.generation.get(), #[cfg(any(debug_assertions, feature = "debug_ownership"))] created_at: std::panic::Location::caller(), _marker: PhantomData, - } + }; + self.owned.borrow_mut().push(location); + key } } diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 5644b440c..d53a16183 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -21,7 +21,7 @@ keyboard-types = "0.7" async-trait = "0.1.58" serde-value = "0.7.0" tokio = { workspace = true, features = ["fs", "io-util"], optional = true } -rfd = { version = "0.11.3", optional = true } +rfd = { version = "0.12", optional = true } async-channel = "1.8.0" serde_json = { version = "1", optional = true } @@ -68,3 +68,4 @@ mounted = [ wasm-bind = ["web-sys", "wasm-bindgen"] native-bind = ["tokio"] hot-reload-context = ["dioxus-rsx"] +html-to-rsx = [] diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index a42c01950..ca3f4aee7 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -79,6 +79,25 @@ macro_rules! impl_attribute_match { }; } +#[cfg(feature = "html-to-rsx")] +macro_rules! impl_html_to_rsx_attribute_match { + ( + $attr:ident $fil:ident $name:literal + ) => { + if $attr == $name { + return Some(stringify!($fil)); + } + }; + + ( + $attr:ident $fil:ident $_:tt + ) => { + if $attr == stringify!($fil) { + return Some(stringify!($fil)); + } + }; +} + macro_rules! impl_element { ( $(#[$attr:meta])* @@ -316,6 +335,38 @@ macro_rules! builder_constructors { } } + #[cfg(feature = "html-to-rsx")] + pub fn map_html_attribute_to_rsx(html: &str) -> Option<&'static str> { + $( + $( + impl_html_to_rsx_attribute_match!( + html $fil $extra + ); + )* + )* + + if let Some(name) = crate::map_html_global_attributes_to_rsx(html) { + return Some(name); + } + + if let Some(name) = crate::map_html_svg_attributes_to_rsx(html) { + return Some(name); + } + + None + } + + #[cfg(feature = "html-to-rsx")] + pub fn map_html_element_to_rsx(html: &str) -> Option<&'static str> { + $( + if html == stringify!($name) { + return Some(stringify!($name)); + } + )* + + None + } + $( impl_element!( $(#[$attr])* @@ -998,9 +1049,8 @@ builder_constructors! { src: Uri DEFAULT, text: String DEFAULT, - // r#async: Bool, - // r#type: String, // TODO could be an enum - r#type: String "type", + r#async: Bool "async", + r#type: String "type", // TODO could be an enum r#script: String "script", }; diff --git a/packages/html/src/events/mouse.rs b/packages/html/src/events/mouse.rs index 55d081900..17128f4e4 100644 --- a/packages/html/src/events/mouse.rs +++ b/packages/html/src/events/mouse.rs @@ -119,10 +119,7 @@ impl_event! { /// oncontextmenu oncontextmenu - /// ondoubleclick - ondoubleclick - - /// ondoubleclick + #[deprecated(since = "0.5.0", note = "use ondoubleclick instead")] ondblclick /// onmousedown @@ -149,6 +146,22 @@ impl_event! { onmouseup } +/// ondoubleclick +#[inline] +pub fn ondoubleclick<'a, E: crate::EventReturn, T>( + _cx: &'a ::dioxus_core::ScopeState, + mut _f: impl FnMut(::dioxus_core::Event) -> E + 'a, +) -> ::dioxus_core::Attribute<'a> { + ::dioxus_core::Attribute::new( + "ondblclick", + _cx.listener(move |e: ::dioxus_core::Event| { + _f(e).spawn(_cx); + }), + None, + false, + ) +} + impl MouseData { /// Construct MouseData with the specified properties /// diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 02b0b32ed..639026b33 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -33,12 +33,44 @@ macro_rules! trait_method_mapping { }; } +#[cfg(feature = "html-to-rsx")] +macro_rules! html_to_rsx_attribute_mapping { + ( + $matching:ident; + $(#[$attr:meta])* + $name:ident; + ) => { + if $matching == stringify!($name) { + return Some(stringify!($name)); + } + }; + ( + $matching:ident; + $(#[$attr:meta])* + $name:ident: $lit:literal; + ) => { + if $matching == stringify!($lit) { + return Some(stringify!($name)); + } + }; + ( + $matching:ident; + $(#[$attr:meta])* + $name:ident: $lit:literal, $ns:literal; + ) => { + if $matching == stringify!($lit) { + return Some(stringify!($name)); + } + }; +} + macro_rules! trait_methods { ( @base $(#[$trait_attr:meta])* $trait:ident; $fn:ident; + $fn_html_to_rsx:ident; $( $(#[$attr:meta])* $name:ident $(: $($arg:literal),*)*; @@ -62,6 +94,18 @@ macro_rules! trait_methods { )* None } + + #[cfg(feature = "html-to-rsx")] + #[doc = "Converts an HTML attribute to an RSX attribute"] + pub(crate) fn $fn_html_to_rsx(html: &str) -> Option<&'static str> { + $( + html_to_rsx_attribute_mapping! { + html; + $name$(: $($arg),*)*; + } + )* + None + } }; // Rename the incoming ident and apply a custom namespace @@ -79,6 +123,7 @@ trait_methods! { GlobalAttributes; map_global_attributes; + map_html_global_attributes_to_rsx; /// Prevent the default action for this element. /// @@ -1593,6 +1638,7 @@ trait_methods! { @base SvgAttributes; map_svg_attributes; + map_html_svg_attributes_to_rsx; /// Prevent the default action for this element. /// diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 6dd7c42bd..c44777801 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -19,6 +19,8 @@ mod elements; #[cfg(feature = "hot-reload-context")] pub use elements::HtmlCtx; +#[cfg(feature = "html-to-rsx")] +pub use elements::{map_html_attribute_to_rsx, map_html_element_to_rsx}; pub mod events; pub mod geometry; mod global_attributes; diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index cbd7b75f5..83504af43 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -34,10 +34,14 @@ warp = { version = "0.3.3", optional = true } axum = { version = "0.6.1", optional = true, features = ["ws"] } # salvo -salvo = { version = "0.44.1", optional = true, features = ["ws"] } +salvo = { version = "0.63.0", optional = true, features = ["websocket"] } once_cell = "1.17.1" async-trait = "0.1.71" +# rocket +rocket = { version = "0.5.0", optional = true } +rocket_ws = { version = "0.1.0", optional = true } + # actix is ... complicated? # actix-files = { version = "0.6.2", optional = true } # actix-web = { version = "4.2.1", optional = true } @@ -49,13 +53,16 @@ tokio = { workspace = true, features = ["full"] } dioxus = { workspace = true } warp = "0.3.3" axum = { version = "0.6.1", features = ["ws"] } -salvo = { version = "0.44.1", features = ["affix", "ws"] } +salvo = { version = "0.63.0", features = ["affix", "websocket"] } +rocket = "0.5.0" +rocket_ws = "0.1.0" tower = "0.4.13" [features] default = ["hot-reload"] # actix = ["actix-files", "actix-web", "actix-ws"] hot-reload = ["dioxus-hot-reload"] +rocket = ["dep:rocket", "dep:rocket_ws"] [[example]] name = "axum" @@ -68,3 +75,7 @@ required-features = ["salvo"] [[example]] name = "warp" required-features = ["warp"] + +[[example]] +name = "rocket" +required-features = ["rocket"] diff --git a/packages/liveview/README.md b/packages/liveview/README.md index cbae3c53a..13af3c36f 100644 --- a/packages/liveview/README.md +++ b/packages/liveview/README.md @@ -28,6 +28,7 @@ The current backend frameworks supported include: - Axum - Warp - Salvo +- Rocket Dioxus-LiveView exports some primitives to wire up an app into an existing backend framework. diff --git a/packages/liveview/examples/rocket.rs b/packages/liveview/examples/rocket.rs new file mode 100644 index 000000000..8256db7f8 --- /dev/null +++ b/packages/liveview/examples/rocket.rs @@ -0,0 +1,76 @@ +#[macro_use] +extern crate rocket; + +use dioxus::prelude::*; +use dioxus_liveview::LiveViewPool; +use rocket::response::content::RawHtml; +use rocket::{Config, Rocket, State}; +use rocket_ws::{Channel, WebSocket}; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello Rocket! {num}" + button { onclick: move |_| num += 1, "Increment" } + } + }) +} + +fn index_page_with_glue(glue: &str) -> RawHtml { + RawHtml(format!( + r#" + + + Dioxus LiveView with Rocket +

+ {glue} + + "#, + glue = glue + )) +} + +#[get("/")] +async fn index(config: &Config) -> RawHtml { + index_page_with_glue(&dioxus_liveview::interpreter_glue(&format!( + "ws://{addr}:{port}/ws", + addr = config.address, + port = config.port, + ))) +} + +#[get("/as-path")] +async fn as_path() -> RawHtml { + index_page_with_glue(&dioxus_liveview::interpreter_glue("/ws")) +} + +#[get("/ws")] +fn ws(ws: WebSocket, pool: &State) -> Channel<'static> { + let pool = pool.inner().to_owned(); + + ws.channel(move |stream| { + Box::pin(async move { + let _ = pool + .launch(dioxus_liveview::rocket_socket(stream), app) + .await; + Ok(()) + }) + }) +} + +#[tokio::main] +async fn main() { + let view = dioxus_liveview::LiveViewPool::new(); + + Rocket::build() + .manage(view) + .mount("/", routes![index, as_path, ws]) + .ignite() + .await + .expect("Failed to ignite rocket") + .launch() + .await + .expect("Failed to launch rocket"); +} diff --git a/packages/liveview/src/adapters/rocket_adapter.rs b/packages/liveview/src/adapters/rocket_adapter.rs new file mode 100644 index 000000000..8a9840bf2 --- /dev/null +++ b/packages/liveview/src/adapters/rocket_adapter.rs @@ -0,0 +1,25 @@ +use crate::{LiveViewError, LiveViewSocket}; +use rocket::futures::{SinkExt, StreamExt}; +use rocket_ws::{result::Error, stream::DuplexStream, Message}; + +/// Convert a rocket websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the rocket web framework +pub fn rocket_socket(stream: DuplexStream) -> impl LiveViewSocket { + stream + .map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) +} + +fn transform_rx(message: Result) -> Result, LiveViewError> { + message + .map_err(|_| LiveViewError::SendingFailed)? + .into_text() + .map(|s| s.into_bytes()) + .map_err(|_| LiveViewError::SendingFailed) +} + +async fn transform_tx(message: Vec) -> Result { + Ok(Message::Text(String::from_utf8_lossy(&message).to_string())) +} diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 6a2768fd5..40d6186dc 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -18,6 +18,11 @@ pub mod adapters { #[cfg(feature = "salvo")] pub use salvo_adapter::*; + + #[cfg(feature = "rocket")] + pub mod rocket_adapter; + #[cfg(feature = "rocket")] + pub use rocket_adapter::*; } pub use adapters::*; diff --git a/packages/rink/src/layout.rs b/packages/rink/src/layout.rs index dbdadac3f..72c8f68b2 100644 --- a/packages/rink/src/layout.rs +++ b/packages/rink/src/layout.rs @@ -92,10 +92,10 @@ impl State for TaffyLayout { attribute, value, .. } in attributes { - if let Some(text) = value.as_text() { + if value.as_custom().is_none() { apply_layout_attributes_cfg( &attribute.name, - text, + &value.to_string(), &mut style, &LayoutConfigeration { border_widths: BorderWidths { diff --git a/packages/rsx-rosetta/Cargo.toml b/packages/rsx-rosetta/Cargo.toml index ab5d4e882..9cd53b02f 100644 --- a/packages/rsx-rosetta/Cargo.toml +++ b/packages/rsx-rosetta/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-autofmt = { workspace = true } dioxus-rsx = { workspace = true } +dioxus-html = { workspace = true, features = ["html-to-rsx"]} html_parser.workspace = true proc-macro2 = "1.0.49" quote = "1.0.23" diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 3c11f3a90..49b6b6fc7 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -3,6 +3,7 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] use convert_case::{Case, Casing}; +use dioxus_html::{map_html_attribute_to_rsx, map_html_element_to_rsx}; use dioxus_rsx::{ BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput, }; @@ -24,26 +25,41 @@ pub fn rsx_node_from_html(node: &Node) -> Option { match node { Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))), Node::Element(el) => { - let el_name = el.name.to_case(Case::Snake); - let el_name = ElementName::Ident(Ident::new(el_name.as_str(), Span::call_site())); + let el_name = if let Some(name) = map_html_element_to_rsx(&el.name) { + ElementName::Ident(Ident::new(name, Span::call_site())) + } else { + // if we don't recognize it and it has a dash, we assume it's a web component + if el.name.contains('-') { + ElementName::Custom(LitStr::new(&el.name, Span::call_site())) + } else { + // otherwise, it might be an element that isn't supported yet + ElementName::Ident(Ident::new(&el.name.to_case(Case::Snake), Span::call_site())) + } + }; let mut attributes: Vec<_> = el .attributes .iter() .map(|(name, value)| { - let ident = if matches!(name.as_str(), "for" | "async" | "type" | "as") { - Ident::new_raw(name.as_str(), Span::call_site()) + let value = ifmt_from_text(value.as_deref().unwrap_or("false")); + let attr = if let Some(name) = map_html_attribute_to_rsx(name) { + let ident = if let Some(name) = name.strip_prefix("r#") { + Ident::new_raw(name, Span::call_site()) + } else { + Ident::new(name, Span::call_site()) + }; + ElementAttr::AttrText { value, name: ident } } else { - let new_name = name.to_case(Case::Snake); - Ident::new(new_name.as_str(), Span::call_site()) + // If we don't recognize the attribute, we assume it's a custom attribute + ElementAttr::CustomAttrText { + value, + name: LitStr::new(name, Span::call_site()), + } }; ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrText { - value: ifmt_from_text(value.as_deref().unwrap_or("false")), - name: ident, - }, + attr, } }) .collect(); diff --git a/packages/rsx-rosetta/tests/h-tags.rs b/packages/rsx-rosetta/tests/h-tags.rs new file mode 100644 index 000000000..eb179c57f --- /dev/null +++ b/packages/rsx-rosetta/tests/h-tags.rs @@ -0,0 +1,33 @@ +use html_parser::Dom; + +#[test] +fn h_tags_translate() { + let html = r#" +
+

hello world!

+

hello world!

+

hello world!

+

hello world!

+
hello world!
+
hello world!
+
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::rsx_from_html(&dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + let expected = r#" + div { + h1 { "hello world!" } + h2 { "hello world!" } + h3 { "hello world!" } + h4 { "hello world!" } + h5 { "hello world!" } + h6 { "hello world!" } + }"#; + pretty_assertions::assert_eq!(&out, &expected); +} diff --git a/packages/rsx-rosetta/tests/raw.rs b/packages/rsx-rosetta/tests/raw.rs new file mode 100644 index 000000000..a3ed1eef8 --- /dev/null +++ b/packages/rsx-rosetta/tests/raw.rs @@ -0,0 +1,21 @@ +use html_parser::Dom; + +#[test] +fn raw_attribute() { + let html = r#" +
+
hello world!
+
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::rsx_from_html(&dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + let expected = r#" + div { div { "unrecognizedattribute": "asd", "hello world!" } }"#; + pretty_assertions::assert_eq!(&out, &expected); +} diff --git a/packages/rsx-rosetta/tests/simple.rs b/packages/rsx-rosetta/tests/simple.rs index b8fd095e1..3f3f8230a 100644 --- a/packages/rsx-rosetta/tests/simple.rs +++ b/packages/rsx-rosetta/tests/simple.rs @@ -9,8 +9,6 @@ fn simple_elements() {
hello world!
hello world!
hello world!
-
hello world!
- hello world! "# .trim(); @@ -28,8 +26,6 @@ fn simple_elements() { div { id: "asd", "hello world!" } div { r#for: "asd", "hello world!" } div { r#async: "asd", "hello world!" } - div { large_thing: "asd", "hello world!" } - ai_is_awesome { "hello world!" } }"#; pretty_assertions::assert_eq!(&out, &expected); } diff --git a/packages/rsx-rosetta/tests/web-component.rs b/packages/rsx-rosetta/tests/web-component.rs new file mode 100644 index 000000000..c7309e1b7 --- /dev/null +++ b/packages/rsx-rosetta/tests/web-component.rs @@ -0,0 +1,21 @@ +use html_parser::Dom; + +#[test] +fn web_components_translate() { + let html = r#" +
+ +
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::rsx_from_html(&dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + let expected = r#" + div { my-component {} }"#; + pretty_assertions::assert_eq!(&out, &expected); +} diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 9c4cd68e9..af188f907 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -204,12 +204,17 @@ impl<'a> ToTokens for TemplateRenderer<'a> { None => quote! { None }, }; - let spndbg = format!("{:?}", self.roots[0].span()); - let root_col = spndbg - .rsplit_once("..") - .and_then(|(_, after)| after.split_once(')').map(|(before, _)| before)) - .unwrap_or_default(); - + let root_col = match self.roots.first() { + Some(first_root) => { + let first_root_span = format!("{:?}", first_root.span()); + first_root_span + .rsplit_once("..") + .and_then(|(_, after)| after.split_once(')').map(|(before, _)| before)) + .unwrap_or_default() + .to_string() + } + _ => "0".to_string(), + }; let root_printer = self.roots.iter().enumerate().map(|(idx, root)| { context.current_path.push(idx as u8); let out = context.render_static_node(root); diff --git a/packages/server-macro/src/lib.rs b/packages/server-macro/src/lib.rs index 5ad9a682b..871de33bc 100644 --- a/packages/server-macro/src/lib.rs +++ b/packages/server-macro/src/lib.rs @@ -19,7 +19,7 @@ use syn::{ /// are enabled), it will instead make a network request to the server. /// /// You can specify one, two, or three arguments to the server function: -/// 1. **Required**: A type name that will be used to identify and register the server function +/// 1. *Optional*: A type name that will be used to identify and register the server function /// (e.g., `MyServerFn`). /// 2. *Optional*: A URL prefix at which the function will be mounted when itโ€™s registered /// (e.g., `"/api"`). Defaults to `"/"`. diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 1dc4bd36e..946a6c380 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -495,3 +495,9 @@ impl Deref for ReadOnlySignal { reference_to_closure as &Self::Target } } + +impl From> for ReadOnlySignal { + fn from(signal: Signal) -> Self { + Self::new(signal) + } +} diff --git a/packages/ssr/src/incremental_cfg.rs b/packages/ssr/src/incremental_cfg.rs index 1a4ea3418..15a6ad44e 100644 --- a/packages/ssr/src/incremental_cfg.rs +++ b/packages/ssr/src/incremental_cfg.rs @@ -67,6 +67,7 @@ pub struct IncrementalRendererConfig { memory_cache_limit: usize, invalidate_after: Option, map_path: Option, + clear_cache: bool, } impl Default for IncrementalRendererConfig { @@ -83,9 +84,16 @@ impl IncrementalRendererConfig { memory_cache_limit: 10000, invalidate_after: None, map_path: None, + clear_cache: true, } } + /// Clear the cache on startup (default: true) + pub fn clear_cache(mut self, clear_cache: bool) -> Self { + self.clear_cache = clear_cache; + self + } + /// Set a mapping from the route to the file path. This will override the default mapping configured with `static_dir`. /// The function should return the path to the folder to store the index.html file in. pub fn map_path PathBuf + Send + Sync + 'static>(mut self, map_path: F) -> Self { @@ -114,7 +122,7 @@ impl IncrementalRendererConfig { /// Build the incremental renderer. pub fn build(self) -> IncrementalRenderer { let static_dir = self.static_dir.clone(); - IncrementalRenderer { + let mut renderer = IncrementalRenderer { static_dir: self.static_dir.clone(), memory_cache: NonZeroUsize::new(self.memory_cache_limit) .map(|limit| lru::LruCache::with_hasher(limit, Default::default())), @@ -129,6 +137,12 @@ impl IncrementalRendererConfig { path }) }), + }; + + if self.clear_cache { + renderer.invalidate_all(); } + + renderer } } diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 98afef8b7..c0f9b6c75 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -238,6 +238,94 @@ fn to_string_works() { assert_eq!(out, "
Hello world 1 -->123<-- Hello world 2
nest 1
nest 2
</diiiiiiiiv>
finalize 0
finalize 1
finalize 2
finalize 3
finalize 4
"); } +#[test] +fn empty_for_loop_works() { + use dioxus::prelude::*; + + fn app(cx: Scope) -> Element { + render! { + div { class: "asdasdasd", + for _ in (0..5) { + + } + } + } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + let mut renderer = Renderer::new(); + let out = renderer.render(&dom); + + for item in renderer.template_cache.iter() { + if item.1.segments.len() > 5 { + assert_eq!( + item.1.segments, + vec![ + PreRendered("
".into()), + InnerHtmlMarker, + PreRendered("
".into(),), + ] + ); + } + } + + use Segment::*; + + assert_eq!(out, "
"); +} + +#[test] +fn empty_render_works() { + use dioxus::prelude::*; + + fn app(cx: Scope) -> Element { + render! {} + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + let mut renderer = Renderer::new(); + let out = renderer.render(&dom); + + for item in renderer.template_cache.iter() { + if item.1.segments.len() > 5 { + assert_eq!(item.1.segments, vec![]); + } + } + assert_eq!(out, ""); +} + +#[test] +fn empty_rsx_works() { + use dioxus::prelude::*; + + fn app(_: Scope) -> Element { + rsx! {}; + None + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + let mut renderer = Renderer::new(); + let out = renderer.render(&dom); + + for item in renderer.template_cache.iter() { + if item.1.segments.len() > 5 { + assert_eq!(item.1.segments, vec![]); + } + } + assert_eq!(out, ""); +} + pub(crate) const BOOL_ATTRS: &[&str] = &[ "allowfullscreen", "allowpaymentrequest", diff --git a/packages/web/src/cache.rs b/packages/web/src/cache.rs index e17203a65..bfd0fd197 100644 --- a/packages/web/src/cache.rs +++ b/packages/web/src/cache.rs @@ -190,7 +190,7 @@ pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[ "oncopy", "oncuechange", "oncut", - "ondblclick", + "ondoubleclick", "ondrag", "ondragend", "ondragenter", diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index b8e9255f7..22701bf4e 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -256,17 +256,21 @@ impl WebsysDom { i.flush(); for id in to_mount { - let node = get_node(id.0 as u32); - if let Some(element) = node.dyn_ref::() { - let data: MountedData = element.into(); - let data = Rc::new(data); - let _ = self.event_channel.unbounded_send(UiEvent { - name: "mounted".to_string(), - bubbles: false, - element: id, - data, - }); - } + self.send_mount_event(id); + } + } + + pub(crate) fn send_mount_event(&self, id: ElementId) { + let node = get_node(id.0 as u32); + if let Some(element) = node.dyn_ref::() { + let data: MountedData = element.into(); + let data = Rc::new(data); + let _ = self.event_channel.unbounded_send(UiEvent { + name: "mounted".to_string(), + bubbles: false, + element: id, + data, + }); } } } diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 4415ceb37..11c68f89c 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -125,6 +125,7 @@ impl WebsysDom { children, attrs, .. } => { let mut mounted_id = None; + let mut should_send_mount_event = true; for attr in *attrs { if let dioxus_core::TemplateAttribute::Dynamic { id } = attr { let attribute = &vnode.dynamic_attrs[*id]; @@ -134,16 +135,24 @@ impl WebsysDom { let name = attribute.name; if let AttributeValue::Listener(_) = value { let event_name = &name[2..]; - self.interpreter.new_event_listener( - event_name, - id.0 as u32, - event_bubbles(event_name) as u8, - ); + match event_name { + "mounted" => should_send_mount_event = true, + _ => { + self.interpreter.new_event_listener( + event_name, + id.0 as u32, + event_bubbles(event_name) as u8, + ); + } + } } } } if let Some(id) = mounted_id { set_node(hydrated, id, current_child.clone()?); + if should_send_mount_event { + self.send_mount_event(id); + } } if !children.is_empty() { let mut children_current_child = current_child