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..3941298bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,3 +133,8 @@ fern = { version = "0.6.0", features = ["colored"] } env_logger = "0.10.0" simple_logger = "4.0.0" thiserror = { workspace = true } + + +[dependencies] +tracing-subscriber = "0.3.17" +http-range = "0.1.5" 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/button.rs b/examples/button.rs deleted file mode 100644 index f001ed531..000000000 --- a/examples/button.rs +++ /dev/null @@ -1,18 +0,0 @@ -use dioxus::prelude::*; - -fn main() { - dioxus_desktop::launch(app); -} - -fn app(cx: Scope) -> Element { - cx.render(rsx! { - button { - onclick: |_| async move { - println!("hello, desktop!"); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - println!("goodbye, desktop!"); - }, - "hello, desktop!" - } - }) -} diff --git a/examples/compose.rs b/examples/compose.rs index 3ee6d36ae..6a303a701 100644 --- a/examples/compose.rs +++ b/examples/compose.rs @@ -1,7 +1,6 @@ //! This example shows how to create a popup window and send data back to the parent window. use dioxus::prelude::*; -use dioxus_desktop::use_window; use futures_util::StreamExt; fn main() { @@ -9,7 +8,6 @@ fn main() { } fn app(cx: Scope) -> Element { - let window = use_window(cx); let emails_sent = use_ref(cx, Vec::new); let tx = use_coroutine(cx, |mut rx: UnboundedReceiver| { @@ -27,13 +25,7 @@ fn app(cx: Scope) -> Element { button { onclick: move |_| { - let dom = VirtualDom::new_with_props(compose, ComposeProps { - app_tx: tx.clone() - }); - - // this returns a weak reference to the other window - // Be careful not to keep a strong reference to the other window or it will never be dropped - // and the window will never close. + let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() }); window.new_window(dom, Default::default()); }, "Click to compose a new email" @@ -57,7 +49,6 @@ struct ComposeProps { fn compose(cx: Scope) -> Element { let user_input = use_state(cx, String::new); - let window = use_window(cx); cx.render(rsx! { div { @@ -66,15 +57,13 @@ fn compose(cx: Scope) -> Element { button { onclick: move |_| { cx.props.app_tx.send(user_input.get().clone()); - window.close(); + dioxus_desktop::window().close(); }, "Click to send" } input { - oninput: move |e| { - user_input.set(e.value.clone()); - }, + oninput: move |e| user_input.set(e.value.clone()), value: "{user_input}" } } diff --git a/examples/crm.rs b/examples/crm.rs index d2cd98118..189f98e5a 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -87,8 +87,6 @@ fn ClientAdd(cx: Scope) -> Element { let last_name = use_state(cx, String::new); let description = use_state(cx, String::new); - let navigator = use_navigator(cx); - cx.render(rsx! { h2 { "Add new Client" } @@ -103,7 +101,7 @@ fn ClientAdd(cx: Scope) -> Element { description: description.to_string(), }); - navigator.push(Route::ClientList {}); + dioxus_router::router().push(Route::ClientList {}); }, fieldset { 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/error_handle.rs b/examples/error_handle.rs index d1fb57933..21b16ec45 100644 --- a/examples/error_handle.rs +++ b/examples/error_handle.rs @@ -1,4 +1,4 @@ -use dioxus::prelude::*; +use dioxus::{core::CapturedError, prelude::*}; fn main() { dioxus_desktop::launch(App); @@ -6,30 +6,25 @@ fn main() { #[component] fn App(cx: Scope) -> Element { - let val = use_state(cx, || "0.0001"); - - let num = match val.parse::() { - Err(_) => return cx.render(rsx!("Parsing failed")), - Ok(num) => num, - }; - cx.render(rsx! { - h1 { "The parsed value is {num}" } - button { - onclick: move |_| val.set("invalid"), - "Set an invalid number" + ErrorBoundary { + handle_error: |error: CapturedError| rsx! {"Found error {error}"}, + DemoC { + x: 1 + } } - (0..5).map(|i| rsx! { - DemoC { x: i } - }) }) } #[component] fn DemoC(cx: Scope, x: i32) -> Element { + let result = Err("Error"); + + result.throw()?; + cx.render(rsx! { h1 { - "asdasdasdasd {x}" + "{x}" } }) } diff --git a/examples/eval.rs b/examples/eval.rs index 54959df69..65c8acbeb 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -5,26 +5,21 @@ fn main() { } fn app(cx: Scope) -> Element { - let eval_provider = use_eval(cx); - - let future = use_future(cx, (), |_| { - to_owned![eval_provider]; - async move { - let eval = eval_provider( - r#" + let future = use_future(cx, (), |_| async move { + let eval = eval( + r#" dioxus.send("Hi from JS!"); let msg = await dioxus.recv(); console.log(msg); return "hello world"; "#, - ) - .unwrap(); + ) + .unwrap(); - eval.send("Hi from Rust!".into()).unwrap(); - let res = eval.recv().await.unwrap(); - println!("{:?}", eval.await); - res - } + eval.send("Hi from Rust!".into()).unwrap(); + let res = eval.recv().await.unwrap(); + println!("{:?}", eval.await); + res }); match future.value() { diff --git a/examples/login_form.rs b/examples/login_form.rs index 5ee47e4b5..d14338c3d 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -8,27 +8,25 @@ fn main() { } fn app(cx: Scope) -> Element { - let onsubmit = move |evt: FormEvent| { - cx.spawn(async move { - let resp = reqwest::Client::new() - .post("http://localhost:8080/login") - .form(&[ - ("username", &evt.values["username"]), - ("password", &evt.values["password"]), - ]) - .send() - .await; + let onsubmit = move |evt: FormEvent| async move { + let resp = reqwest::Client::new() + .post("http://localhost:8080/login") + .form(&[ + ("username", &evt.values["username"]), + ("password", &evt.values["password"]), + ]) + .send() + .await; - match resp { - // Parse data from here, such as storing a response token - Ok(_data) => println!("Login successful!"), + match resp { + // Parse data from here, such as storing a response token + Ok(_data) => println!("Login successful!"), - //Handle any errors from the fetch here - Err(_err) => { - println!("Login failed - you need a login server running on localhost:8080.") - } + //Handle any errors from the fetch here + Err(_err) => { + println!("Login failed - you need a login server running on localhost:8080.") } - }); + } }; cx.render(rsx! { 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/multiwindow.rs b/examples/multiwindow.rs index e2caeba80..c7ad2c603 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -5,14 +5,12 @@ fn main() { } fn app(cx: Scope) -> Element { - let window = dioxus_desktop::use_window(cx); - cx.render(rsx! { div { button { onclick: move |_| { let dom = VirtualDom::new(popup); - window.new_window(dom, Default::default()); + dioxus_desktop::window().new_window(dom, Default::default()); }, "New Window" } diff --git a/examples/openid_connect_demo/src/errors.rs b/examples/openid_connect_demo/src/errors.rs index 2b7e3f145..30005c1d8 100644 --- a/examples/openid_connect_demo/src/errors.rs +++ b/examples/openid_connect_demo/src/errors.rs @@ -1,20 +1,20 @@ -use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("Discovery error: {0}")] - OpenIdConnect( - #[from] openidconnect::DiscoveryError>, - ), - #[error("Parsing error: {0}")] - Parse(#[from] url::ParseError), - #[error("Request token error: {0}")] - RequestToken( - #[from] - RequestTokenError< - openidconnect::reqwest::Error, - StandardErrorResponse, - >, - ), -} +use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Discovery error: {0}")] + OpenIdConnect( + #[from] openidconnect::DiscoveryError>, + ), + #[error("Parsing error: {0}")] + Parse(#[from] url::ParseError), + #[error("Request token error: {0}")] + RequestToken( + #[from] + RequestTokenError< + openidconnect::reqwest::Error, + StandardErrorResponse, + >, + ), +} diff --git a/examples/overlay.rs b/examples/overlay.rs index 0afd830be..a4b9e7bfe 100644 --- a/examples/overlay.rs +++ b/examples/overlay.rs @@ -1,13 +1,11 @@ use dioxus::prelude::*; -use dioxus_desktop::{tao::dpi::PhysicalPosition, use_window, LogicalSize, WindowBuilder}; +use dioxus_desktop::{tao::dpi::PhysicalPosition, LogicalSize, WindowBuilder}; fn main() { dioxus_desktop::launch_cfg(app, make_config()); } fn app(cx: Scope) -> Element { - let window = use_window(cx); - cx.render(rsx! { div { width: "100%", @@ -19,7 +17,7 @@ fn app(cx: Scope) -> Element { width: "100%", height: "10px", background_color: "black", - onmousedown: move |_| window.drag(), + onmousedown: move |_| dioxus_desktop::window().drag(), } "This is an overlay!" diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index f9ecb4939..317288d55 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -53,6 +53,7 @@ fn App(cx: Scope) -> Element { let formatting = "formatting!"; let formatting_tuple = ("a", "b"); let lazy_fmt = format_args!("lazily formatted text"); + let asd = 123; cx.render(rsx! { div { // Elements @@ -80,6 +81,10 @@ fn App(cx: Scope) -> Element { // pass simple rust expressions in class: lazy_fmt, id: format_args!("attributes can be passed lazily with std::fmt::Arguments"), + class: "asd", + class: "{asd}", + // if statements can be used to conditionally render attributes + class: if formatting.contains("form") { "{asd}" }, div { class: { const WORD: &str = "expressions"; diff --git a/examples/tailwind/src/main.rs b/examples/tailwind/src/main.rs index 2dbb183c9..8a49071db 100644 --- a/examples/tailwind/src/main.rs +++ b/examples/tailwind/src/main.rs @@ -14,9 +14,13 @@ fn main() { } pub fn app(cx: Scope) -> Element { + let grey_background = true; cx.render(rsx!( div { - header { class: "text-gray-400 bg-gray-900 body-font", + header { + class: "text-gray-400 body-font", + // you can use optional attributes to optionally apply a tailwind class + class: if grey_background { "bg-gray-900" }, div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", StacksIcon {} 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/examples/window_zoom.rs b/examples/window_zoom.rs index e45d71be2..bf0949d1f 100644 --- a/examples/window_zoom.rs +++ b/examples/window_zoom.rs @@ -1,12 +1,10 @@ use dioxus::prelude::*; -use dioxus_desktop::use_window; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - let window = use_window(cx); let level = use_state(cx, || 1.0); cx.render(rsx! { @@ -16,7 +14,7 @@ fn app(cx: Scope) -> Element { oninput: |e| { if let Ok(new_zoom) = e.value.parse::() { level.set(new_zoom); - window.webview.zoom(new_zoom); + dioxus_desktop::window().webview.zoom(new_zoom); } } } diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 27e7b31f1..ebbce7538 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -49,6 +49,7 @@ impl Writer<'_> { attributes, children, brace, + .. } = el; /* @@ -209,12 +210,34 @@ impl Writer<'_> { Ok(()) } - fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result { - match &attr.attr { - ElementAttr::AttrText { name, value } => { - write!(self.out, "{name}: {value}", value = ifmt_to_string(value))?; + fn write_attribute_name(&mut self, attr: &ElementAttrName) -> Result { + match attr { + ElementAttrName::BuiltIn(name) => { + write!(self.out, "{}", name)?; } - ElementAttr::AttrExpression { name, value } => { + ElementAttrName::Custom(name) => { + write!(self.out, "{}", name.to_token_stream())?; + } + } + + Ok(()) + } + + fn write_attribute_value(&mut self, value: &ElementAttrValue) -> Result { + match value { + ElementAttrValue::AttrOptionalExpr { condition, value } => { + write!( + self.out, + "if {condition} {{ ", + condition = prettyplease::unparse_expr(condition), + )?; + self.write_attribute_value(value)?; + write!(self.out, " }}")?; + } + ElementAttrValue::AttrLiteral(value) => { + write!(self.out, "{value}", value = ifmt_to_string(value))?; + } + ElementAttrValue::AttrExpr(value) => { let out = prettyplease::unparse_expr(value); let mut lines = out.split('\n').peekable(); let first = lines.next().unwrap(); @@ -222,9 +245,9 @@ impl Writer<'_> { // a one-liner for whatever reason // Does not need a new line if lines.peek().is_none() { - write!(self.out, "{name}: {first}")?; + write!(self.out, "{first}")?; } else { - writeln!(self.out, "{name}: {first}")?; + writeln!(self.out, "{first}")?; while let Some(line) = lines.next() { self.out.indented_tab()?; @@ -237,22 +260,7 @@ impl Writer<'_> { } } } - - ElementAttr::CustomAttrText { name, value } => { - write!( - self.out, - "{name}: {value}", - name = name.to_token_stream(), - value = ifmt_to_string(value) - )?; - } - - ElementAttr::CustomAttrExpression { name, value } => { - let out = prettyplease::unparse_expr(value); - write!(self.out, "{}: {}", name.to_token_stream(), out)?; - } - - ElementAttr::EventTokens { name, tokens } => { + ElementAttrValue::EventTokens(tokens) => { let out = self.retrieve_formatted_expr(tokens).to_string(); let mut lines = out.split('\n').peekable(); @@ -261,9 +269,9 @@ impl Writer<'_> { // a one-liner for whatever reason // Does not need a new line if lines.peek().is_none() { - write!(self.out, "{name}: {first}")?; + write!(self.out, "{first}")?; } else { - writeln!(self.out, "{name}: {first}")?; + writeln!(self.out, "{first}")?; while let Some(line) = lines.next() { self.out.indented_tab()?; @@ -281,6 +289,14 @@ impl Writer<'_> { Ok(()) } + fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result { + self.write_attribute_name(&attr.attr.name)?; + write!(self.out, ": ")?; + self.write_attribute_value(&attr.attr.value)?; + + Ok(()) + } + // make sure the comments are actually relevant to this element. // test by making sure this element is the primary element on this line pub fn current_span_is_primary(&self, location: Span) -> bool { diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index bb333e8df..ede35d768 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -1,4 +1,4 @@ -use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop}; +use dioxus_rsx::{BodyNode, ElementAttrNamed, ElementAttrValue, ForLoop}; use proc_macro2::{LineColumn, Span}; use quote::ToTokens; use std::{ @@ -132,6 +132,39 @@ impl<'a> Writer<'a> { Ok(()) } + pub(crate) fn attr_value_len(&mut self, value: &ElementAttrValue) -> usize { + match value { + ElementAttrValue::AttrOptionalExpr { condition, value } => { + let condition_len = self.retrieve_formatted_expr(condition).len(); + let value_len = self.attr_value_len(value); + + condition_len + value_len + 6 + } + ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(), + ElementAttrValue::AttrExpr(expr) => expr.span().line_length(), + ElementAttrValue::EventTokens(tokens) => { + let location = Location::new(tokens.span().start()); + + let len = if let std::collections::hash_map::Entry::Vacant(e) = + self.cached_formats.entry(location) + { + let formatted = prettyplease::unparse_expr(tokens); + let len = if formatted.contains('\n') { + 10000 + } else { + formatted.len() + }; + e.insert(formatted); + len + } else { + self.cached_formats[&location].len() + }; + + len + } + } + } + pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize { let mut total = 0; @@ -146,40 +179,17 @@ impl<'a> Writer<'a> { } } - total += match &attr.attr { - ElementAttr::AttrText { value, name } => { - ifmt_to_string(value).len() + name.span().line_length() + 6 - } - ElementAttr::AttrExpression { name, value } => { - value.span().line_length() + name.span().line_length() + 6 - } - ElementAttr::CustomAttrText { value, name } => { - ifmt_to_string(value).len() + name.to_token_stream().to_string().len() + 6 - } - ElementAttr::CustomAttrExpression { name, value } => { - name.to_token_stream().to_string().len() + value.span().line_length() + 6 - } - ElementAttr::EventTokens { tokens, name } => { - let location = Location::new(tokens.span().start()); - - let len = if let std::collections::hash_map::Entry::Vacant(e) = - self.cached_formats.entry(location) - { - let formatted = prettyplease::unparse_expr(tokens); - let len = if formatted.contains('\n') { - 10000 - } else { - formatted.len() - }; - e.insert(formatted); - len - } else { - self.cached_formats[&location].len() - }; - - len + name.span().line_length() + 6 + total += match &attr.attr.name { + dioxus_rsx::ElementAttrName::BuiltIn(name) => { + let name = name.to_string(); + name.len() } + dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2, }; + + total += self.attr_value_len(&attr.attr.value); + + total += 6; } total @@ -218,7 +228,7 @@ impl<'a> Writer<'a> { } } -trait SpanLength { +pub(crate) trait SpanLength { fn line_length(&self) -> usize; } impl SpanLength for Span { diff --git a/packages/autofmt/tests/samples/simple.rsx b/packages/autofmt/tests/samples/simple.rsx index a0e573974..a3bfd1388 100644 --- a/packages/autofmt/tests/samples/simple.rsx +++ b/packages/autofmt/tests/samples/simple.rsx @@ -33,7 +33,7 @@ rsx! { } // No children, minimal props - img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" } + img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png" } // One level compression div { 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/web/mod.rs b/packages/cli/src/server/web/mod.rs index 0e1413597..98ff1e078 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -11,6 +11,7 @@ use axum::{ body::{Full, HttpBody}, extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, http::{ + self, header::{HeaderName, HeaderValue}, Method, Response, StatusCode, }, @@ -262,7 +263,7 @@ async fn setup_router( .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) .and_then( move |response: Response| async move { - let response = if file_service_config + let mut response = if file_service_config .dioxus_config .web .watcher @@ -290,6 +291,13 @@ async fn setup_router( } else { response.map(|body| body.boxed()) }; + let headers = response.headers_mut(); + headers.insert( + http::header::CACHE_CONTROL, + HeaderValue::from_static("no-cache"), + ); + headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache")); + headers.insert(http::header::EXPIRES, HeaderValue::from_static("0")); Ok(response) }, ) diff --git a/packages/core-macro/src/component_body_deserializers/component.rs b/packages/core-macro/src/component_body_deserializers/component.rs index 36cda850c..994e51709 100644 --- a/packages/core-macro/src/component_body_deserializers/component.rs +++ b/packages/core-macro/src/component_body_deserializers/component.rs @@ -58,8 +58,11 @@ impl ToTokens for ComponentDeserializerOutput { fn to_tokens(&self, tokens: &mut TokenStream2) { let comp_fn = &self.comp_fn; let props_struct = &self.props_struct; + let fn_ident = &comp_fn.sig.ident; + let doc = format!("Properties for the [`{fn_ident}`] component."); tokens.append_all(quote! { + #[doc = #doc] #props_struct #[allow(non_snake_case)] #comp_fn diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 785d4b174..ffdd4656b 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -13,62 +13,50 @@ use crate::{ #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct ElementId(pub usize); -pub(crate) struct ElementRef { +/// An Element that can be bubbled to's unique identifier. +/// +/// `BubbleId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is +/// unmounted, then the `BubbleId` will be reused for a new component. +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct VNodeId(pub usize); + +#[derive(Debug, Clone, Copy)] +pub struct ElementRef { // the pathway of the real element inside the template - pub path: ElementPath, + pub(crate) path: ElementPath, // The actual template - pub template: Option>>, + pub(crate) template: VNodeId, // The scope the element belongs to - pub scope: ScopeId, + pub(crate) scope: ScopeId, } #[derive(Clone, Copy, Debug)] -pub enum ElementPath { - Deep(&'static [u8]), - Root(usize), -} - -impl ElementRef { - pub(crate) fn none() -> Self { - Self { - template: None, - path: ElementPath::Root(0), - scope: ScopeId::ROOT, - } - } +pub struct ElementPath { + pub(crate) path: &'static [u8], } impl VirtualDom { - pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { - self.next_reference(template, ElementPath::Deep(path)) + pub(crate) fn next_element(&mut self) -> ElementId { + ElementId(self.elements.insert(None)) } - pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { - self.next_reference(template, ElementPath::Root(path)) - } + pub(crate) fn next_vnode_ref(&mut self, vnode: &VNode) -> VNodeId { + let new_id = VNodeId(self.element_refs.insert(Some(unsafe { + std::mem::transmute::, _>(vnode.into()) + }))); - pub(crate) fn next_null(&mut self) -> ElementId { - let entry = self.elements.vacant_entry(); - let id = entry.key(); + // Set this id to be dropped when the scope is rerun + if let Some(scope) = self.runtime.current_scope_id() { + self.scopes[scope.0] + .element_refs_to_drop + .borrow_mut() + .push(new_id); + } - entry.insert(ElementRef::none()); - ElementId(id) - } - - fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId { - let entry = self.elements.vacant_entry(); - let id = entry.key(); - let scope = self.runtime.current_scope_id().unwrap_or(ScopeId::ROOT); - - entry.insert(ElementRef { - // We know this is non-null because it comes from a reference - template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }), - path, - scope, - }); - ElementId(id) + new_id } pub(crate) fn reclaim(&mut self, el: ElementId) { @@ -76,7 +64,7 @@ impl VirtualDom { .unwrap_or_else(|| panic!("cannot reclaim {:?}", el)); } - pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option { + pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> { if el.0 == 0 { panic!( "Cannot reclaim the root element - {:#?}", @@ -84,12 +72,12 @@ impl VirtualDom { ); } - self.elements.try_remove(el.0) + self.elements.try_remove(el.0).map(|_| ()) } - pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) { - let node: *const VNode = node as *const _; - self.elements[el.0].template = unsafe { std::mem::transmute(node) }; + pub(crate) fn set_template(&mut self, id: VNodeId, vnode: &VNode) { + self.element_refs[id.0] = + Some(unsafe { std::mem::transmute::, _>(vnode.into()) }); } // Drop a scope and all its children @@ -101,6 +89,15 @@ impl VirtualDom { id, }); + // Remove all VNode ids from the scope + for id in self.scopes[id.0] + .element_refs_to_drop + .borrow_mut() + .drain(..) + { + self.element_refs.try_remove(id.0); + } + self.ensure_drop_safety(id); if recursive { @@ -145,14 +142,25 @@ impl VirtualDom { } /// Descend through the tree, removing any borrowed props and listeners - pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { + pub(crate) fn ensure_drop_safety(&mut self, scope_id: ScopeId) { let scope = &self.scopes[scope_id.0]; + { + // Drop all element refs that could be invalidated when the component was rerun + let mut element_refs = self.scopes[scope_id.0].element_refs_to_drop.borrow_mut(); + let element_refs_slab = &mut self.element_refs; + for element_ref in element_refs.drain(..) { + if let Some(element_ref) = element_refs_slab.get_mut(element_ref.0) { + *element_ref = None; + } + } + } + // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we // run the hooks (which hold an &mut Reference) // recursively call ensure_drop_safety on all children - let mut props = scope.borrowed_props.borrow_mut(); - props.drain(..).for_each(|comp| { + let props = { scope.borrowed_props.borrow_mut().clone() }; + for comp in props { let comp = unsafe { &*comp }; match comp.scope.get() { Some(child) if child != scope_id => self.ensure_drop_safety(child), @@ -161,7 +169,9 @@ impl VirtualDom { if let Ok(mut props) = comp.props.try_borrow_mut() { *props = None; } - }); + } + let scope = &self.scopes[scope_id.0]; + scope.borrowed_props.borrow_mut().clear(); // Now that all the references are gone, we can safely drop our own references in our listeners. let mut listeners = scope.attributes_to_drop_before_render.borrow_mut(); @@ -176,18 +186,12 @@ impl VirtualDom { impl ElementPath { pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool { - match *self { - ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()], - ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8, - } + small.len() <= self.path.len() && *small == &self.path[..small.len()] } } impl PartialEq<&[u8]> for ElementPath { fn eq(&self, other: &&[u8]) -> bool { - match *self { - ElementPath::Deep(deep) => deep.eq(*other), - ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8, - } + self.path.eq(*other) } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index a61a8cace..f3b89ae9e 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,5 +1,7 @@ use crate::any_props::AnyProps; -use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText}; +use crate::innerlude::{ + BorrowedAttributeValue, ElementPath, ElementRef, VComponent, VPlaceholder, VText, +}; use crate::mutations::Mutation; use crate::mutations::Mutation::*; use crate::nodes::VNode; @@ -94,6 +96,9 @@ impl<'b> VirtualDom { nodes_mut.resize(len, ElementId::default()); }; + // Set this node id + node.stable_id.set(Some(self.next_vnode_ref(node))); + // The best renderers will have templates prehydrated and registered // Just in case, let's create the template using instructions anyways self.register_template(node.template.get()); @@ -181,15 +186,30 @@ impl<'b> VirtualDom { use DynamicNode::*; match &template.dynamic_nodes[idx] { node @ Component { .. } | node @ Fragment(_) => { - self.create_dynamic_node(template, node, idx) + let template_ref = ElementRef { + path: ElementPath { + path: template.template.get().node_paths[idx], + }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + self.create_dynamic_node(template_ref, node) } - Placeholder(VPlaceholder { id }) => { - let id = self.set_slot(template, id, idx); + Placeholder(VPlaceholder { id, parent }) => { + let template_ref = ElementRef { + path: ElementPath { + path: template.template.get().node_paths[idx], + }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + parent.set(Some(template_ref)); + let id = self.set_slot(id); self.mutations.push(CreatePlaceholder { id }); 1 } Text(VText { id, value }) => { - let id = self.set_slot(template, id, idx); + let id = self.set_slot(id); self.create_static_text(value, id); 1 } @@ -205,7 +225,7 @@ impl<'b> VirtualDom { }); } - /// We write all the descndent data for this element + /// We write all the descendent data for this element /// /// Elements can contain other nodes - and those nodes can be dynamic or static /// @@ -265,7 +285,14 @@ impl<'b> VirtualDom { .map(|sorted_index| dynamic_nodes[sorted_index].0); for idx in reversed_iter { - let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx); + let boundary_ref = ElementRef { + path: ElementPath { + path: template.template.get().node_paths[idx], + }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + let m = self.create_dynamic_node(boundary_ref, &template.dynamic_nodes[idx]); if m > 0 { // The path is one shorter because the top node is the root let path = &template.template.get().node_paths[idx][1..]; @@ -279,15 +306,15 @@ impl<'b> VirtualDom { attrs: &mut Peekable>, root_idx: u8, root: ElementId, - node: &VNode, + node: &'b VNode<'b>, ) { while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx)) { - let id = self.assign_static_node_as_dynamic(path, root, node, attr_id); + let id = self.assign_static_node_as_dynamic(path, root); loop { - self.write_attribute(&node.dynamic_attrs[attr_id], id); + self.write_attribute(node, attr_id, &node.dynamic_attrs[attr_id], id); // Only push the dynamic attributes forward if they match the current path (same element) match attrs.next_if(|(_, p)| *p == path) { @@ -298,7 +325,13 @@ impl<'b> VirtualDom { } } - fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) { + fn write_attribute( + &mut self, + template: &'b VNode<'b>, + idx: usize, + attribute: &'b crate::Attribute<'b>, + id: ElementId, + ) { // Make sure we set the attribute's associated id attribute.mounted_element.set(id); @@ -307,6 +340,13 @@ impl<'b> VirtualDom { match &attribute.value { AttributeValue::Listener(_) => { + let path = &template.template.get().attr_paths[idx]; + let element_ref = ElementRef { + path: ElementPath { path }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + self.elements[id.0] = Some(element_ref); self.mutations.push(NewEventListener { // all listeners start with "on" name: &unbounded_name[2..], @@ -330,7 +370,7 @@ impl<'b> VirtualDom { fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId { // Get an ID for this root since it's a real root - let this_id = self.next_root(template, root_idx); + let this_id = self.next_element(); template.root_ids.borrow_mut()[root_idx] = this_id; self.mutations.push(LoadTemplate { @@ -353,8 +393,6 @@ impl<'b> VirtualDom { &mut self, path: &'static [u8], this_id: ElementId, - template: &VNode, - attr_id: usize, ) -> ElementId { if path.len() == 1 { return this_id; @@ -362,7 +400,7 @@ impl<'b> VirtualDom { // if attribute is on a root node, then we've already created the element // Else, it's deep in the template and we should create a new id for it - let id = self.next_element(template, template.template.get().attr_paths[attr_id]); + let id = self.next_element(); self.mutations.push(Mutation::AssignId { path: &path[1..], @@ -405,6 +443,7 @@ impl<'b> VirtualDom { #[allow(unused_mut)] pub(crate) fn register_template(&mut self, mut template: Template<'static>) { let (path, byte_index) = template.name.rsplit_once(':').unwrap(); + let byte_index = byte_index.parse::().unwrap(); // First, check if we've already seen this template if self @@ -439,27 +478,21 @@ impl<'b> VirtualDom { pub(crate) fn create_dynamic_node( &mut self, - template: &'b VNode<'b>, + parent: ElementRef, node: &'b DynamicNode<'b>, - idx: usize, ) -> usize { use DynamicNode::*; match node { - Text(text) => self.create_dynamic_text(template, text, idx), - Placeholder(place) => self.create_placeholder(place, template, idx), - Component(component) => self.create_component_node(template, component), - Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(), + Text(text) => self.create_dynamic_text(parent, text), + Placeholder(place) => self.create_placeholder(place, parent), + Component(component) => self.create_component_node(Some(parent), component), + Fragment(frag) => self.create_children(*frag, Some(parent)), } } - fn create_dynamic_text( - &mut self, - template: &'b VNode<'b>, - text: &'b VText<'b>, - idx: usize, - ) -> usize { + fn create_dynamic_text(&mut self, parent: ElementRef, text: &'b VText<'b>) -> usize { // Allocate a dynamic element reference for this text node - let new_id = self.next_element(template, template.template.get().node_paths[idx]); + let new_id = self.next_element(); // Make sure the text node is assigned to the correct element text.id.set(Some(new_id)); @@ -470,7 +503,7 @@ impl<'b> VirtualDom { // Add the mutation to the list self.mutations.push(HydrateText { id: new_id, - path: &template.template.get().node_paths[idx][1..], + path: &parent.path.path[1..], value, }); @@ -481,18 +514,20 @@ impl<'b> VirtualDom { pub(crate) fn create_placeholder( &mut self, placeholder: &VPlaceholder, - template: &'b VNode<'b>, - idx: usize, + parent: ElementRef, ) -> usize { // Allocate a dynamic element reference for this text node - let id = self.next_element(template, template.template.get().node_paths[idx]); + let id = self.next_element(); // Make sure the text node is assigned to the correct element placeholder.id.set(Some(id)); + // Assign the placeholder's parent + placeholder.parent.set(Some(parent)); + // Assign the ID to the existing node in the template self.mutations.push(AssignId { - path: &template.template.get().node_paths[idx][1..], + path: &parent.path.path[1..], id, }); @@ -502,7 +537,7 @@ impl<'b> VirtualDom { pub(super) fn create_component_node( &mut self, - template: &'b VNode<'b>, + parent: Option, component: &'b VComponent<'b>, ) -> usize { use RenderReturn::*; @@ -514,8 +549,11 @@ impl<'b> VirtualDom { match unsafe { self.run_scope(scope).extend_lifetime_ref() } { // Create the component's root element - Ready(t) => self.create_scope(scope, t), - Aborted(t) => self.mount_aborted(template, t), + Ready(t) => { + self.assign_boundary_ref(parent, t); + self.create_scope(scope, t) + } + Aborted(t) => self.mount_aborted(t, parent), } } @@ -531,20 +569,17 @@ impl<'b> VirtualDom { .unwrap_or_else(|| component.scope.get().unwrap()) } - fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize { - let id = self.next_element(parent, &[]); + fn mount_aborted(&mut self, placeholder: &VPlaceholder, parent: Option) -> usize { + let id = self.next_element(); self.mutations.push(Mutation::CreatePlaceholder { id }); placeholder.id.set(Some(id)); + placeholder.parent.set(parent); + 1 } - fn set_slot( - &mut self, - template: &'b VNode<'b>, - slot: &'b Cell>, - id: usize, - ) -> ElementId { - let id = self.next_element(template, template.template.get().node_paths[id]); + fn set_slot(&mut self, slot: &'b Cell>) -> ElementId { + let id = self.next_element(); slot.set(Some(id)); id } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 2de862218..43b27f2e7 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,7 +1,10 @@ use crate::{ any_props::AnyProps, arena::ElementId, - innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText}, + innerlude::{ + BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent, VPlaceholder, + VText, + }, mutations::Mutation, nodes::RenderReturn, nodes::{DynamicNode, VNode}, @@ -39,19 +42,27 @@ impl<'b> VirtualDom { (Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p), // Just move over the placeholder - (Aborted(l), Aborted(r)) => r.id.set(l.id.get()), + (Aborted(l), Aborted(r)) => { + r.id.set(l.id.get()); + r.parent.set(l.parent.get()) + } // Placeholder becomes something // We should also clear the error now - (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), + (Aborted(l), Ready(r)) => self.replace_placeholder( + l, + [r], + l.parent.get().expect("root node should not be none"), + ), }; } self.runtime.scope_stack.borrow_mut().pop(); } fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { - let id = self.next_null(); + let id = self.next_element(); p.id.set(Some(id)); + p.parent.set(l.parent.get()); self.mutations.push(Mutation::CreatePlaceholder { id }); let pre_edits = self.mutations.edits.len(); @@ -81,12 +92,24 @@ impl<'b> VirtualDom { if let Some(&template) = map.get(&byte_index) { right_template.template.set(template); if template != left_template.template.get() { - return self.replace(left_template, [right_template]); + let parent = left_template.parent.take(); + return self.replace(left_template, [right_template], parent); } } } } + // Copy over the parent + { + right_template.parent.set(left_template.parent.get()); + } + + // Update the bubble id pointer + right_template.stable_id.set(left_template.stable_id.get()); + if let Some(bubble_id) = right_template.stable_id.get() { + self.set_template(bubble_id, right_template); + } + // If the templates are the same, we don't need to do anything, nor do we want to if templates_are_the_same(left_template, right_template) { return; @@ -105,12 +128,8 @@ impl<'b> VirtualDom { .zip(right_template.dynamic_attrs.iter()) .for_each(|(left_attr, right_attr)| { // Move over the ID from the old to the new - right_attr - .mounted_element - .set(left_attr.mounted_element.get()); - - // We want to make sure anything that gets pulled is valid - self.update_template(left_attr.mounted_element.get(), right_template); + let mounted_element = left_attr.mounted_element.get(); + right_attr.mounted_element.set(mounted_element); // If the attributes are different (or volatile), we need to update them if left_attr.value != right_attr.value || left_attr.volatile { @@ -123,8 +142,16 @@ impl<'b> VirtualDom { .dynamic_nodes .iter() .zip(right_template.dynamic_nodes.iter()) - .for_each(|(left_node, right_node)| { - self.diff_dynamic_node(left_node, right_node, right_template); + .enumerate() + .for_each(|(dyn_node_idx, (left_node, right_node))| { + let current_ref = ElementRef { + template: right_template.stable_id().unwrap(), + path: ElementPath { + path: left_template.template.get().node_paths[dyn_node_idx], + }, + scope: self.runtime.scope_stack.borrow().last().copied().unwrap(), + }; + self.diff_dynamic_node(left_node, right_node, current_ref); }); // Make sure the roots get transferred over while we're here @@ -135,30 +162,24 @@ impl<'b> VirtualDom { right.push(element); } } - - let root_ids = right_template.root_ids.borrow(); - - // Update the node refs - for i in 0..root_ids.len() { - if let Some(root_id) = root_ids.get(i) { - self.update_template(*root_id, right_template); - } - } } fn diff_dynamic_node( &mut self, left_node: &'b DynamicNode<'b>, right_node: &'b DynamicNode<'b>, - node: &'b VNode<'b>, + parent: ElementRef, ) { match (left_node, right_node) { - (Text(left), Text(right)) => self.diff_vtext(left, right, node), - (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), - (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), - (Component(left), Component(right)) => self.diff_vcomponent(left, right, node), - (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right), - (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), + (Text(left), Text(right)) => self.diff_vtext(left, right), + (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right, parent), + (Placeholder(left), Placeholder(right)) => { + right.id.set(left.id.get()); + right.parent.set(left.parent.get()); + }, + (Component(left), Component(right)) => self.diff_vcomponent(left, right, Some(parent)), + (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right, parent), + (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right, parent), _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), }; } @@ -179,7 +200,7 @@ impl<'b> VirtualDom { &mut self, left: &'b VComponent<'b>, right: &'b VComponent<'b>, - right_template: &'b VNode<'b>, + parent: Option, ) { if std::ptr::eq(left, right) { return; @@ -187,7 +208,7 @@ impl<'b> VirtualDom { // Replace components that have different render fns if left.render_fn != right.render_fn { - return self.replace_vcomponent(right_template, right, left); + return self.replace_vcomponent(right, left, parent); } // Make sure the new vcomponent has the right scopeid associated to it @@ -228,11 +249,11 @@ impl<'b> VirtualDom { fn replace_vcomponent( &mut self, - right_template: &'b VNode<'b>, right: &'b VComponent<'b>, left: &'b VComponent<'b>, + parent: Option, ) { - let m = self.create_component_node(right_template, right); + let m = self.create_component_node(parent, right); let pre_edits = self.mutations.edits.len(); @@ -287,11 +308,12 @@ impl<'b> VirtualDom { /// } /// ``` fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { + let parent = left.parent.take(); match matching_components(left, right) { - None => self.replace(left, [right]), + None => self.replace(left, [right], parent), Some(components) => components .into_iter() - .for_each(|(l, r)| self.diff_vcomponent(l, r, right)), + .for_each(|(l, r)| self.diff_vcomponent(l, r, parent)), } } @@ -299,11 +321,8 @@ impl<'b> VirtualDom { /// /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's /// different. - fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) { - let id = left - .id - .get() - .unwrap_or_else(|| self.next_element(node, &[0])); + fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) { + let id = left.id.get().unwrap_or_else(|| self.next_element()); right.id.set(Some(id)); if left.value != right.value { @@ -312,7 +331,12 @@ impl<'b> VirtualDom { } } - fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_non_empty_fragment( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { let new_is_keyed = new[0].key.is_some(); let old_is_keyed = old[0].key.is_some(); debug_assert!( @@ -325,9 +349,9 @@ impl<'b> VirtualDom { ); if new_is_keyed && old_is_keyed { - self.diff_keyed_children(old, new); + self.diff_keyed_children(old, new, parent); } else { - self.diff_non_keyed_children(old, new); + self.diff_non_keyed_children(old, new, parent); } } @@ -339,7 +363,12 @@ impl<'b> VirtualDom { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_non_keyed_children( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { use std::cmp::Ordering; // Handled these cases in `diff_children` before calling this function. @@ -348,7 +377,9 @@ impl<'b> VirtualDom { match old.len().cmp(&new.len()) { Ordering::Greater => self.remove_nodes(&old[new.len()..]), - Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), + Ordering::Less => { + self.create_and_insert_after(&new[old.len()..], old.last().unwrap(), parent) + } Ordering::Equal => {} } @@ -373,7 +404,12 @@ impl<'b> VirtualDom { // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 // // The stack is empty upon entry. - fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_keyed_children( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { if cfg!(debug_assertions) { let mut keys = rustc_hash::FxHashSet::default(); let mut assert_unique_keys = |children: &'b [VNode<'b>]| { @@ -401,7 +437,7 @@ impl<'b> VirtualDom { // // `shared_prefix_count` is the count of how many nodes at the start of // `new` and `old` share the same keys. - let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { + let (left_offset, right_offset) = match self.diff_keyed_ends(old, new, parent) { Some(count) => count, None => return, }; @@ -427,18 +463,18 @@ impl<'b> VirtualDom { if left_offset == 0 { // insert at the beginning of the old list let foothold = &old[old.len() - right_offset]; - self.create_and_insert_before(new_middle, foothold); + self.create_and_insert_before(new_middle, foothold, parent); } else if right_offset == 0 { // insert at the end the old list let foothold = old.last().unwrap(); - self.create_and_insert_after(new_middle, foothold); + self.create_and_insert_after(new_middle, foothold, parent); } else { // inserting in the middle let foothold = &old[left_offset - 1]; - self.create_and_insert_after(new_middle, foothold); + self.create_and_insert_after(new_middle, foothold, parent); } } else { - self.diff_keyed_middle(old_middle, new_middle); + self.diff_keyed_middle(old_middle, new_middle, parent); } } @@ -451,6 +487,7 @@ impl<'b> VirtualDom { &mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>], + parent: ElementRef, ) -> Option<(usize, usize)> { let mut left_offset = 0; @@ -466,7 +503,7 @@ impl<'b> VirtualDom { // If that was all of the old children, then create and append the remaining // new children and we're finished. if left_offset == old.len() { - self.create_and_insert_after(&new[left_offset..], old.last().unwrap()); + self.create_and_insert_after(&new[left_offset..], old.last().unwrap(), parent); return None; } @@ -505,7 +542,12 @@ impl<'b> VirtualDom { // // Upon exit from this function, it will be restored to that same self. #[allow(clippy::too_many_lines)] - fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_keyed_middle( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { /* 1. Map the old keys into a numerical ordering based on indices. 2. Create a map of old key to its index @@ -560,9 +602,9 @@ impl<'b> VirtualDom { // If none of the old keys are reused by the new children, then we remove all the remaining old children and // create the new children afresh. if shared_keys.is_empty() { - if old.first().is_some() { + if !old.is_empty() { self.remove_nodes(&old[1..]); - self.replace(&old[0], new); + self.replace(&old[0], new, Some(parent)); } else { // I think this is wrong - why are we appending? // only valid of the if there are no trailing elements @@ -739,20 +781,38 @@ impl<'b> VirtualDom { .sum() } - fn create_children(&mut self, nodes: impl IntoIterator>) -> usize { + pub(crate) fn create_children( + &mut self, + nodes: impl IntoIterator>, + parent: Option, + ) -> usize { nodes .into_iter() - .fold(0, |acc, child| acc + self.create(child)) + .map(|child| { + self.assign_boundary_ref(parent, child); + self.create(child) + }) + .sum() } - fn create_and_insert_before(&mut self, new: &'b [VNode<'b>], before: &'b VNode<'b>) { - let m = self.create_children(new); + fn create_and_insert_before( + &mut self, + new: &'b [VNode<'b>], + before: &'b VNode<'b>, + parent: ElementRef, + ) { + let m = self.create_children(new, Some(parent)); let id = self.find_first_element(before); self.mutations.push(Mutation::InsertBefore { id, m }) } - fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) { - let m = self.create_children(new); + fn create_and_insert_after( + &mut self, + new: &'b [VNode<'b>], + after: &'b VNode<'b>, + parent: ElementRef, + ) { + let m = self.create_children(new, Some(parent)); let id = self.find_last_element(after); self.mutations.push(Mutation::InsertAfter { id, m }) } @@ -762,15 +822,21 @@ impl<'b> VirtualDom { &mut self, l: &'b VPlaceholder, r: impl IntoIterator>, + parent: ElementRef, ) { - let m = self.create_children(r); + let m = self.create_children(r, Some(parent)); let id = l.id.get().unwrap(); self.mutations.push(Mutation::ReplaceWith { id, m }); self.reclaim(id); } - fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator>) { - let m = self.create_children(right); + fn replace( + &mut self, + left: &'b VNode<'b>, + right: impl IntoIterator>, + parent: Option, + ) { + let m = self.create_children(right, parent); let pre_edits = self.mutations.edits.len(); @@ -789,11 +855,12 @@ impl<'b> VirtualDom { }; } - fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) { + fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder, parent: ElementRef) { // Create the placeholder first, ensuring we get a dedicated ID for the placeholder - let placeholder = self.next_element(&l[0], &[]); + let placeholder = self.next_element(); r.id.set(Some(placeholder)); + r.parent.set(Some(parent)); self.mutations .push(Mutation::CreatePlaceholder { id: placeholder }); @@ -831,6 +898,16 @@ impl<'b> VirtualDom { // Clean up the roots, assuming we need to generate mutations for these // This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim) self.reclaim_roots(node, gen_muts); + + // Clean up the vnode id + self.reclaim_vnode_id(node); + } + + fn reclaim_vnode_id(&mut self, node: &'b VNode<'b>) { + // Clean up the vnode id + if let Some(id) = node.stable_id() { + self.element_refs.remove(id.0); + } } fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) { @@ -989,6 +1066,13 @@ impl<'b> VirtualDom { } } } + + pub(crate) fn assign_boundary_ref(&mut self, parent: Option, child: &'b VNode<'b>) { + if let Some(parent) = parent { + // assign the parent of the child + child.parent.set(Some(parent)); + } + } } /// Are the templates the same? diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index 819d2f99e..2f64bea9f 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,25 +1,68 @@ -use crate::{ScopeId, ScopeState}; +use crate::{ + scope_context::{consume_context, current_scope_id, schedule_update_any}, + Element, IntoDynNode, LazyNodes, Properties, Scope, ScopeId, ScopeState, Template, + TemplateAttribute, TemplateNode, VNode, +}; use std::{ any::{Any, TypeId}, + backtrace::Backtrace, cell::RefCell, - fmt::Debug, + error::Error, + fmt::{Debug, Display}, + rc::Rc, + sync::Arc, }; -/// A boundary that will capture any errors from child components -pub struct ErrorBoundary { - error: RefCell>, - _id: ScopeId, +/// Provide an error boundary to catch errors from child components +pub fn use_error_boundary(cx: &ScopeState) -> &ErrorBoundary { + cx.use_hook(|| cx.provide_context(ErrorBoundary::new())) } +/// A boundary that will capture any errors from child components +#[derive(Debug, Clone, Default)] +pub struct ErrorBoundary { + inner: Rc, +} + +/// A boundary that will capture any errors from child components +pub struct ErrorBoundaryInner { + error: RefCell>, + _id: ScopeId, + rerun_boundary: Arc, +} + +impl Debug for ErrorBoundaryInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ErrorBoundaryInner") + .field("error", &self.error) + .finish() + } +} + +#[derive(Debug)] /// An instance of an error captured by a descendant component. pub struct CapturedError { /// The error captured by the error boundary pub error: Box, + /// The backtrace of the error + pub backtrace: Backtrace, + /// The scope that threw the error pub scope: ScopeId, } +impl Display for CapturedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "Encountered error: {:?}\nIn scope: {:?}\nBacktrace: {}", + self.error, self.scope, self.backtrace + )) + } +} + +impl Error for CapturedError {} + impl CapturedError { /// Downcast the error type into a concrete error type pub fn downcast(&self) -> Option<&T> { @@ -32,17 +75,56 @@ impl CapturedError { } } -impl ErrorBoundary { - pub fn new(id: ScopeId) -> Self { +impl Default for ErrorBoundaryInner { + fn default() -> Self { Self { error: RefCell::new(None), - _id: id, + _id: current_scope_id() + .expect("Cannot create an error boundary outside of a component's scope."), + rerun_boundary: schedule_update_any().unwrap(), + } + } +} + +impl ErrorBoundary { + /// Create a new error boundary + pub fn new() -> Self { + Self::default() + } + + /// Create a new error boundary in the current scope + pub(crate) fn new_in_scope( + scope: ScopeId, + rerun_boundary: Arc, + ) -> Self { + Self { + inner: Rc::new(ErrorBoundaryInner { + error: RefCell::new(None), + _id: scope, + rerun_boundary, + }), } } /// Push an error into this Error Boundary - pub fn insert_error(&self, scope: ScopeId, error: Box) { - self.error.replace(Some(CapturedError { error, scope })); + pub fn insert_error( + &self, + scope: ScopeId, + error: Box, + backtrace: Backtrace, + ) { + println!("{:?} {:?}", error, self.inner._id); + self.inner.error.replace(Some(CapturedError { + error, + scope, + backtrace, + })); + (self.inner.rerun_boundary)(self.inner._id); + } + + /// Take any error that has been captured by this error boundary + pub fn take_error(&self) -> Option { + self.inner.error.take() } } @@ -59,7 +141,7 @@ impl ErrorBoundary { /// ```rust, ignore /// #[component] /// fn App(cx: Scope, count: String) -> Element { -/// let id: i32 = count.parse().throw(cx)?; +/// let id: i32 = count.parse().throw()?; /// /// cx.render(rsx! { /// div { "Count {}" } @@ -84,14 +166,14 @@ pub trait Throw: Sized { /// ```rust, ignore /// #[component] /// fn App(cx: Scope, count: String) -> Element { - /// let id: i32 = count.parse().throw(cx)?; + /// let id: i32 = count.parse().throw()?; /// /// cx.render(rsx! { /// div { "Count {}" } /// }) /// } /// ``` - fn throw(self, cx: &ScopeState) -> Option; + fn throw(self) -> Option; /// Returns an option that evaluates to None if there is an error, injecting the error to the nearest error boundary. /// @@ -107,45 +189,46 @@ pub trait Throw: Sized { /// ```rust, ignore /// #[component] /// fn App(cx: Scope, count: String) -> Element { - /// let id: i32 = count.parse().throw(cx)?; + /// let id: i32 = count.parse().throw()?; /// /// cx.render(rsx! { /// div { "Count {}" } /// }) /// } /// ``` - fn throw_with( - self, - cx: &ScopeState, - e: impl FnOnce() -> D, - ) -> Option; + fn throw_with(self, e: impl FnOnce() -> D) -> Option { + self.throw().or_else(|| throw_error(e())) + } +} + +fn throw_error(e: impl Debug + 'static) -> Option { + if let Some(cx) = consume_context::() { + match current_scope_id() { + Some(id) => cx.insert_error(id, Box::new(e), Backtrace::capture()), + None => { + tracing::error!("Cannot throw error outside of a component's scope.") + } + } + } + + None } /// We call clone on any errors that can be owned out of a reference impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result { type Out = &'a T; - fn throw(self, cx: &ScopeState) -> Option { + fn throw(self) -> Option { match self { Ok(t) => Some(t), - Err(e) => { - cx.throw(e.to_owned()); - None - } + Err(e) => throw_error(e.to_owned()), } } - fn throw_with( - self, - cx: &ScopeState, - err: impl FnOnce() -> D, - ) -> Option { + fn throw_with(self, err: impl FnOnce() -> D) -> Option { match self { Ok(t) => Some(t), - Err(_e) => { - cx.throw(err()); - None - } + Err(_e) => throw_error(err()), } } } @@ -154,25 +237,15 @@ impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result Throw for Result { type Out = T; - fn throw(self, cx: &ScopeState) -> Option { + fn throw(self) -> Option { match self { Ok(t) => Some(t), - Err(e) => { - cx.throw(e); - None - } + Err(e) => throw_error(e), } } - fn throw_with( - self, - cx: &ScopeState, - error: impl FnOnce() -> D, - ) -> Option { - self.ok().or_else(|| { - cx.throw(error()); - None - }) + fn throw_with(self, error: impl FnOnce() -> D) -> Option { + self.ok().or_else(|| throw_error(error())) } } @@ -180,21 +253,234 @@ impl Throw for Result { impl Throw for Option { type Out = T; - fn throw(self, cx: &ScopeState) -> Option { - self.or_else(|| { - cx.throw("None error."); - None - }) + fn throw(self) -> Option { + self.or_else(|| throw_error("Attempted to unwrap a None value.")) } - fn throw_with( - self, - cx: &ScopeState, - error: impl FnOnce() -> D, - ) -> Option { - self.or_else(|| { - cx.throw(error()); - None - }) + fn throw_with(self, error: impl FnOnce() -> D) -> Option { + self.or_else(|| throw_error(error())) + } +} + +pub struct ErrorHandler<'a>(Box LazyNodes<'a, 'a> + 'a>); +impl<'a, F: Fn(CapturedError) -> LazyNodes<'a, 'a> + 'a> From for ErrorHandler<'a> { + fn from(value: F) -> Self { + Self(Box::new(value)) + } +} +fn default_handler<'a>(error: CapturedError) -> LazyNodes<'a, 'a> { + LazyNodes::new(move |__cx: &ScopeState| -> VNode { + static TEMPLATE: Template = Template { + name: "error_handle.rs:42:5:884", + roots: &[TemplateNode::Element { + tag: "pre", + namespace: None, + attrs: &[TemplateAttribute::Static { + name: "color", + namespace: Some("style"), + value: "red", + }], + children: &[TemplateNode::DynamicText { id: 0usize }], + }], + node_paths: &[&[0u8, 0u8]], + attr_paths: &[], + }; + VNode { + parent: None, + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(), + dynamic_nodes: __cx + .bump() + .alloc([__cx.text_node(format_args!("{0}", error))]), + dynamic_attrs: __cx.bump().alloc([]), + } + }) +} +pub struct ErrorBoundaryProps<'a> { + children: Element<'a>, + handle_error: ErrorHandler<'a>, +} +impl<'a> ErrorBoundaryProps<'a> { + /** + Create a builder for building `ErrorBoundaryProps`. + On the builder, call `.children(...)`(optional), `.handle_error(...)`(optional) to set the values of the fields. + Finally, call `.build()` to create the instance of `ErrorBoundaryProps`. + */ + #[allow(dead_code)] + pub fn builder() -> ErrorBoundaryPropsBuilder<'a, ((), ())> { + ErrorBoundaryPropsBuilder { + fields: ((), ()), + _phantom: ::core::default::Default::default(), + } + } +} +#[must_use] +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub struct ErrorBoundaryPropsBuilder<'a, TypedBuilderFields> { + fields: TypedBuilderFields, + _phantom: ::core::marker::PhantomData<&'a ()>, +} +impl<'a, TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<'a, TypedBuilderFields> +where + TypedBuilderFields: Clone, +{ + fn clone(&self) -> Self { + Self { + fields: self.fields.clone(), + _phantom: ::core::default::Default::default(), + } + } +} +impl<'a> Properties for ErrorBoundaryProps<'a> { + type Builder = ErrorBoundaryPropsBuilder<'a, ((), ())>; + const IS_STATIC: bool = false; + fn builder() -> Self::Builder { + ErrorBoundaryProps::builder() + } + unsafe fn memoize(&self, _: &Self) -> bool { + false + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub trait ErrorBoundaryPropsBuilder_Optional { + fn into_value T>(self, default: F) -> T; +} +impl ErrorBoundaryPropsBuilder_Optional for () { + fn into_value T>(self, default: F) -> T { + default() + } +} +impl ErrorBoundaryPropsBuilder_Optional for (T,) { + fn into_value T>(self, _: F) -> T { + self.0 + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((), __handle_error)> { + pub fn children( + self, + children: Element<'a>, + ) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> { + let children = (children,); + let (_, handle_error) = self.fields; + ErrorBoundaryPropsBuilder { + fields: (children, handle_error), + _phantom: self._phantom, + } + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_children {} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> { + #[deprecated(note = "Repeated field children")] + pub fn children( + self, + _: ErrorBoundaryPropsBuilder_Error_Repeated_field_children, + ) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> { + self + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, ())> { + pub fn handle_error( + self, + handle_error: impl ::core::convert::Into>, + ) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> { + let handle_error = (handle_error.into(),); + let (children, _) = self.fields; + ErrorBoundaryPropsBuilder { + fields: (children, handle_error), + _phantom: self._phantom, + } + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error {} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> { + #[deprecated(note = "Repeated field handle_error")] + pub fn handle_error( + self, + _: ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error, + ) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> { + self + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl< + 'a, + __handle_error: ErrorBoundaryPropsBuilder_Optional>, + __children: ErrorBoundaryPropsBuilder_Optional>, + > ErrorBoundaryPropsBuilder<'a, (__children, __handle_error)> +{ + pub fn build(self) -> ErrorBoundaryProps<'a> { + let (children, handle_error) = self.fields; + let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, || { + ::core::default::Default::default() + }); + let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || { + ErrorHandler(Box::new(default_handler)) + }); + ErrorBoundaryProps { + children, + handle_error, + } + } +} +/// Create a new error boundary component. +/// +/// ## Details +/// +/// Error boundaries handle errors within a specific part of your application. Any errors passed in a child with [`Throw`] will be caught by the nearest error boundary. +/// +/// ## Example +/// +/// ```rust, ignore +/// rsx!{ +/// ErrorBoundary { +/// handle_error: |error| rsx! { "Oops, we encountered an error. Please report {error} to the developer of this application" } +/// ThrowsError {} +/// } +/// } +/// ``` +/// +/// ## Usage +/// +/// Error boundaries are an easy way to handle errors in your application. +/// They are similar to `try/catch` in JavaScript, but they only catch errors in the tree below them. +/// Error boundaries are quick to implement, but it can be useful to individually handle errors in your components to provide a better user experience when you know that an error is likely to occur. +#[allow(non_upper_case_globals, non_snake_case)] +pub fn ErrorBoundary<'a>(cx: Scope<'a, ErrorBoundaryProps<'a>>) -> Element { + let error_boundary = use_error_boundary(cx); + match error_boundary.take_error() { + Some(error) => cx.render((cx.props.handle_error.0)(error)), + None => Some({ + let __cx = cx; + static TEMPLATE: Template = Template { + name: "examples/error_handle.rs:81:17:2342", + roots: &[TemplateNode::Dynamic { id: 0usize }], + node_paths: &[&[0u8]], + attr_paths: &[], + }; + VNode { + parent: None, + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(), + dynamic_nodes: __cx.bump().alloc([{ + let ___nodes = (&cx.props.children).into_vnode(__cx); + ___nodes + }]), + dynamic_attrs: __cx.bump().alloc([]), + } + }), } } diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index a9e919bfa..dbc8e8131 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -30,7 +30,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { let children = cx.props.0.as_ref()?; Some(VNode { key: children.key, - parent: children.parent, + parent: children.parent.clone(), + stable_id: children.stable_id.clone(), template: children.template.clone(), root_ids: children.root_ids.clone(), dynamic_nodes: children.dynamic_nodes, diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 392774767..13d55826b 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -90,10 +90,11 @@ pub mod prelude { pub use crate::innerlude::{ consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, provide_context, provide_context_to_scope, provide_root_context, push_future, - remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue, - Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, IntoDynNode, - LazyNodes, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, - Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom, + remove_future, schedule_update_any, spawn, spawn_forever, suspend, use_error_boundary, + AnyValue, Component, Element, ErrorBoundary, Event, EventHandler, Fragment, + IntoAttributeValue, IntoDynNode, LazyNodes, Properties, Runtime, RuntimeGuard, Scope, + ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw, + VNode, VirtualDom, }; } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index f33590e2e..25e64cecc 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -26,7 +26,7 @@ pub struct Mutations<'a> { /// Any templates encountered while diffing the DOM. /// /// These must be loaded into a cache before applying the edits - pub templates: Vec>, + pub templates: Vec>, /// Any mutations required to patch the renderer to match the layout of the VirtualDom pub edits: Vec>, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 78868d29f..d3ad1875f 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,3 +1,4 @@ +use crate::innerlude::{ElementRef, VNodeId}; use crate::{ any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState, }; @@ -47,7 +48,10 @@ pub struct VNode<'a> { pub key: Option<&'a str>, /// When rendered, this template will be linked to its parent manually - pub parent: Option, + pub(crate) parent: Cell>, + + /// The bubble id assigned to the child that we need to update and drop when diffing happens + pub(crate) stable_id: Cell>, /// The static nodes and static descriptor of the template pub template: Cell>, @@ -68,7 +72,8 @@ impl<'a> VNode<'a> { pub fn empty(cx: &'a ScopeState) -> Element<'a> { Some(VNode { key: None, - parent: None, + parent: Default::default(), + stable_id: Default::default(), root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())), dynamic_nodes: &[], dynamic_attrs: &[], @@ -81,6 +86,30 @@ impl<'a> VNode<'a> { }) } + /// Create a new VNode + pub fn new( + key: Option<&'a str>, + template: Template<'static>, + root_ids: bumpalo::collections::Vec<'a, ElementId>, + dynamic_nodes: &'a [DynamicNode<'a>], + dynamic_attrs: &'a [Attribute<'a>], + ) -> Self { + Self { + key, + parent: Cell::new(None), + stable_id: Cell::new(None), + template: Cell::new(template), + root_ids: RefCell::new(root_ids), + dynamic_nodes, + dynamic_attrs, + } + } + + /// Get the stable id of this node used for bubbling events + pub(crate) fn stable_id(&self) -> Option { + self.stable_id.get() + } + /// Load a dynamic root at the given index /// /// Returns [`None`] if the root is actually a static node (Element/Text) @@ -319,7 +348,7 @@ pub struct VComponent<'a> { /// The function pointer of the component, known at compile time /// - /// It is possible that components get folded at comppile time, so these shouldn't be really used as a key + /// It is possible that components get folded at compile time, so these shouldn't be really used as a key pub(crate) render_fn: *const (), pub(crate) props: RefCell + 'a>>>, @@ -372,6 +401,8 @@ impl<'a> VText<'a> { pub struct VPlaceholder { /// The ID of this node in the real DOM pub(crate) id: Cell>, + /// The parent of this node + pub(crate) parent: Cell>, } impl VPlaceholder { @@ -651,52 +682,52 @@ pub trait IntoDynNode<'a, A = ()> { /// Consume this item along with a scopestate and produce a DynamicNode /// /// You can use the bump alloactor of the scopestate to creat the dynamic node - fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>; + fn into_dyn_node(self, cx: &'a ScopeState) -> DynamicNode<'a>; } impl<'a> IntoDynNode<'a> for () { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::default() } } impl<'a> IntoDynNode<'a> for VNode<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(_cx.bump().alloc([self])) } } impl<'a> IntoDynNode<'a> for DynamicNode<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { self } } impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { match self { - Some(val) => val.into_vnode(_cx), + Some(val) => val.into_dyn_node(_cx), None => DynamicNode::default(), } } } impl<'a> IntoDynNode<'a> for &Element<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { match self.as_ref() { - Some(val) => val.clone().into_vnode(_cx), + Some(val) => val.clone().into_dyn_node(_cx), _ => DynamicNode::default(), } } } impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { - fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(cx.bump().alloc([cx.render(self).unwrap()])) } } impl<'a, 'b> IntoDynNode<'b> for &'a str { - fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { + fn into_dyn_node(self, cx: &'b ScopeState) -> DynamicNode<'b> { DynamicNode::Text(VText { value: cx.bump().alloc_str(self), id: Default::default(), @@ -705,7 +736,7 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str { } impl IntoDynNode<'_> for String { - fn into_vnode(self, cx: &ScopeState) -> DynamicNode { + fn into_dyn_node(self, cx: &ScopeState) -> DynamicNode { DynamicNode::Text(VText { value: cx.bump().alloc_str(&self), id: Default::default(), @@ -714,15 +745,16 @@ impl IntoDynNode<'_> for String { } impl<'b> IntoDynNode<'b> for Arguments<'_> { - fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { + fn into_dyn_node(self, cx: &'b ScopeState) -> DynamicNode<'b> { cx.text_node(self) } } impl<'a> IntoDynNode<'a> for &'a VNode<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(_cx.bump().alloc([VNode { - parent: self.parent, + parent: self.parent.clone(), + stable_id: self.stable_id.clone(), template: self.template.clone(), root_ids: self.root_ids.clone(), key: self.key, @@ -732,24 +764,24 @@ impl<'a> IntoDynNode<'a> for &'a VNode<'a> { } } -pub trait IntoTemplate<'a> { - fn into_template(self, _cx: &'a ScopeState) -> VNode<'a>; +pub trait IntoVNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> VNode<'a>; } -impl<'a> IntoTemplate<'a> for VNode<'a> { - fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { +impl<'a> IntoVNode<'a> for VNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> VNode<'a> { self } } -impl<'a> IntoTemplate<'a> for Element<'a> { - fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { +impl<'a> IntoVNode<'a> for Element<'a> { + fn into_vnode(self, cx: &'a ScopeState) -> VNode<'a> { match self { - Some(val) => val.into_template(cx), + Some(val) => val.into_vnode(cx), _ => VNode::empty(cx).unwrap(), } } } -impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> { - fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { +impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> { + fn into_vnode(self, cx: &'a ScopeState) -> VNode<'a> { cx.render(self).unwrap() } } @@ -759,12 +791,12 @@ pub struct FromNodeIterator; impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T where T: Iterator, - I: IntoTemplate<'a>, + I: IntoVNode<'a>, { - fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, cx: &'a ScopeState) -> DynamicNode<'a> { let mut nodes = bumpalo::collections::Vec::new_in(cx.bump()); - nodes.extend(self.into_iter().map(|node| node.into_template(cx))); + nodes.extend(self.into_iter().map(|node| node.into_vnode(cx))); match nodes.into_bump_slice() { children if children.is_empty() => DynamicNode::default(), diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index fc668b364..d2e9135e6 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -19,7 +19,7 @@ pub struct TaskId(pub usize); /// the task itself is the waker pub(crate) struct LocalTask { pub scope: ScopeId, - pub(super) task: RefCell + 'static>>>, + pub task: RefCell + 'static>>>, pub waker: Waker, } @@ -48,11 +48,15 @@ impl Scheduler { })), }; - entry.insert(task); + let mut cx = std::task::Context::from_waker(&task.waker); - self.sender - .unbounded_send(SchedulerMsg::TaskNotified(task_id)) - .expect("Scheduler should exist"); + if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { + self.sender + .unbounded_send(SchedulerMsg::TaskNotified(task_id)) + .expect("Scheduler should exist"); + } + + entry.insert(task); task_id } @@ -60,8 +64,8 @@ impl Scheduler { /// Drop the future with the given TaskId /// /// This does not abort the task, so you'll want to wrap it in an aborthandle if that's important to you - pub fn remove(&self, id: TaskId) { - self.tasks.borrow_mut().try_remove(id.0); + pub fn remove(&self, id: TaskId) -> Option { + self.tasks.borrow_mut().try_remove(id.0) } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 397f5328e..962a55e86 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -36,6 +36,7 @@ impl VirtualDom { borrowed_props: Default::default(), attributes_to_drop_before_render: Default::default(), + element_refs_to_drop: Default::default(), })); let context = diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 59bed3b1f..e4cb256d2 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -1,5 +1,5 @@ use crate::{ - innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, + innerlude::{Scheduler, SchedulerMsg}, runtime::{with_current_scope, with_runtime}, Element, ScopeId, TaskId, }; @@ -7,7 +7,6 @@ use rustc_hash::FxHashSet; use std::{ any::Any, cell::{Cell, RefCell}, - fmt::Debug, future::Future, rc::Rc, sync::Arc, @@ -230,17 +229,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. @@ -250,19 +239,6 @@ impl ScopeContext { self.tasks.remove(id); } - /// Inject an error into the nearest error boundary and quit rendering - /// - /// The error doesn't need to implement Error or any specific traits since the boundary - /// itself will downcast the error into a trait object. - pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { - if let Some(cx) = self.consume_context::>() { - cx.insert_error(self.scope_id(), Box::new(error)); - } - - // Always return none during a throw - None - } - /// Mark this component as suspended and then return None pub fn suspend(&self) -> Option { self.suspended.set(true); @@ -332,11 +308,6 @@ pub fn suspend() -> Option> { None } -/// Throw an error into the nearest error boundary -pub fn throw(error: impl Debug + 'static) -> Option<()> { - with_current_scope(|cx| cx.throw(error)).flatten() -} - /// Pushes the future onto the poll queue to be polled after the component renders. pub fn push_future(fut: impl Future + 'static) -> Option { with_current_scope(|cx| cx.push_future(fut)) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index f0bbd776c..1a26d3847 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -3,7 +3,7 @@ use crate::{ any_props::VProps, bump_frame::BumpFrame, innerlude::ErrorBoundary, - innerlude::{DynamicNode, EventHandler, VComponent, VText}, + innerlude::{DynamicNode, EventHandler, VComponent, VNodeId, VText}, lazynodes::LazyNodes, nodes::{IntoAttributeValue, IntoDynNode, RenderReturn}, runtime::Runtime, @@ -94,6 +94,7 @@ pub struct ScopeState { pub(crate) hook_idx: Cell, pub(crate) borrowed_props: RefCell>>, + pub(crate) element_refs_to_drop: RefCell>, pub(crate) attributes_to_drop_before_render: RefCell>>, pub(crate) props: Option>>, @@ -406,7 +407,7 @@ impl<'src> ScopeState { /// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] using the internal [`Bump`] allocator pub fn make_node<'c, I>(&'src self, into: impl IntoDynNode<'src, I> + 'c) -> DynamicNode { - into.into_vnode(self) + into.into_dyn_node(self) } /// Create a new [`Attribute`] from a name, value, namespace, and volatile bool @@ -466,7 +467,7 @@ impl<'src> ScopeState { render_fn: component as *const (), static_props: P::IS_STATIC, props: RefCell::new(Some(extended)), - scope: Cell::new(None), + scope: Default::default(), }) } @@ -517,19 +518,6 @@ impl<'src> ScopeState { AttributeValue::Any(RefCell::new(Some(boxed))) } - /// Inject an error into the nearest error boundary and quit rendering - /// - /// The error doesn't need to implement Error or any specific traits since the boundary - /// itself will downcast the error into a trait object. - pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { - if let Some(cx) = self.consume_context::>() { - cx.insert_error(self.scope_id(), Box::new(error)); - } - - // Always return none during a throw - None - } - /// Mark this component as suspended and then return None pub fn suspend(&self) -> Option { let cx = self.context(); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 2d83ba63a..f469cd9e2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -4,19 +4,19 @@ use crate::{ any_props::VProps, - arena::{ElementId, ElementRef}, - innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg}, + arena::ElementId, + innerlude::{DirtyScope, ElementRef, ErrorBoundary, Mutations, Scheduler, SchedulerMsg}, mutations::Mutation, nodes::RenderReturn, nodes::{Template, TemplateId}, runtime::{Runtime, RuntimeGuard}, scopes::{ScopeId, ScopeState}, - AttributeValue, Element, Event, Scope, + AttributeValue, Element, Event, Scope, VNode, }; use futures_util::{pin_mut, StreamExt}; use rustc_hash::{FxHashMap, FxHashSet}; use slab::Slab; -use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; +use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc, sync::Arc}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -186,7 +186,10 @@ pub struct VirtualDom { pub(crate) templates: FxHashMap>>, // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template - pub(crate) elements: Slab, + pub(crate) element_refs: Slab>>>, + + // The element ids that are used in the renderer + pub(crate) elements: Slab>, pub(crate) mutations: Mutations<'static>, @@ -263,6 +266,7 @@ impl VirtualDom { dirty_scopes: Default::default(), templates: Default::default(), elements: Default::default(), + element_refs: Default::default(), mutations: Mutations::default(), suspended_scopes: Default::default(), }; @@ -273,10 +277,13 @@ impl VirtualDom { ); // Unlike react, we provide a default error boundary that just renders the error as a string - root.provide_context(Rc::new(ErrorBoundary::new(ScopeId::ROOT))); + root.provide_context(Rc::new(ErrorBoundary::new_in_scope( + ScopeId::ROOT, + Arc::new(|_| {}), + ))); // the root element is always given element ID 0 since it's the container for the entire tree - dom.elements.insert(ElementRef::none()); + dom.elements.insert(None); dom } @@ -314,9 +321,9 @@ impl VirtualDom { } } - /// Call a listener inside the VirtualDom with data from outside the VirtualDom. + /// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an dynamic element, not a static node or a text node.** /// - /// This method will identify the appropriate element. The data must match up with the listener delcared. Note that + /// This method will identify the appropriate element. The data must match up with the listener declared. Note that /// this method does not give any indication as to the success of the listener call. If the listener is not found, /// nothing will happen. /// @@ -353,7 +360,15 @@ impl VirtualDom { | | | <-- no, broke early | <-- no, broke early */ - let mut parent_path = self.elements.get(element.0); + let parent_path = match self.elements.get(element.0) { + Some(Some(el)) => el, + _ => return, + }; + let mut parent_node = self + .element_refs + .get(parent_path.template.0) + .cloned() + .map(|el| (*parent_path, el)); let mut listeners = vec![]; // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required @@ -365,36 +380,72 @@ impl VirtualDom { // If the event bubbles, we traverse through the tree until we find the target element. if bubbles { // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent. - while let Some(el_ref) = parent_path { + while let Some((path, el_ref)) = parent_node { // safety: we maintain references of all vnodes in the element slab - if let Some(template) = el_ref.template { - let template = unsafe { template.as_ref() }; - let node_template = template.template.get(); - let target_path = el_ref.path; + let template = unsafe { el_ref.unwrap().as_ref() }; + let node_template = template.template.get(); + let target_path = path.path; - for (idx, attr) in template.dynamic_attrs.iter().enumerate() { - let this_path = node_template.attr_paths[idx]; + for (idx, attr) in template.dynamic_attrs.iter().enumerate() { + let this_path = node_template.attr_paths[idx]; - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - if attr.name.trim_start_matches("on") == name - && target_path.is_decendant(&this_path) - { - listeners.push(&attr.value); + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + if attr.name.trim_start_matches("on") == name + && target_path.is_decendant(&this_path) + { + listeners.push(&attr.value); - // Break if this is the exact target element. - // This means we won't call two listeners with the same name on the same element. This should be - // documented, or be rejected from the rsx! macro outright - if target_path == this_path { - break; - } + // Break if this is the exact target element. + // This means we won't call two listeners with the same name on the same element. This should be + // documented, or be rejected from the rsx! macro outright + if target_path == this_path { + break; } } + } - // Now that we've accumulated all the parent attributes for the target element, call them in reverse order - // We check the bubble state between each call to see if the event has been stopped from bubbling - for listener in listeners.drain(..).rev() { - if let AttributeValue::Listener(listener) = listener { - let origin = el_ref.scope; + // Now that we've accumulated all the parent attributes for the target element, call them in reverse order + // We check the bubble state between each call to see if the event has been stopped from bubbling + for listener in listeners.drain(..).rev() { + if let AttributeValue::Listener(listener) = listener { + let origin = path.scope; + self.runtime.scope_stack.borrow_mut().push(origin); + self.runtime.rendering.set(false); + if let Some(cb) = listener.borrow_mut().as_deref_mut() { + cb(uievent.clone()); + } + self.runtime.scope_stack.borrow_mut().pop(); + self.runtime.rendering.set(true); + + if !uievent.propagates.get() { + return; + } + } + } + + parent_node = template.parent.get().and_then(|element_ref| { + self.element_refs + .get(element_ref.template.0) + .cloned() + .map(|el| (element_ref, el)) + }); + } + } else { + // Otherwise, we just call the listener on the target element + if let Some((path, el_ref)) = parent_node { + // safety: we maintain references of all vnodes in the element slab + let template = unsafe { el_ref.unwrap().as_ref() }; + let node_template = template.template.get(); + let target_path = path.path; + + for (idx, attr) in template.dynamic_attrs.iter().enumerate() { + let this_path = node_template.attr_paths[idx]; + + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + // Only call the listener if this is the exact target element. + if attr.name.trim_start_matches("on") == name && target_path == this_path { + if let AttributeValue::Listener(listener) = &attr.value { + let origin = path.scope; self.runtime.scope_stack.borrow_mut().push(origin); self.runtime.rendering.set(false); if let Some(cb) = listener.borrow_mut().as_deref_mut() { @@ -403,44 +454,7 @@ impl VirtualDom { self.runtime.scope_stack.borrow_mut().pop(); self.runtime.rendering.set(true); - if !uievent.propagates.get() { - return; - } - } - } - - parent_path = template.parent.and_then(|id| self.elements.get(id.0)); - } else { - break; - } - } - } else { - // Otherwise, we just call the listener on the target element - if let Some(el_ref) = parent_path { - // safety: we maintain references of all vnodes in the element slab - if let Some(template) = el_ref.template { - let template = unsafe { template.as_ref() }; - let node_template = template.template.get(); - let target_path = el_ref.path; - - for (idx, attr) in template.dynamic_attrs.iter().enumerate() { - let this_path = node_template.attr_paths[idx]; - - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - // Only call the listener if this is the exact target element. - if attr.name.trim_start_matches("on") == name && target_path == this_path { - if let AttributeValue::Listener(listener) = &attr.value { - let origin = el_ref.scope; - self.runtime.scope_stack.borrow_mut().push(origin); - self.runtime.rendering.set(false); - if let Some(cb) = listener.borrow_mut().as_deref_mut() { - cb(uievent.clone()); - } - self.runtime.scope_stack.borrow_mut().pop(); - self.runtime.rendering.set(true); - - break; - } + break; } } } @@ -563,7 +577,7 @@ impl VirtualDom { // If an error occurs, we should try to render the default error component and context where the error occured RenderReturn::Aborted(placeholder) => { tracing::debug!("Ran into suspended or aborted scope during rebuild"); - let id = self.next_null(); + let id = self.next_element(); placeholder.id.set(Some(id)); self.mutations.push(Mutation::CreatePlaceholder { id }); } @@ -595,15 +609,12 @@ impl VirtualDom { /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content pub async fn wait_for_suspense(&mut self) { loop { - // println!("waiting for suspense {:?}", self.suspended_scopes); if self.suspended_scopes.is_empty() { return; } - // println!("waiting for suspense"); self.wait_for_work().await; - // println!("Rendered immediately"); _ = self.render_immediate(); } } diff --git a/packages/core/tests/error_boundary.rs b/packages/core/tests/error_boundary.rs index ce26fba31..58d861169 100644 --- a/packages/core/tests/error_boundary.rs +++ b/packages/core/tests/error_boundary.rs @@ -24,9 +24,9 @@ fn NoneChild(_cx: Scope) -> Element { } fn ThrowChild(cx: Scope) -> Element { - cx.throw(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?; + Err(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd")).throw()?; - let _g: i32 = "123123".parse().throw(cx)?; + let _g: i32 = "123123".parse().throw()?; cx.render(rsx! { div {} diff --git a/packages/core/tests/event_propagation.rs b/packages/core/tests/event_propagation.rs new file mode 100644 index 000000000..d694b1ff1 --- /dev/null +++ b/packages/core/tests/event_propagation.rs @@ -0,0 +1,69 @@ +use dioxus::prelude::*; +use dioxus_core::ElementId; +use std::{rc::Rc, sync::Mutex}; + +static CLICKS: Mutex = Mutex::new(0); + +#[test] +fn events_propagate() { + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + // Top-level click is registered + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true); + assert_eq!(*CLICKS.lock().unwrap(), 1); + + // break reference.... + for _ in 0..5 { + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + } + + // Lower click is registered + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + assert_eq!(*CLICKS.lock().unwrap(), 3); + + // break reference.... + for _ in 0..5 { + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + } + + // Stop propagation occurs + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + assert_eq!(*CLICKS.lock().unwrap(), 3); +} + +fn app(cx: Scope) -> Element { + render! { + div { + onclick: move |_| { + println!("top clicked"); + *CLICKS.lock().unwrap() += 1; + }, + + vec![ + render! { + problematic_child {} + } + ].into_iter() + } + } +} + +fn problematic_child(cx: Scope) -> Element { + render! { + button { + onclick: move |evt| { + println!("bottom clicked"); + let mut clicks = CLICKS.lock().unwrap(); + + if *clicks == 3 { + evt.stop_propagation(); + } else { + *clicks += 1; + } + } + } + } +} diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index 1523c4e22..65d131eb8 100644 --- a/packages/core/tests/fuzzing.rs +++ b/packages/core/tests/fuzzing.rs @@ -2,7 +2,7 @@ use dioxus::prelude::Props; use dioxus_core::*; -use std::{cell::Cell, collections::HashSet}; +use std::{cfg, collections::HashSet}; fn random_ns() -> Option<&'static str> { let namespace = rand::random::() % 2; @@ -170,22 +170,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { let range = if depth > 5 { 1 } else { 4 }; match rand::random::() % range { 0 => DynamicNode::Placeholder(Default::default()), - 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| VNode { - key: None, - parent: Default::default(), - template: Cell::new(Template { - name: concat!(file!(), ":", line!(), ":", column!(), ":0"), - roots: &[TemplateNode::Dynamic { id: 0 }], - node_paths: &[&[0]], - attr_paths: &[], - }), - root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(), - dynamic_nodes: cx.bump().alloc([cx.component( - create_random_element, - DepthProps { depth, root: false }, - "create_random_element", - )]), - dynamic_attrs: &[], + 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| { + VNode::new( + None, + Template { + name: concat!(file!(), ":", line!(), ":", column!(), ":0"), + roots: &[TemplateNode::Dynamic { id: 0 }], + node_paths: &[&[0]], + attr_paths: &[], + }, + bumpalo::collections::Vec::new_in(cx.bump()), + cx.bump().alloc([cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + )]), + &[], + ) })), 2 => cx.component( create_random_element, @@ -271,13 +272,11 @@ fn create_random_element(cx: Scope) -> Element { ) .into_boxed_str(), )); - // println!("{template:#?}"); - let node = VNode { - key: None, - parent: None, - template: Cell::new(template), - root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(), - dynamic_nodes: { + let node = VNode::new( + None, + template, + bumpalo::collections::Vec::new_in(cx.bump()), + { let dynamic_nodes: Vec<_> = dynamic_node_types .iter() .map(|ty| match ty { @@ -291,12 +290,12 @@ fn create_random_element(cx: Scope) -> Element { .collect(); cx.bump().alloc(dynamic_nodes) }, - dynamic_attrs: cx.bump().alloc( + cx.bump().alloc( (0..template.attr_paths.len()) .map(|_| create_random_dynamic_attr(cx)) .collect::>(), ), - }; + ); Some(node) } _ => None, @@ -306,10 +305,10 @@ fn create_random_element(cx: Scope) -> Element { } // test for panics when creating random nodes and templates -#[cfg(not(miri))] #[test] fn create() { - for _ in 0..1000 { + let repeat_count = if cfg!(miri) { 100 } else { 1000 }; + for _ in 0..repeat_count { let mut vdom = VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); let _ = vdom.rebuild(); @@ -318,10 +317,10 @@ fn create() { // test for panics when diffing random nodes // This test will change the template every render which is not very realistic, but it helps stress the system -#[cfg(not(miri))] #[test] fn diff() { - for _ in 0..100000 { + let repeat_count = if cfg!(miri) { 100 } else { 1000 }; + for _ in 0..repeat_count { let mut vdom = VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); let _ = vdom.rebuild(); diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index 191d1da7c..0abe3756e 100644 --- a/packages/core/tests/kitchen_sink.rs +++ b/packages/core/tests/kitchen_sink.rs @@ -10,6 +10,8 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element { div { key: "12345", class: "asd", class: "{asd}", + class: if true { "{asd}" }, + class: if false { "{asd}" }, onclick: move |_| {}, div { "{var}" } div { @@ -24,6 +26,7 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element { } }) } + #[test] fn dual_stream() { let mut dom = VirtualDom::new(basic_syntax_is_a_template); @@ -36,7 +39,7 @@ fn dual_stream() { LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetAttribute { name: "class", - value: (&*bump.alloc("123".into_value(&bump))).into(), + value: (&*bump.alloc("asd 123 123".into_value(&bump))).into(), id: ElementId(1), ns: None, }, diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore new file mode 100644 index 000000000..1aec3a9f9 --- /dev/null +++ b/packages/desktop/.gitignore @@ -0,0 +1 @@ +/src/minified.js \ No newline at end of file diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index f0ea9e189..3d8c2d31b 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -12,14 +12,14 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-core = { workspace = true, features = ["serialize"] } dioxus-html = { workspace = true, features = ["serialize", "native-bind"] } -dioxus-interpreter-js = { workspace = true } +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } 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", @@ -33,14 +33,17 @@ webbrowser = "0.8.0" infer = "0.11.0" dunce = "1.0.2" slab = { workspace = true } +rustc-hash = { 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,7 +59,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 = [] @@ -72,6 +74,10 @@ dioxus = { workspace = true } exitcode = "1.1.2" scraper = "0.16.0" +[build-dependencies] +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } +minify-js = "0.5.6" + # These tests need to be run on the main thread, so they cannot use rust's test harness. [[test]] name = "check_events" diff --git a/packages/desktop/build.rs b/packages/desktop/build.rs index 2061732e8..8d1f4b3e5 100644 --- a/packages/desktop/build.rs +++ b/packages/desktop/build.rs @@ -1,4 +1,19 @@ -fn main() { +use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS; + +use std::io::Write; + +const EDITS_PATH: &str = { + #[cfg(any(target_os = "android", target_os = "windows"))] + { + "http://dioxus.index.html/edits" + } + #[cfg(not(any(target_os = "android", target_os = "windows")))] + { + "dioxus://index.html/edits" + } +}; + +fn check_gnu() { // WARN about wry support on windows gnu targets. GNU windows targets don't work well in wry currently if std::env::var("CARGO_CFG_WINDOWS").is_ok() && std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu" @@ -7,3 +22,73 @@ fn main() { println!("cargo:warning=GNU windows targets have some limitations within Wry. Using the MSVC windows toolchain is recommended. If you would like to use continue using GNU, you can read https://github.com/wravery/webview2-rs#cross-compilation and disable this warning by adding the gnu feature to dioxus-desktop in your Cargo.toml") } } + +fn main() { + check_gnu(); + + let prevent_file_upload = r#"// Prevent file inputs from opening the file dialog on click + let inputs = document.querySelectorAll("input"); + for (let input of inputs) { + if (!input.getAttribute("data-dioxus-file-listener")) { + // prevent file inputs from opening the file dialog on click + const type = input.getAttribute("type"); + if (type === "file") { + input.setAttribute("data-dioxus-file-listener", true); + input.addEventListener("click", (event) => { + let target = event.target; + let target_id = find_real_id(target); + if (target_id !== null) { + const send = (event_name) => { + const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); + window.ipc.postMessage(message); + }; + send("change&input"); + } + event.preventDefault(); + }); + } + } + }"#; + let polling_request = format!( + r#"// Poll for requests + window.interpreter.wait_for_request = (headless) => {{ + fetch(new Request("{EDITS_PATH}")) + .then(response => {{ + response.arrayBuffer() + .then(bytes => {{ + // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly + if (headless) {{ + run_from_bytes(bytes); + }} + else {{ + requestAnimationFrame(() => {{ + run_from_bytes(bytes); + }}); + }} + window.interpreter.wait_for_request(headless); + }}); + }}) + }}"# + ); + let mut interpreter = SLEDGEHAMMER_JS + .replace("/*POST_HANDLE_EDITS*/", prevent_file_upload) + .replace("export", "") + + &polling_request; + while let Some(import_start) = interpreter.find("import") { + let import_end = interpreter[import_start..] + .find(|c| c == ';' || c == '\n') + .map(|i| i + import_start) + .unwrap_or_else(|| interpreter.len()); + interpreter.replace_range(import_start..import_end, ""); + } + + let js = format!("{interpreter}\nconst config = new InterpreterConfig(false);"); + + use minify_js::*; + let session = Session::new(); + let mut out = Vec::new(); + minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap(); + let minified = String::from_utf8(out).unwrap(); + let mut file = std::fs::File::create("src/minified.js").unwrap(); + file.write_all(minified.as_bytes()).unwrap(); +} diff --git a/packages/desktop/examples/stress.rs b/packages/desktop/examples/stress.rs new file mode 100644 index 000000000..5f317bc99 --- /dev/null +++ b/packages/desktop/examples/stress.rs @@ -0,0 +1,32 @@ +use dioxus::prelude::*; + +fn app(cx: Scope) -> Element { + let state = use_state(cx, || 0); + use_future(cx, (), |_| { + to_owned![state]; + async move { + loop { + state += 1; + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + } + } + }); + + cx.render(rsx! { + button { + onclick: move |_| { + state.set(0); + }, + "reset" + } + for _ in 0..10000 { + div { + "hello desktop! {state}" + } + } + }) +} + +fn main() { + dioxus_desktop::launch(app); +} diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index ac62cd37b..b0aef5f79 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -52,7 +52,7 @@ fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) { #[allow(deprecated)] fn app(cx: Scope) -> Element { let desktop_context: DesktopContext = cx.consume_context().unwrap(); - let recieved_events = use_state(cx, || 0); + let received_events = use_state(cx, || 0); // button mock_event( @@ -216,7 +216,7 @@ fn app(cx: Scope) -> Element { r#"new FocusEvent("focusout",{bubbles: true})"#, ); - if **recieved_events == 12 { + if **received_events == 12 { println!("all events recieved"); desktop_context.close(); } @@ -230,8 +230,14 @@ fn app(cx: Scope) -> Element { 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)); - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) }, + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); + recieved_events.modify(|x| *x + 1) + } } div { id: "mouse_move_div", @@ -239,8 +245,8 @@ fn app(cx: Scope) -> Element { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); - recieved_events.modify(|x| *x + 1) - }, + received_events.modify(|x| *x + 1) + } } div { id: "mouse_click_div", @@ -249,55 +255,54 @@ fn app(cx: Scope) -> Element { 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) - }, + received_events.modify(|x| *x + 1) + } } - div{ + div { id: "mouse_dblclick_div", - ondblclick: move |event| { + ondoubleclick: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); 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) + received_events.modify(|x| *x + 1) } } - div{ + 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)); - recieved_events.modify(|x| *x + 1) + received_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)); - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } - div{ + div { id: "wheel_div", width: "100px", height: "100px", background_color: "red", onwheel: move |event| { println!("{:?}", event.data); - let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{ - panic!("Expected delta to be in pixels") - }; + let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else { + panic!("Expected delta to be in pixels") }; assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0)); - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } - input{ + input { id: "key_down_div", onkeydown: move |event| { println!("{:?}", event.data); @@ -306,11 +311,11 @@ fn app(cx: Scope) -> Element { assert_eq!(event.data.code().to_string(), "KeyA"); assert_eq!(event.data.location, 0); assert!(event.data.is_auto_repeating()); + received_events.modify(|x| *x + 1) - recieved_events.modify(|x| *x + 1) } } - input{ + input { id: "key_up_div", onkeyup: move |event| { println!("{:?}", event.data); @@ -319,11 +324,10 @@ fn app(cx: Scope) -> Element { assert_eq!(event.data.code().to_string(), "KeyA"); assert_eq!(event.data.location, 0); assert!(!event.data.is_auto_repeating()); - - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } - input{ + input { id: "key_press_div", onkeypress: move |event| { println!("{:?}", event.data); @@ -332,22 +336,21 @@ 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) + received_events.modify(|x| *x + 1) } } - input{ + input { id: "focus_in_div", onfocusin: move |event| { println!("{:?}", event.data); - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } - input{ + input { id: "focus_out_div", onfocusout: move |event| { println!("{:?}", event.data); - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } } diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 07281e364..596b21938 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -6,7 +6,6 @@ use wry::{ application::window::{Window, WindowBuilder}, http::{Request as HttpRequest, Response as HttpResponse}, webview::FileDropEvent, - Result as WryResult, }; // pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); @@ -36,13 +35,14 @@ 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>; pub(crate) type WryProtocol = ( String, - Box>) -> WryResult>> + 'static>, + Box>) -> HttpResponse> + 'static>, ); impl Config { @@ -65,9 +65,18 @@ impl Config { root_name: "main".to_string(), background_color: None, last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp, + enable_default_menu_bar: true, } } + /// Set whether the default menu bar should be enabled. + /// + /// > Note: `enable` is `true` by default. To disable the default menu bar pass `false`. + pub fn with_default_menu_bar(mut self, enable: bool) -> Self { + self.enable_default_menu_bar = enable; + self + } + /// set the directory from which assets will be searched in release mode pub fn with_resource_directory(mut self, path: impl Into) -> Self { self.resource_dir = Some(path.into()); @@ -120,7 +129,7 @@ impl Config { /// Set a custom protocol pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self where - F: Fn(&HttpRequest>) -> WryResult>> + 'static, + F: Fn(HttpRequest>) -> HttpResponse> + 'static, { self.protocols.push((name, Box::new(handler))); self diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 897f8eba8..79c943851 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,21 +1,27 @@ -use std::cell::RefCell; -use std::rc::Rc; -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; use dioxus_core::VirtualDom; #[cfg(all(feature = "hot-reload", debug_assertions))] use dioxus_hot_reload::HotReloadMsg; +use dioxus_interpreter_js::binary_protocol::Channel; +use rustc_hash::FxHashMap; use slab::Slab; -use wry::application::accelerator::Accelerator; +use std::cell::RefCell; +use std::fmt::Debug; +use std::fmt::Formatter; +use std::rc::Rc; +use std::rc::Weak; +use std::sync::atomic::AtomicU16; +use std::sync::Arc; +use std::sync::Mutex; use wry::application::event::Event; use wry::application::event_loop::EventLoopProxy; use wry::application::event_loop::EventLoopWindowTarget; @@ -28,13 +34,62 @@ use wry::webview::WebView; pub type ProxyType = EventLoopProxy; +/// Get an imperative handle to the current window without using a hook +/// +/// ## Panics +/// +/// This function will panic if it is called outside of the context of a Dioxus App. +pub fn window() -> DesktopContext { + dioxus_core::prelude::consume_context().unwrap() +} + /// Get an imperative handle to the current window +#[deprecated = "Prefer the using the `window` function directly for cleaner code"] pub fn use_window(cx: &ScopeState) -> &DesktopContext { cx.use_hook(|| cx.consume_context::()) .as_ref() .unwrap() } +/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events. +/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made. +#[derive(Default, Clone)] +pub(crate) struct EditQueue { + queue: Arc>>>, + responder: Arc>>, +} + +impl Debug for EditQueue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EditQueue") + .field("queue", &self.queue) + .field("responder", { + &self.responder.lock().unwrap().as_ref().map(|_| ()) + }) + .finish() + } +} + +impl EditQueue { + pub fn handle_request(&self, responder: wry::webview::RequestAsyncResponder) { + let mut queue = self.queue.lock().unwrap(); + if let Some(bytes) = queue.pop() { + responder.respond(wry::http::Response::new(bytes)); + } else { + *self.responder.lock().unwrap() = Some(responder); + } + } + + pub fn add_edits(&self, edits: Vec) { + let mut responder = self.responder.lock().unwrap(); + if let Some(responder) = responder.take() { + responder.respond(wry::http::Response::new(edits)); + } else { + self.queue.lock().unwrap().push(edits); + } + } +} + pub(crate) type WebviewQueue = Rc>>; /// An imperative interface to the current window. @@ -67,6 +122,13 @@ pub struct DesktopService { pub(crate) shortcut_manager: ShortcutRegistry, + pub(crate) edit_queue: EditQueue, + pub(crate) templates: RefCell>, + pub(crate) max_template_count: AtomicU16, + + pub(crate) channel: RefCell, + pub(crate) asset_handlers: AssetHandlerRegistry, + #[cfg(target_os = "ios")] pub(crate) views: Rc>>, } @@ -91,6 +153,8 @@ impl DesktopService { webviews: WebviewQueue, event_handlers: WindowEventHandlers, shortcut_manager: ShortcutRegistry, + edit_queue: EditQueue, + asset_handlers: AssetHandlerRegistry, ) -> Self { Self { webview: Rc::new(webview), @@ -100,6 +164,11 @@ impl DesktopService { pending_windows: webviews, event_handlers, shortcut_manager, + edit_queue, + templates: Default::default(), + max_template_count: Default::default(), + channel: Default::default(), + asset_handlers, #[cfg(target_os = "ios")] views: Default::default(), } @@ -233,11 +302,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 +319,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 +452,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/element.rs b/packages/desktop/src/element.rs index 20ca951bf..b5f12c3f2 100644 --- a/packages/desktop/src/element.rs +++ b/packages/desktop/src/element.rs @@ -30,7 +30,7 @@ impl RenderedElementBacking for DesktopElement { >, >, > { - let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + let script = format!("return window.interpreter.getClientRect({});", self.id.0); let fut = self .query @@ -54,7 +54,7 @@ impl RenderedElementBacking for DesktopElement { behavior: dioxus_html::ScrollBehavior, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.ScrollTo({}, {});", + "return window.interpreter.scrollTo({}, {});", self.id.0, serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") ); @@ -81,7 +81,7 @@ impl RenderedElementBacking for DesktopElement { focus: bool, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.SetFocus({}, {});", + "return window.interpreter.setFocus({}, {});", self.id.0, focus ); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 63a8914f3..63f030b3f 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -10,32 +10,36 @@ 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::{ - use_window, use_wry_event_handler, DesktopService, WryEventHandler, WryEventHandlerId, + use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId, }; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; -use dioxus_html::MountedData; +use dioxus_html::{event_bubbles, MountedData}; use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; +use dioxus_interpreter_js::binary_protocol::Channel; use element::DesktopElement; use eval::init_eval; use futures_util::{pin_mut, FutureExt}; +use rustc_hash::FxHashMap; +pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse}; use shortcut::ShortcutRegistry; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; use std::cell::Cell; use std::rc::Rc; +use std::sync::atomic::AtomicU16; use std::task::Waker; use std::{collections::HashMap, sync::Arc}; pub use tao::dpi::{LogicalSize, PhysicalSize}; @@ -43,10 +47,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 +126,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 +163,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 +173,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, .. @@ -272,7 +283,10 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) let evt = match serde_json::from_value::(params) { Ok(value) => value, - Err(_) => return, + Err(err) => { + tracing::error!("Error parsing user_event: {:?}", err); + return; + } }; let HtmlEvent { @@ -304,7 +318,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) view.dom.handle_event(&name, as_any, element, bubbles); - send_edits(view.dom.render_immediate(), &view.desktop_context.webview); + send_edits(view.dom.render_immediate(), &view.desktop_context); } // When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query @@ -327,7 +341,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) EventData::Ipc(msg) if msg.method() == "initialize" => { let view = webviews.get_mut(&event.1).unwrap(); - send_edits(view.dom.rebuild(), &view.desktop_context.webview); + send_edits(view.dom.rebuild(), &view.desktop_context); view.desktop_context .webview .window() @@ -369,13 +383,12 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) view.dom.handle_event(event_name, data, id, event_bubbles); } - send_edits(view.dom.render_immediate(), &view.desktop_context.webview); + send_edits(view.dom.render_immediate(), &view.desktop_context); } } _ => {} }, - Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id), _ => {} } }) @@ -390,7 +403,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, edit_queue) = + webview::build(&mut cfg, event_loop, proxy.clone()); let desktop_context = Rc::from(DesktopService::new( webview, proxy.clone(), @@ -398,6 +412,8 @@ fn create_new_window( queue.clone(), event_handlers.clone(), shortcut_manager, + asset_handlers, + edit_queue, )); let cx = dom.base_scope(); @@ -444,16 +460,149 @@ fn poll_vdom(view: &mut WebviewHandler) { } } - send_edits(view.dom.render_immediate(), &view.desktop_context.webview); + send_edits(view.dom.render_immediate(), &view.desktop_context); } } /// Send a list of mutations to the webview -fn send_edits(edits: Mutations, webview: &WebView) { - let serialized = serde_json::to_string(&edits).unwrap(); +fn send_edits(edits: Mutations, desktop_context: &DesktopContext) { + let mut channel = desktop_context.channel.borrow_mut(); + let mut templates = desktop_context.templates.borrow_mut(); + if let Some(bytes) = apply_edits( + edits, + &mut channel, + &mut templates, + &desktop_context.max_template_count, + ) { + desktop_context.edit_queue.add_edits(bytes) + } +} - // todo: use SSE and binary data to send the edits with lower overhead - _ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})")); +fn apply_edits( + mutations: Mutations, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &AtomicU16, +) -> Option> { + use dioxus_core::Mutation::*; + if mutations.templates.is_empty() && mutations.edits.is_empty() { + return None; + } + for template in mutations.templates { + add_template(&template, channel, templates, max_template_count); + } + for edit in mutations.edits { + match edit { + AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16), + AssignId { path, id } => channel.assign_id(path, id.0 as u32), + CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32), + CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32), + HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32), + LoadTemplate { name, index, id } => { + if let Some(tmpl_id) = templates.get(name) { + channel.load_template(*tmpl_id, index as u16, id.0 as u32) + } + } + ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16), + ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16), + InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16), + InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16), + SetAttribute { + name, + value, + id, + ns, + } => match value { + BorrowedAttributeValue::Text(txt) => { + channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default()) + } + BorrowedAttributeValue::Float(f) => { + channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Int(n) => { + channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Bool(b) => channel.set_attribute( + id.0 as u32, + name, + if b { "true" } else { "false" }, + ns.unwrap_or_default(), + ), + BorrowedAttributeValue::None => { + channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default()) + } + _ => unreachable!(), + }, + SetText { value, id } => channel.set_text(id.0 as u32, value), + NewEventListener { name, id, .. } => { + channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + RemoveEventListener { name, id } => { + channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + Remove { id } => channel.remove(id.0 as u32), + PushRoot { id } => channel.push_root(id.0 as u32), + } + } + + let bytes: Vec<_> = channel.export_memory().collect(); + channel.reset(); + Some(bytes) +} + +fn add_template( + template: &Template<'static>, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &AtomicU16, +) { + let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed); + for root in template.roots.iter() { + create_template_node(channel, root); + templates.insert(template.name.to_owned(), current_max_template_count); + } + channel.add_templates(current_max_template_count, template.roots.len() as u16); + + max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); +} + +fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { + use TemplateNode::*; + match v { + Element { + tag, + namespace, + attrs, + children, + .. + } => { + // Push the current node onto the stack + match namespace { + Some(ns) => channel.create_element_ns(tag, ns), + None => channel.create_element(tag), + } + // Set attributes on the current node + for attr in *attrs { + if let TemplateAttribute::Static { + name, + value, + namespace, + } = attr + { + channel.set_top_attribute(name, value, namespace.unwrap_or_default()) + } + } + // Add each child to the stack + for child in *children { + create_template_node(channel, child); + } + // Add all children to the parent + channel.append_children_to_top(children.len() as u16); + } + Text { text } => channel.create_raw_text(text), + DynamicText { .. } => channel.create_raw_text("p"), + Dynamic { .. } => channel.add_placeholder(), + } } /// Different hide implementations per platform 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..f00221d85 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,14 +1,31 @@ +use dioxus_core::ScopeState; use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS}; +use slab::Slab; use std::{ borrow::Cow, + future::Future, + ops::Deref, path::{Path, PathBuf}, + pin::Pin, + rc::Rc, + sync::Arc, +}; +use tokio::{ + runtime::Handle, + sync::{OnceCell, RwLock}, }; use wry::{ http::{status::StatusCode, Request, Response}, + webview::RequestAsyncResponder, Result, }; +use crate::{use_window, DesktopContext}; -fn module_loader(root_name: &str) -> String { +use crate::desktop_context::EditQueue; + +static MINIFIED: &str = include_str!("./minified.js"); + +fn module_loader(root_name: &str, headless: bool) -> String { let js = INTERPRETER_JS.replace( "/*POST_HANDLE_EDITS*/", r#"// Prevent file inputs from opening the file dialog on click @@ -35,35 +52,188 @@ fn module_loader(root_name: &str) -> String { } }"#, ); + format!( r#" "# ) } -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, + edit_queue: &EditQueue, + headless: bool, +) -> 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. // we'll look for the closing tag and insert our little module loader there. let body = match custom_index { Some(custom_index) => custom_index - .replace("", &format!("{}", module_loader(root_name))) + .replace( + "", + &format!("{}", module_loader(root_name, headless)), + ) .into_bytes(), None => { @@ -75,47 +245,83 @@ pub(super) fn desktop_handler( } template - .replace("", &module_loader(root_name)) + .replace( + "", + &module_loader(root_name, headless), + ) .into_bytes() } }; - return Response::builder() + match Response::builder() .header("Content-Type", "text/html") + .header("Access-Control-Allow-Origin", "*") .body(Cow::from(body)) - .map_err(From::from); - } else if request.uri().path() == "/common.js" { - return Response::builder() - .header("Content-Type", "text/javascript") - .body(Cow::from(COMMON_JS.as_bytes())) - .map_err(From::from); + { + Ok(response) => { + responder.respond(response); + return; + } + Err(err) => tracing::error!("error building response: {}", err), + } + } else if request.uri().path().trim_matches('/') == "edits" { + edit_queue.handle_request(responder); + return; + } + + // If the user provided a custom asset handler, then call it and return the response + // if the request was handled. + if 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() { - return Response::builder() - .header("Content-Type", get_mime_from_path(&asset)?) - .body(Cow::from(std::fs::read(asset)?)) - .map_err(From::from); + let content_type = match get_mime_from_path(&asset) { + Ok(content_type) => content_type, + Err(err) => { + tracing::error!("error getting mime type: {}", err); + return; + } + }; + let asset = match std::fs::read(asset) { + Ok(asset) => asset, + Err(err) => { + tracing::error!("error reading asset: {}", err); + return; + } + }; + match Response::builder() + .header("Content-Type", content_type) + .body(Cow::from(asset)) + { + Ok(response) => { + responder.respond(response); + return; + } + Err(err) => tracing::error!("error building response: {}", err), + } } - Response::builder() + match Response::builder() .status(StatusCode::NOT_FOUND) .body(Cow::from(String::from("Not Found").into_bytes())) - .map_err(From::from) + { + Ok(response) => { + responder.respond(response); + } + Err(err) => tracing::error!("error building response: {}", err), + } } #[allow(unreachable_code)] 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..da5c78de2 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,17 +1,19 @@ -use crate::desktop_context::EventData; -use crate::protocol; +use crate::desktop_context::{EditQueue, EventData}; +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( +pub(crate) fn build( cfg: &mut Config, event_loop: &EventLoopWindowTarget, proxy: EventLoopProxy, -) -> (WebView, WebContext) { +) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue) { let builder = cfg.window.clone(); let window = builder.with_visible(false).build(event_loop).unwrap(); let file_handler = cfg.file_drop_handler.take(); @@ -19,6 +21,12 @@ pub fn build( 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 +40,10 @@ pub fn build( } let mut web_context = WebContext::new(cfg.data_dir.clone()); + let edit_queue = EditQueue::default(); + let headless = !cfg.window.window.visible; + let asset_handlers = AssetHandlerRegistry::new(); + let asset_handlers_ref = asset_handlers.clone(); let mut webview = WebViewBuilder::new(window) .unwrap() @@ -44,8 +56,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 +97,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 +129,63 @@ pub fn build( webview = webview.with_devtools(true); } - (webview.build().unwrap(), web_context) + (webview.build().unwrap(), web_context, asset_handlers, edit_queue) +} + +/// 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/hooks/src/use_effect.rs b/packages/hooks/src/use_effect.rs index 6fb2fcf9e..72744e8ae 100644 --- a/packages/hooks/src/use_effect.rs +++ b/packages/hooks/src/use_effect.rs @@ -99,9 +99,8 @@ where // Create the new future let return_value = future(dependencies.out()); - if let Some(task) = return_value.apply(state.cleanup.clone(), cx) { - state.task.set(Some(task)); - } + let task = return_value.apply(state.cleanup.clone(), cx); + state.task.set(Some(task)); } } @@ -109,15 +108,15 @@ type UseEffectCleanup = Rc>>>; /// Something that can be returned from a `use_effect` hook. pub trait UseEffectReturn { - fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> Option; + fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> TaskId; } impl UseEffectReturn<()> for T where T: Future + 'static, { - fn apply(self, _: UseEffectCleanup, cx: &ScopeState) -> Option { - Some(cx.push_future(self)) + fn apply(self, _: UseEffectCleanup, cx: &ScopeState) -> TaskId { + cx.push_future(self) } } @@ -128,12 +127,11 @@ where T: Future + 'static, F: FnOnce() + 'static, { - fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> Option { - let task = cx.push_future(async move { + fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> TaskId { + cx.push_future(async move { let cleanup = self.await; *oncleanup.borrow_mut() = Some(Box::new(cleanup) as Box); - }); - Some(task) + }) } } 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/eval.rs b/packages/html/src/eval.rs index 292de70fc..394713af6 100644 --- a/packages/html/src/eval.rs +++ b/packages/html/src/eval.rs @@ -48,6 +48,15 @@ pub fn use_eval(cx: &ScopeState) -> &EvalCreator { }) } +pub fn eval(script: &str) -> Result { + let eval_provider = dioxus_core::prelude::consume_context::>() + .expect("evaluator not provided"); + + eval_provider + .new_evaluator(script.to_string()) + .map(UseEval::new) +} + /// A wrapper around the target platform's evaluator. #[derive(Clone)] pub struct UseEval { 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/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 7298cd4bd..7d046fbc8 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -14,13 +14,14 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] wasm-bindgen = { workspace = true, optional = true } js-sys = { version = "0.3.56", optional = true } web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } -sledgehammer_bindgen = { version = "0.2.1", optional = true } +sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true } sledgehammer_utils = { version = "0.2", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } [features] default = [] serialize = ["serde"] -web = ["wasm-bindgen", "js-sys", "web-sys"] -sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"] +sledgehammer = ["sledgehammer_bindgen", "sledgehammer_utils"] +web = ["sledgehammer", "wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen/web"] +binary-protocol = ["sledgehammer", "wasm-bindgen"] minimal_bindings = [] diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs deleted file mode 100644 index 4fd9016f1..000000000 --- a/packages/interpreter/src/bindings.rs +++ /dev/null @@ -1,83 +0,0 @@ -#![allow(clippy::unused_unit, non_upper_case_globals)] - -use js_sys::Function; -use wasm_bindgen::prelude::*; -use web_sys::Element; - -#[wasm_bindgen(module = "/src/interpreter.js")] -extern "C" { - pub type InterpreterConfig; - #[wasm_bindgen(constructor)] - pub fn new(intercept_link_redirects: bool) -> InterpreterConfig; - - pub type Interpreter; - - #[wasm_bindgen(constructor)] - pub fn new(arg: Element, config: InterpreterConfig) -> Interpreter; - - #[wasm_bindgen(method)] - pub fn SaveTemplate(this: &Interpreter, template: JsValue); - - #[wasm_bindgen(method)] - pub fn MountToRoot(this: &Interpreter); - - #[wasm_bindgen(method)] - pub fn AssignId(this: &Interpreter, path: &[u8], id: u32); - - #[wasm_bindgen(method)] - pub fn CreatePlaceholder(this: &Interpreter, id: u32); - - #[wasm_bindgen(method)] - pub fn CreateTextNode(this: &Interpreter, value: JsValue, id: u32); - - #[wasm_bindgen(method)] - pub fn HydrateText(this: &Interpreter, path: &[u8], value: &str, id: u32); - - #[wasm_bindgen(method)] - pub fn LoadTemplate(this: &Interpreter, name: &str, index: u32, id: u32); - - #[wasm_bindgen(method)] - pub fn ReplaceWith(this: &Interpreter, id: u32, m: u32); - - #[wasm_bindgen(method)] - pub fn ReplacePlaceholder(this: &Interpreter, path: &[u8], m: u32); - - #[wasm_bindgen(method)] - pub fn InsertAfter(this: &Interpreter, id: u32, n: u32); - - #[wasm_bindgen(method)] - pub fn InsertBefore(this: &Interpreter, id: u32, n: u32); - - #[wasm_bindgen(method)] - pub fn SetAttribute(this: &Interpreter, id: u32, name: &str, value: JsValue, ns: Option<&str>); - - #[wasm_bindgen(method)] - pub fn SetBoolAttribute(this: &Interpreter, id: u32, name: &str, value: bool); - - #[wasm_bindgen(method)] - pub fn SetText(this: &Interpreter, id: u32, text: JsValue); - - #[wasm_bindgen(method)] - pub fn NewEventListener( - this: &Interpreter, - name: &str, - id: u32, - bubbles: bool, - handler: &Function, - ); - - #[wasm_bindgen(method)] - pub fn RemoveEventListener(this: &Interpreter, name: &str, id: u32); - - #[wasm_bindgen(method)] - pub fn RemoveAttribute(this: &Interpreter, id: u32, field: &str, ns: Option<&str>); - - #[wasm_bindgen(method)] - pub fn Remove(this: &Interpreter, id: u32); - - #[wasm_bindgen(method)] - pub fn PushRoot(this: &Interpreter, id: u32); - - #[wasm_bindgen(method)] - pub fn AppendChildren(this: &Interpreter, id: u32, m: u32); -} diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.js index 3745e8c6b..0c64085f9 100644 --- a/packages/interpreter/src/common.js +++ b/packages/interpreter/src/common.js @@ -1,33 +1,3 @@ -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, - webkitdirectory: true, -}; - export function setAttributeInner(node, field, value, ns) { const name = field; if (ns === "style") { @@ -36,7 +6,7 @@ export function setAttributeInner(node, field, value, ns) { node.style = {}; } node.style[name] = value; - } else if (ns != null && ns != undefined) { + } else if (!!ns) { node.setAttributeNS(ns, name, value); } else { switch (name) { @@ -74,6 +44,36 @@ export function setAttributeInner(node, field, value, ns) { } } +const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, + webkitdirectory: true, +}; + function truthy(val) { return val === "true" || val === true; } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index a03983ffa..54f1d517c 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -1,390 +1,9 @@ -import { setAttributeInner } from "./common.js"; - -class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = root; - } - - create(event_name, element, handler, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.global[event_name].callback = handler; - this.root.addEventListener(event_name, handler); - } else { - this.global[event_name].active++; - } - } else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - this.local[id][event_name] = handler; - element.addEventListener(event_name, handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener( - event_name, - this.global[event_name].callback - ); - delete this.global[event_name]; - } - } else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, handler); - } - } - - removeAllNonBubbling(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } -} - class InterpreterConfig { constructor(intercept_link_redirects) { this.intercept_link_redirects = intercept_link_redirects; } } -class Interpreter { - constructor(root, config) { - this.config = config; - this.root = root; - this.listeners = new ListenerMap(root); - this.nodes = [root]; - this.stack = [root]; - this.handlers = {}; - this.templates = {}; - this.lastNodeWasText = false; - } - top() { - return this.stack[this.stack.length - 1]; - } - pop() { - return this.stack.pop(); - } - MountToRoot() { - this.AppendChildren(this.stack.length - 1); - } - SetNode(id, node) { - this.nodes[id] = node; - } - PushRoot(root) { - const node = this.nodes[root]; - this.stack.push(node); - } - PopRoot() { - this.stack.pop(); - } - AppendChildren(many) { - // let root = this.nodes[id]; - let root = this.stack[this.stack.length - 1 - many]; - let to_add = this.stack.splice(this.stack.length - many); - for (let i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - } - ReplaceWith(root_id, m) { - let root = this.nodes[root_id]; - let els = this.stack.splice(this.stack.length - m); - if (is_element_node(root.nodeType)) { - this.listeners.removeAllNonBubbling(root); - } - root.replaceWith(...els); - } - InsertAfter(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.after(...new_nodes); - } - InsertBefore(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.before(...new_nodes); - } - Remove(root) { - let node = this.nodes[root]; - if (node !== undefined) { - if (is_element_node(node)) { - this.listeners.removeAllNonBubbling(node); - } - node.remove(); - } - } - CreateTextNode(text, root) { - const node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - } - CreatePlaceholder(root) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - NewEventListener(event_name, root, bubbles, handler) { - const element = this.nodes[root]; - element.setAttribute("data-dioxus-id", `${root}`); - this.listeners.create(event_name, element, handler, bubbles); - } - RemoveEventListener(root, event_name, bubbles) { - const element = this.nodes[root]; - element.removeAttribute(`data-dioxus-id`); - this.listeners.remove(element, event_name, bubbles); - } - SetText(root, text) { - this.nodes[root].textContent = text; - } - SetAttribute(id, field, value, ns) { - if (value === null) { - this.RemoveAttribute(id, field, ns); - } else { - const node = this.nodes[id]; - setAttributeInner(node, field, value, ns); - } - } - RemoveAttribute(root, field, ns) { - const node = this.nodes[root]; - if (!ns) { - switch (field) { - case "value": - node.value = ""; - break; - case "checked": - node.checked = false; - break; - case "selected": - node.selected = false; - break; - case "dangerous_inner_html": - node.innerHTML = ""; - break; - default: - node.removeAttribute(field); - break; - } - } else if (ns == "style") { - node.style.removeProperty(name); - } else { - node.removeAttributeNS(ns, field); - } - } - - GetClientRect(id) { - const node = this.nodes[id]; - if (!node) { - return; - } - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; - } - - ScrollTo(id, behavior) { - const node = this.nodes[id]; - if (!node) { - return false; - } - node.scrollIntoView({ - behavior: behavior, - }); - return true; - } - - /// Set the focus on the element - SetFocus(id, focus) { - const node = this.nodes[id]; - if (!node) { - return false; - } - if (focus) { - node.focus(); - } else { - node.blur(); - } - return true; - } - - handleEdits(edits) { - for (let template of edits.templates) { - this.SaveTemplate(template); - } - - for (let edit of edits.edits) { - this.handleEdit(edit); - } - - /*POST_HANDLE_EDITS*/ - } - - SaveTemplate(template) { - let roots = []; - for (let root of template.roots) { - roots.push(this.MakeTemplateNode(root)); - } - this.templates[template.name] = roots; - } - - MakeTemplateNode(node) { - switch (node.type) { - case "Text": - return document.createTextNode(node.text); - case "Dynamic": - let dyn = document.createElement("pre"); - dyn.hidden = true; - return dyn; - case "DynamicText": - return document.createTextNode("placeholder"); - case "Element": - let el; - - if (node.namespace != null) { - el = document.createElementNS(node.namespace, node.tag); - } else { - el = document.createElement(node.tag); - } - - for (let attr of node.attrs) { - if (attr.type == "Static") { - setAttributeInner(el, attr.name, attr.value, attr.namespace); - } - } - - for (let child of node.children) { - el.appendChild(this.MakeTemplateNode(child)); - } - - return el; - } - } - AssignId(path, id) { - this.nodes[id] = this.LoadChild(path); - } - LoadChild(path) { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1]; - - for (let i = 0; i < path.length; i++) { - node = node.childNodes[path[i]]; - } - - return node; - } - HydrateText(path, value, id) { - let node = this.LoadChild(path); - - if (node.nodeType == Node.TEXT_NODE) { - node.textContent = value; - } else { - // replace with a textnode - let text = document.createTextNode(value); - node.replaceWith(text); - node = text; - } - - this.nodes[id] = node; - } - ReplacePlaceholder(path, m) { - let els = this.stack.splice(this.stack.length - m); - let node = this.LoadChild(path); - node.replaceWith(...els); - } - LoadTemplate(name, index, id) { - let node = this.templates[name][index].cloneNode(true); - this.nodes[id] = node; - this.stack.push(node); - } - handleEdit(edit) { - switch (edit.type) { - case "AppendChildren": - this.AppendChildren(edit.m); - break; - case "AssignId": - this.AssignId(edit.path, edit.id); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.id); - break; - case "CreateTextNode": - this.CreateTextNode(edit.value, edit.id); - break; - case "HydrateText": - this.HydrateText(edit.path, edit.value, edit.id); - break; - case "LoadTemplate": - this.LoadTemplate(edit.name, edit.index, edit.id); - break; - case "PushRoot": - this.PushRoot(edit.id); - break; - case "ReplaceWith": - this.ReplaceWith(edit.id, edit.m); - break; - case "ReplacePlaceholder": - this.ReplacePlaceholder(edit.path, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.id, edit.m); - break; - case "InsertBefore": - this.InsertBefore(edit.id, edit.m); - break; - case "Remove": - this.Remove(edit.id); - break; - case "SetText": - this.SetText(edit.id, edit.value); - break; - case "SetAttribute": - this.SetAttribute(edit.id, edit.name, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.id, edit.name, edit.ns); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.id, edit.name); - break; - case "NewEventListener": - let bubbles = event_bubbles(edit.name); - - // if this is a mounted listener, we send the event immediately - if (edit.name === "mounted") { - window.ipc.postMessage( - serializeIpcMessage("user_event", { - name: edit.name, - element: edit.id, - data: null, - bubbles, - }) - ); - } else { - this.NewEventListener(edit.name, edit.id, bubbles, (event) => { - handler(event, edit.name, bubbles, this.config); - }); - } - break; - } - } -} - // this handler is only provided on the desktop and liveview implementations since this // method is not used by the web implementation function handler(event, name, bubbles, config) { @@ -416,7 +35,7 @@ function handler(event, name, bubbles, config) { const href = a_element.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { window.ipc.postMessage( - serializeIpcMessage("browser_open", { href }) + window.interpreter.serializeIpcMessage("browser_open", { href }) ); } } @@ -444,7 +63,44 @@ function handler(event, name, bubbles, config) { let contents = serialize_event(event); - /*POST_EVENT_SERIALIZATION*/ + // TODO: this should be liveview only + if ( + target.tagName === "INPUT" && + (event.type === "change" || event.type === "input") + ) { + const type = target.getAttribute("type"); + if (type === "file") { + async function read_files() { + const files = target.files; + const file_contents = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + let file_engine = { + files: file_contents, + }; + contents.files = file_engine; + + if (realId === null) { + return; + } + const message = window.interpreter.serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }); + window.ipc.postMessage(message); + } + read_files(); + return; + } + } if ( target.tagName === "FORM" && @@ -476,7 +132,7 @@ function handler(event, name, bubbles, config) { return; } window.ipc.postMessage( - serializeIpcMessage("user_event", { + window.interpreter.serializeIpcMessage("user_event", { name: name, element: parseInt(realId), data: contents, @@ -506,6 +162,130 @@ function find_real_id(target) { return realId; } +class ListenerMap { + constructor(root) { + // bubbling events can listen at the root element + this.global = {}; + // non bubbling events listen at the element the listener was created at + this.local = {}; + this.root = null; + } + + create(event_name, element, bubbles, handler) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = {}; + this.global[event_name].active = 1; + this.root.addEventListener(event_name, handler); + } else { + this.global[event_name].active++; + } + } + else { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, handler); + } + } + + remove(element, event_name, bubbles) { + if (bubbles) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + else { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (this.local[id].length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.global[event_name].callback); + } + } + + removeAllNonBubbling(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } +} +function LoadChild(array) { + // iterate through each number and get that child + node = stack[stack.length - 1]; + + for (let i = 0; i < array.length; i++) { + end = array[i]; + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + return node; +} +const listeners = new ListenerMap(); +let nodes = []; +let stack = []; +let root; +const templates = {}; +let node, els, end, k; + +function AppendChildren(id, many) { + root = nodes[id]; + els = stack.splice(stack.length - many); + for (k = 0; k < many; k++) { + root.appendChild(els[k]); + } +} + +window.interpreter = {} + +window.interpreter.initialize = function (root) { + nodes = [root]; + stack = [root]; + listeners.root = root; +} + +window.interpreter.getClientRect = function (id) { + const node = nodes[id]; + if (!node) { + return; + } + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [rect.x, rect.y], + size: [rect.width, rect.height], + }; +} + +window.interpreter.scrollTo = function (id, behavior) { + const node = nodes[id]; + if (!node) { + return false; + } + node.scrollIntoView({ + behavior: behavior, + }); + return true; +} + +/// Set the focus on the element +window.interpreter.setFocus = function (id, focus) { + const node = nodes[id]; + if (!node) { + return false; + } + if (focus) { + node.focus(); + } else { + node.blur(); + } + return true; +} + function get_mouse_data(event) { const { altKey, @@ -782,7 +562,7 @@ function serialize_event(event) { } } } -function serializeIpcMessage(method, params = {}) { +window.interpreter.serializeIpcMessage = function (method, params = {}) { return JSON.stringify({ method, params }); } diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index c540b0d7d..ccad95abd 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -10,14 +10,8 @@ mod sledgehammer_bindings; #[cfg(feature = "sledgehammer")] pub use sledgehammer_bindings::*; -#[cfg(feature = "web")] -mod bindings; - -#[cfg(feature = "web")] -pub use bindings::Interpreter; - // Common bindings for minimal usage. -#[cfg(feature = "minimal_bindings")] +#[cfg(all(feature = "minimal_bindings", feature = "web"))] pub mod minimal_bindings { use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; #[wasm_bindgen(module = "/src/common.js")] diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 8b44f4486..a42824e15 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -1,9 +1,17 @@ +#[cfg(feature = "web")] use js_sys::Function; +#[cfg(feature = "web")] use sledgehammer_bindgen::bindgen; +#[cfg(feature = "web")] use web_sys::Node; -#[bindgen] +#[cfg(feature = "web")] +pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; + +#[cfg(feature = "web")] +#[bindgen(module)] mod js { + const JS_FILE: &str = "./packages/interpreter/src/common.js"; const JS: &str = r#" class ListenerMap { constructor(root) { @@ -57,51 +65,6 @@ mod js { delete this.local[id]; } } - function SetAttributeInner(node, field, value, ns) { - const name = field; - if (ns === "style") { - // ????? why do we need to do this - if (node.style === undefined) { - node.style = {}; - } - node.style[name] = value; - } else if (ns !== null && ns !== undefined && ns !== "") { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value !== node.value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (!truthy(value) && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } function LoadChild(ptr, len) { // iterate through each number and get that child node = stack[stack.length - 1]; @@ -164,7 +127,7 @@ mod js { export function get_node(id) { return nodes[id]; } - export function initilize(root, handler) { + export function initialize(root, handler) { listeners.handler = handler; nodes = [root]; stack = [root]; @@ -177,43 +140,11 @@ mod js { root.appendChild(els[k]); } } - const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, - webkitdirectory: true, - }; - function truthy(val) { - return val === "true" || val === true; - } "#; extern "C" { #[wasm_bindgen] - pub fn save_template(nodes: Vec, tmpl_id: u32); + pub fn save_template(nodes: Vec, tmpl_id: u16); #[wasm_bindgen] pub fn hydrate(ids: Vec); @@ -222,7 +153,7 @@ mod js { pub fn get_node(id: u32) -> Node; #[wasm_bindgen] - pub fn initilize(root: Node, handler: &Function); + pub fn initialize(root: Node, handler: &Function); } fn mount_to_root() { @@ -231,19 +162,19 @@ mod js { fn push_root(root: u32) { "{stack.push(nodes[$root$]);}" } - fn append_children(id: u32, many: u32) { + fn append_children(id: u32, many: u16) { "{AppendChildren($id$, $many$);}" } fn pop_root() { "{stack.pop();}" } - fn replace_with(id: u32, n: u32) { + fn replace_with(id: u32, n: u16) { "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" } - fn insert_after(id: u32, n: u32) { + fn insert_after(id: u32, n: u16) { "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" } - fn insert_before(id: u32, n: u32) { + fn insert_before(id: u32, n: u16) { "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" } fn remove(id: u32) { @@ -268,7 +199,7 @@ mod js { "{nodes[$id$].textContent = $text$;}" } fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { - "{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}" + "{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}" } fn remove_attribute(id: u32, field: &str, ns: &str) { r#"{ @@ -314,10 +245,167 @@ mod js { nodes[$id$] = node; }"# } - fn replace_placeholder(ptr: u32, len: u8, n: u32) { + fn replace_placeholder(ptr: u32, len: u8, n: u16) { "{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}" } - fn load_template(tmpl_id: u32, index: u32, id: u32) { + fn load_template(tmpl_id: u16, index: u16, id: u32) { "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" } } + +#[cfg(feature = "binary-protocol")] +pub mod binary_protocol { + use sledgehammer_bindgen::bindgen; + pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; + + #[bindgen] + mod protocol_js { + const JS_FILE: &str = "./packages/interpreter/src/interpreter.js"; + const JS_FILE: &str = "./packages/interpreter/src/common.js"; + + fn mount_to_root() { + "{AppendChildren(root, stack.length-1);}" + } + fn push_root(root: u32) { + "{stack.push(nodes[$root$]);}" + } + fn append_children(id: u32, many: u16) { + "{AppendChildren($id$, $many$);}" + } + fn append_children_to_top(many: u16) { + "{ + root = stack[stack.length-many-1]; + els = stack.splice(stack.length-many); + for (k = 0; k < many; k++) { + root.appendChild(els[k]); + } + }" + } + fn pop_root() { + "{stack.pop();}" + } + fn replace_with(id: u32, n: u16) { + "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" + } + fn insert_after(id: u32, n: u16) { + "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" + } + fn insert_before(id: u32, n: u16) { + "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" + } + fn remove(id: u32) { + "{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}" + } + fn create_raw_text(text: &str) { + "{stack.push(document.createTextNode($text$));}" + } + fn create_text_node(text: &str, id: u32) { + "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}" + } + fn create_element(element: &'static str) { + "{stack.push(document.createElement($element$))}" + } + fn create_element_ns(element: &'static str, ns: &'static str) { + "{stack.push(document.createElementNS($ns$, $element$))}" + } + fn create_placeholder(id: u32) { + "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}" + } + fn add_placeholder() { + "{node = document.createElement('pre'); node.hidden = true; stack.push(node);}" + } + fn new_event_listener(event: &str, id: u32, bubbles: u8) { + r#" + bubbles = bubbles == 1; + node = nodes[id]; + if(node.listening){ + node.listening += 1; + } else { + node.listening = 1; + } + node.setAttribute('data-dioxus-id', `\${id}`); + const event_name = $event$; + + // if this is a mounted listener, we send the event immediately + if (event_name === "mounted") { + window.ipc.postMessage( + window.interpreter.serializeIpcMessage("user_event", { + name: event_name, + element: id, + data: null, + bubbles, + }) + ); + } else { + listeners.create(event_name, node, bubbles, (event) => { + handler(event, event_name, bubbles, config); + }); + }"# + } + fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { + "{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}" + } + fn set_text(id: u32, text: &str) { + "{nodes[$id$].textContent = $text$;}" + } + fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { + "{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}" + } + fn set_top_attribute(field: &str, value: &str, ns: &str) { + "{setAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}" + } + fn remove_attribute(id: u32, field: &str, ns: &str) { + r#"{ + node = nodes[$id$]; + if (!ns) { + switch (field) { + case "value": + node.value = ""; + break; + case "checked": + node.checked = false; + break; + case "selected": + node.selected = false; + break; + case "dangerous_inner_html": + node.innerHTML = ""; + break; + default: + node.removeAttribute(field); + break; + } + } else if (ns == "style") { + node.style.removeProperty(name); + } else { + node.removeAttributeNS(ns, field); + } + }"# + } + fn assign_id(array: &[u8], id: u32) { + "{nodes[$id$] = LoadChild($array$);}" + } + fn hydrate_text(array: &[u8], value: &str, id: u32) { + r#"{ + node = LoadChild($array$); + if (node.nodeType == Node.TEXT_NODE) { + node.textContent = value; + } else { + let text = document.createTextNode(value); + node.replaceWith(text); + node = text; + } + nodes[$id$] = node; + }"# + } + fn replace_placeholder(array: &[u8], n: u16) { + "{els = stack.splice(stack.length - $n$); node = LoadChild($array$); node.replaceWith(...els);}" + } + fn load_template(tmpl_id: u16, index: u16, id: u32) { + "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" + } + fn add_templates(tmpl_id: u16, len: u16) { + "{templates[$tmpl_id$] = stack.splice(stack.length-$len$);}" + } + } +} diff --git a/packages/liveview/.gitignore b/packages/liveview/.gitignore new file mode 100644 index 000000000..1aec3a9f9 --- /dev/null +++ b/packages/liveview/.gitignore @@ -0,0 +1 @@ +/src/minified.js \ No newline at end of file diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 0f35f4ae6..31e5a2bda 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -22,9 +22,10 @@ tokio-stream = { version = "0.1.11", features = ["net"] } tokio-util = { version = "0.7.4", features = ["rt"] } serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" +rustc-hash = { workspace = true } dioxus-html = { workspace = true, features = ["serialize"] } dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-interpreter-js = { workspace = true } +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } # warp @@ -34,7 +35,7 @@ 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" @@ -53,11 +54,15 @@ 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" +[build-dependencies] +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } +minify-js = "0.5.6" + [features] default = ["hot-reload"] # actix = ["actix-files", "actix-web", "actix-ws"] @@ -68,6 +73,10 @@ rocket = ["dep:rocket", "dep:rocket_ws"] name = "axum" required-features = ["axum"] +[[example]] +name = "axum_stress" +required-features = ["axum"] + [[example]] name = "salvo" required-features = ["salvo"] diff --git a/packages/liveview/build.rs b/packages/liveview/build.rs new file mode 100644 index 000000000..d4dbeb09a --- /dev/null +++ b/packages/liveview/build.rs @@ -0,0 +1,64 @@ +use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS; +use minify_js::*; +use std::io::Write; + +fn main() { + let serialize_file_uploads = r#"if ( + target.tagName === "INPUT" && + (event.type === "change" || event.type === "input") + ) { + const type = target.getAttribute("type"); + if (type === "file") { + async function read_files() { + const files = target.files; + const file_contents = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + let file_engine = { + files: file_contents, + }; + contents.files = file_engine; + + if (realId === null) { + return; + } + const message = window.interpreter.serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }); + window.ipc.postMessage(message); + } + read_files(); + return; + } + }"#; + let mut interpreter = SLEDGEHAMMER_JS + .replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads) + .replace("export", ""); + while let Some(import_start) = interpreter.find("import") { + let import_end = interpreter[import_start..] + .find(|c| c == ';' || c == '\n') + .map(|i| i + import_start) + .unwrap_or_else(|| interpreter.len()); + interpreter.replace_range(import_start..import_end, ""); + } + + let main_js = std::fs::read_to_string("src/main.js").unwrap(); + + let js = format!("{interpreter}\n{main_js}"); + + let session = Session::new(); + let mut out = Vec::new(); + minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap(); + let minified = String::from_utf8(out).unwrap(); + let mut file = std::fs::File::create("src/minified.js").unwrap(); + file.write_all(minified.as_bytes()).unwrap(); +} diff --git a/packages/liveview/examples/axum_stress.rs b/packages/liveview/examples/axum_stress.rs new file mode 100644 index 000000000..46a4ceb10 --- /dev/null +++ b/packages/liveview/examples/axum_stress.rs @@ -0,0 +1,65 @@ +use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; +use dioxus::prelude::*; + +fn app(cx: Scope) -> Element { + let state = use_state(cx, || 0); + use_future(cx, (), |_| { + to_owned![state]; + async move { + loop { + state += 1; + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + } + } + }); + + cx.render(rsx! { + for _ in 0..10000 { + div { + "hello axum! {state}" + } + } + }) +} + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); + + let view = dioxus_liveview::LiveViewPool::new(); + + let app = Router::new() + .route( + "/", + get(move || async move { + Html(format!( + r#" + + + Dioxus LiveView with axum +

+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws")) + )) + }), + ) + .route( + "/ws", + get(move |ws: WebSocketUpgrade| async move { + ws.on_upgrade(move |socket| async move { + _ = view.launch(dioxus_liveview::axum_socket(socket), app).await; + }) + }), + ); + + println!("Listening on http://{addr}"); + + axum::Server::bind(&addr.to_string().parse().unwrap()) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index dda37acc1..8c6f5ce31 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -20,5 +20,5 @@ fn transform_rx(message: Result) -> Result, LiveVi } async fn transform_tx(message: Vec) -> Result { - Ok(Message::Text(String::from_utf8_lossy(&message).to_string())) + Ok(Message::Binary(message)) } diff --git a/packages/liveview/src/element.rs b/packages/liveview/src/element.rs index ae0eb4efe..600f7f20d 100644 --- a/packages/liveview/src/element.rs +++ b/packages/liveview/src/element.rs @@ -29,7 +29,7 @@ impl RenderedElementBacking for LiveviewElement { >, >, > { - let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + let script = format!("return window.interpreter.getClientRect({});", self.id.0); let fut = self .query @@ -53,7 +53,7 @@ impl RenderedElementBacking for LiveviewElement { behavior: dioxus_html::ScrollBehavior, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.ScrollTo({}, {});", + "return window.interpreter.scrollTo({}, {});", self.id.0, serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") ); @@ -77,7 +77,7 @@ impl RenderedElementBacking for LiveviewElement { focus: bool, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.SetFocus({}, {});", + "return window.interpreter.setFocus({}, {});", self.id.0, focus ); diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 40d6186dc..c2ca679ce 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -46,58 +46,7 @@ pub enum LiveViewError { SendingFailed, } -use once_cell::sync::Lazy; - -static INTERPRETER_JS: Lazy = Lazy::new(|| { - let interpreter = dioxus_interpreter_js::INTERPRETER_JS; - let serialize_file_uploads = r#"if ( - target.tagName === "INPUT" && - (event.type === "change" || event.type === "input") - ) { - const type = target.getAttribute("type"); - if (type === "file") { - async function read_files() { - const files = target.files; - const file_contents = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - let file_engine = { - files: file_contents, - }; - contents.files = file_engine; - - if (realId === null) { - return; - } - const message = serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }); - window.ipc.postMessage(message); - } - read_files(); - return; - } - }"#; - - let interpreter = interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads); - interpreter.replace("import { setAttributeInner } from \"./common.js\";", "") -}); - -static COMMON_JS: Lazy = Lazy::new(|| { - let common = dioxus_interpreter_js::COMMON_JS; - common.replace("export", "") -}); - -static MAIN_JS: &str = include_str!("./main.js"); +static MINIFIED: &str = include_str!("./minified.js"); /// This script that gets injected into your app connects this page to the websocket endpoint /// @@ -135,8 +84,6 @@ pub fn interpreter_glue(url_or_path: &str) -> String { "return path;" }; - let js = &*INTERPRETER_JS; - let common = &*COMMON_JS; format!( r#" "# ) diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index e98d02a9c..22267dbab 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -1,3 +1,5 @@ +const config = new InterpreterConfig(false); + function main() { let root = window.document.getElementById("main"); if (root != null) { @@ -7,10 +9,9 @@ function main() { class IPC { constructor(root) { - // connect to the websocket - window.interpreter = new Interpreter(root, new InterpreterConfig(false)); - - let ws = new WebSocket(WS_ADDR); + window.interpreter.initialize(root); + const ws = new WebSocket(WS_ADDR); + ws.binaryType = "arraybuffer"; function ping() { ws.send("__ping__"); @@ -19,7 +20,7 @@ class IPC { ws.onopen = () => { // we ping every 30 seconds to keep the websocket alive setInterval(ping, 30000); - ws.send(serializeIpcMessage("initialize")); + ws.send(window.interpreter.serializeIpcMessage("initialize")); }; ws.onerror = (err) => { @@ -27,17 +28,29 @@ class IPC { }; ws.onmessage = (message) => { - // Ignore pongs - if (message.data != "__pong__") { - const event = JSON.parse(message.data); - switch (event.type) { - case "edits": - let edits = event.data; - window.interpreter.handleEdits(edits); - break; - case "query": - Function("Eval", `"use strict";${event.data};`)(); - break; + const u8view = new Uint8Array(message.data); + const binaryFrame = u8view[0] == 1; + const messageData = message.data.slice(1) + // The first byte tells the shim if this is a binary of text frame + if (binaryFrame) { + // binary frame + run_from_bytes(messageData); + } + else { + // text frame + + let decoder = new TextDecoder("utf-8"); + + // Using decode method to get string output + let str = decoder.decode(messageData); + // Ignore pongs + if (str != "__pong__") { + const event = JSON.parse(str); + switch (event.type) { + case "query": + Function("Eval", `"use strict";${event.data};`)(); + break; + } } } }; @@ -49,3 +62,5 @@ class IPC { this.ws.send(msg); } } + +main(); \ No newline at end of file diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index 2fd06cec6..eaf699248 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -4,9 +4,11 @@ use crate::{ query::{QueryEngine, QueryResult}, LiveViewError, }; -use dioxus_core::{prelude::*, Mutations}; -use dioxus_html::{EventData, HtmlEvent, MountedData}; +use dioxus_core::{prelude::*, BorrowedAttributeValue, Mutations}; +use dioxus_html::{event_bubbles, EventData, HtmlEvent, MountedData}; +use dioxus_interpreter_js::binary_protocol::Channel; use futures_util::{pin_mut, SinkExt, StreamExt}; +use rustc_hash::FxHashMap; use serde::Serialize; use std::{rc::Rc, time::Duration}; use tokio_util::task::LocalPoolHandle; @@ -107,7 +109,7 @@ impl LiveViewSocket for S where /// /// This function makes it easy to integrate Dioxus LiveView with any socket-based framework. /// -/// As long as your framework can provide a Sink and Stream of Strings, you can use this function. +/// As long as your framework can provide a Sink and Stream of Bytes, you can use this function. /// /// You might need to transform the error types of the web backend into the LiveView error type. pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), LiveViewError> { @@ -120,20 +122,31 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li rx }; + let mut templates: FxHashMap = Default::default(); + let mut max_template_count = 0; + // Create the a proxy for query engine let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel(); let query_engine = QueryEngine::new(query_tx); vdom.base_scope().provide_context(query_engine.clone()); init_eval(vdom.base_scope()); - // todo: use an efficient binary packed format for this - let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap(); - // pin the futures so we can use select! pin_mut!(ws); - // send the initial render to the client - ws.send(edits.into_bytes()).await?; + let mut edit_channel = Channel::default(); + if let Some(edits) = { + let mutations = vdom.rebuild(); + apply_edits( + mutations, + &mut edit_channel, + &mut templates, + &mut max_template_count, + ) + } { + // send the initial render to the client + ws.send(edits).await?; + } // desktop uses this wrapper struct thing around the actual event itself // this is sorta driven by tao/wry @@ -160,7 +173,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li match evt.as_ref().map(|o| o.as_deref()) { // respond with a pong every ping to keep the websocket alive Some(Ok(b"__ping__")) => { - ws.send(b"__pong__".to_vec()).await?; + ws.send(text_frame("__pong__")).await?; } Some(Ok(evt)) => { if let Ok(message) = serde_json::from_str::(&String::from_utf8_lossy(evt)) { @@ -199,7 +212,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li // handle any new queries Some(query) = query_rx.recv() => { - ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap().into_bytes()).await?; + ws.send(text_frame(&serde_json::to_string(&ClientUpdate::Query(query)).unwrap())).await?; } Some(msg) = hot_reload_wait => { @@ -221,20 +234,156 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) .await; - ws.send( - serde_json::to_string(&ClientUpdate::Edits(edits)) - .unwrap() - .into_bytes(), - ) - .await?; + if let Some(edits) = { + apply_edits( + edits, + &mut edit_channel, + &mut templates, + &mut max_template_count, + ) + } { + ws.send(edits).await?; + } } } +fn text_frame(text: &str) -> Vec { + let mut bytes = vec![0]; + bytes.extend(text.as_bytes()); + bytes +} + +fn add_template( + template: &Template<'static>, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &mut u16, +) { + for root in template.roots.iter() { + create_template_node(channel, root); + templates.insert(template.name.to_owned(), *max_template_count); + } + channel.add_templates(*max_template_count, template.roots.len() as u16); + + *max_template_count += 1 +} + +fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { + use TemplateNode::*; + match v { + Element { + tag, + namespace, + attrs, + children, + .. + } => { + // Push the current node onto the stack + match namespace { + Some(ns) => channel.create_element_ns(tag, ns), + None => channel.create_element(tag), + } + // Set attributes on the current node + for attr in *attrs { + if let TemplateAttribute::Static { + name, + value, + namespace, + } = attr + { + channel.set_top_attribute(name, value, namespace.unwrap_or_default()) + } + } + // Add each child to the stack + for child in *children { + create_template_node(channel, child); + } + // Add all children to the parent + channel.append_children_to_top(children.len() as u16); + } + Text { text } => channel.create_raw_text(text), + DynamicText { .. } => channel.create_raw_text("p"), + Dynamic { .. } => channel.add_placeholder(), + } +} + +fn apply_edits( + mutations: Mutations, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &mut u16, +) -> Option> { + use dioxus_core::Mutation::*; + if mutations.templates.is_empty() && mutations.edits.is_empty() { + return None; + } + for template in mutations.templates { + add_template(&template, channel, templates, max_template_count); + } + for edit in mutations.edits { + match edit { + AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16), + AssignId { path, id } => channel.assign_id(path, id.0 as u32), + CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32), + CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32), + HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32), + LoadTemplate { name, index, id } => { + if let Some(tmpl_id) = templates.get(name) { + channel.load_template(*tmpl_id, index as u16, id.0 as u32) + } + } + ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16), + ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16), + InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16), + InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16), + SetAttribute { + name, + value, + id, + ns, + } => match value { + BorrowedAttributeValue::Text(txt) => { + channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default()) + } + BorrowedAttributeValue::Float(f) => { + channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Int(n) => { + channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Bool(b) => channel.set_attribute( + id.0 as u32, + name, + if b { "true" } else { "false" }, + ns.unwrap_or_default(), + ), + BorrowedAttributeValue::None => { + channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default()) + } + _ => unreachable!(), + }, + SetText { value, id } => channel.set_text(id.0 as u32, value), + NewEventListener { name, id, .. } => { + channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + RemoveEventListener { name, id } => { + channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + Remove { id } => channel.remove(id.0 as u32), + PushRoot { id } => channel.push_root(id.0 as u32), + } + } + + // Add an extra one at the beginning to tell the shim this is a binary frame + let mut bytes = vec![1]; + bytes.extend(channel.export_memory()); + channel.reset(); + Some(bytes) +} + #[derive(Serialize)] #[serde(tag = "type", content = "data")] -enum ClientUpdate<'a> { - #[serde(rename = "edits")] - Edits(Mutations<'a>), +enum ClientUpdate { #[serde(rename = "query")] Query(String), } diff --git a/packages/liveview/src/query.rs b/packages/liveview/src/query.rs index a638625c1..c99dd5151 100644 --- a/packages/liveview/src/query.rs +++ b/packages/liveview/src/query.rs @@ -169,10 +169,7 @@ pub(crate) struct Query { impl Query { /// Resolve the query pub async fn resolve(mut self) -> Result { - match self.receiver.recv().await { - Some(result) => V::deserialize(result).map_err(QueryError::Deserialize), - None => Err(QueryError::Recv(RecvError::Closed)), - } + V::deserialize(self.result().await?).map_err(QueryError::Deserialize) } /// Send a message to the query diff --git a/packages/native-core/tests/fuzzing.rs b/packages/native-core/tests/fuzzing.rs index 4f0a34b70..29c4fef9e 100644 --- a/packages/native-core/tests/fuzzing.rs +++ b/packages/native-core/tests/fuzzing.rs @@ -3,7 +3,6 @@ use dioxus_core::*; use dioxus_native_core::prelude::*; use dioxus_native_core_macro::partial_derive_state; use shipyard::Component; -use std::cell::Cell; fn random_ns() -> Option<&'static str> { let namespace = rand::random::() % 2; @@ -178,22 +177,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { let range = if depth > 3 { 1 } else { 3 }; match rand::random::() % range { 0 => DynamicNode::Placeholder(Default::default()), - 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| VNode { - key: None, - parent: Default::default(), - template: Cell::new(Template { - name: concat!(file!(), ":", line!(), ":", column!(), ":0"), - roots: &[TemplateNode::Dynamic { id: 0 }], - node_paths: &[&[0]], - attr_paths: &[], - }), - root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()).into(), - dynamic_nodes: cx.bump().alloc([cx.component( - create_random_element, - DepthProps { depth, root: false }, - "create_random_element", - )]), - dynamic_attrs: &[], + 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| { + VNode::new( + None, + Template { + name: concat!(file!(), ":", line!(), ":", column!(), ":0"), + roots: &[TemplateNode::Dynamic { id: 0 }], + node_paths: &[&[0]], + attr_paths: &[], + }, + dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()), + cx.bump().alloc([cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + )]), + &[], + ) })), 2 => cx.component( create_random_element, @@ -253,13 +253,11 @@ fn create_random_element(cx: Scope) -> Element { .into_boxed_str(), )); println!("{template:#?}"); - let node = VNode { - key: None, - parent: None, - template: Cell::new(template), - root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()) - .into(), - dynamic_nodes: { + let node = VNode::new( + None, + template, + dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()), + { let dynamic_nodes: Vec<_> = dynamic_node_types .iter() .map(|ty| match ty { @@ -273,12 +271,12 @@ fn create_random_element(cx: Scope) -> Element { .collect(); cx.bump().alloc(dynamic_nodes) }, - dynamic_attrs: cx.bump().alloc( + cx.bump().alloc( (0..template.attr_paths.len()) .map(|_| create_random_dynamic_attr(cx)) .collect::>(), ), - }; + ); Some(node) } _ => None, 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/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs index aac1e8658..a8fb367d0 100644 --- a/packages/router/examples/simple_routes.rs +++ b/packages/router/examples/simple_routes.rs @@ -112,7 +112,6 @@ fn Route2(cx: Scope, user_id: usize) -> Element { #[component] fn Route3(cx: Scope, dynamic: String) -> Element { - let navigator = use_navigator(cx); let current_route = use_route(cx)?; let current_route_str = use_ref(cx, String::new); let parsed = Route::from_str(¤t_route_str.read()); @@ -122,11 +121,11 @@ fn Route3(cx: Scope, dynamic: String) -> Element { .flat_map(|seg| seg.flatten().into_iter()) .collect::>(); + let navigator = use_navigator(cx); + render! { input { - oninput: move |evt| { - *current_route_str.write() = evt.value.clone(); - }, + oninput: move |evt| *current_route_str.write() = evt.value.clone(), value: "{current_route_str.read()}" } "dynamic: {dynamic}" diff --git a/packages/router/src/contexts/navigator.rs b/packages/router/src/contexts/navigator.rs index 961122c05..8c8bdd109 100644 --- a/packages/router/src/contexts/navigator.rs +++ b/packages/router/src/contexts/navigator.rs @@ -1,5 +1,19 @@ use crate::prelude::{ExternalNavigationFailure, IntoRoutable, RouterContext}; +/// Acquire the navigator without subscribing to updates. +/// +/// Can be called anywhere in the application provided a Router has been initialized. +/// +/// ## Panics +/// +/// Panics if there is no router present. +pub fn navigator() -> Navigator { + Navigator( + dioxus::core::prelude::consume_context::() + .expect("A router must be present to use navigator"), + ) +} + /// A view into the navigation state of a router. #[derive(Clone)] pub struct Navigator(pub(crate) RouterContext); diff --git a/packages/router/src/hooks/use_navigator.rs b/packages/router/src/hooks/use_navigator.rs index a05cd036f..9d367d83d 100644 --- a/packages/router/src/hooks/use_navigator.rs +++ b/packages/router/src/hooks/use_navigator.rs @@ -49,6 +49,7 @@ use crate::prelude::{Navigator, RouterContext}; /// # let _ = vdom.rebuild(); /// ``` #[must_use] +#[deprecated = "Prefer acquiring the router directly with `dioxus_router::router()`"] pub fn use_navigator(cx: &ScopeState) -> &Navigator { &*cx.use_hook(|| { let router = cx diff --git a/packages/router/src/hooks/use_router.rs b/packages/router/src/hooks/use_router.rs index 5f1f13e45..2cb628160 100644 --- a/packages/router/src/hooks/use_router.rs +++ b/packages/router/src/hooks/use_router.rs @@ -2,7 +2,7 @@ use dioxus::prelude::ScopeState; use crate::{prelude::RouterContext, utils::use_router_internal::use_router_internal}; -#[deprecated = "prefer the use_navigator or use_route functions"] +#[deprecated = "prefer the `router()` function or `use_route` functions"] #[must_use] /// A hook that provides access to information about the router. pub fn use_router(cx: &ScopeState) -> &RouterContext { @@ -10,3 +10,8 @@ pub fn use_router(cx: &ScopeState) -> &RouterContext { .as_ref() .expect("use_route must have access to a router") } + +/// Aquire the router without subscribing to updates. +pub fn router() -> RouterContext { + dioxus::core::prelude::consume_context().unwrap() +} diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index e871dc6c9..a1432d5b9 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -53,6 +53,8 @@ pub mod hooks { pub use use_navigator::*; } +pub use hooks::router; + /// A collection of useful items most applications might need. pub mod prelude { pub use crate::components::*; 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..09bac974c 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,47 @@ 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: ElementAttr { +// value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text( +// value.as_deref().unwrap_or("false"), +// )), +// name: dioxus_rsx::ElementAttrName::BuiltIn(ident), + // }, + attr, } }) .collect(); @@ -52,9 +74,12 @@ pub fn rsx_node_from_html(node: &Node) -> Option { if !class.is_empty() { attributes.push(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name: Ident::new("class", Span::call_site()), - value: ifmt_from_text(&class), + attr: ElementAttr { + name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new( + "class", + Span::call_site(), + )), + value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)), }, }); } @@ -62,9 +87,12 @@ pub fn rsx_node_from_html(node: &Node) -> Option { if let Some(id) = &el.id { attributes.push(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name: Ident::new("id", Span::call_site()), - value: ifmt_from_text(id), + attr: ElementAttr { + name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new( + "id", + Span::call_site(), + )), + value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)), }, }); } @@ -75,6 +103,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { name: el_name, children, attributes, + merged_attributes: Default::default(), key: None, brace: Default::default(), })) 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/Cargo.toml b/packages/rsx/Cargo.toml index 72b51a051..20893e7f4 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -23,5 +23,7 @@ krates = { version = "0.12.6", optional = true } tracing.workspace = true [features] +default = ["html"] hot_reload = ["krates", "internment"] serde = ["dep:serde"] +html = [] diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs new file mode 100644 index 000000000..38cb7c853 --- /dev/null +++ b/packages/rsx/src/attribute.rs @@ -0,0 +1,309 @@ +use std::fmt::{Display, Formatter}; + +use super::*; + +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{parse_quote, Expr, ExprIf, Ident, LitStr}; + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub struct ElementAttrNamed { + pub el_name: ElementName, + pub attr: ElementAttr, +} + +impl ElementAttrNamed { + pub(crate) fn try_combine(&self, other: &Self) -> Option { + if self.el_name == other.el_name && self.attr.name == other.attr.name { + if let Some(separator) = self.attr.name.multi_attribute_separator() { + return Some(ElementAttrNamed { + el_name: self.el_name.clone(), + attr: ElementAttr { + name: self.attr.name.clone(), + value: self.attr.value.combine(separator, &other.attr.value), + }, + }); + } + } + None + } +} + +impl ToTokens for ElementAttrNamed { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ElementAttrNamed { el_name, attr } = self; + + let ns = |name: &ElementAttrName| match (el_name, name) { + (ElementName::Ident(i), ElementAttrName::BuiltIn(_)) => { + quote! { dioxus_elements::#i::#name.1 } + } + _ => quote! { None }, + }; + let volitile = |name: &ElementAttrName| match (el_name, name) { + (ElementName::Ident(i), ElementAttrName::BuiltIn(_)) => { + quote! { dioxus_elements::#i::#name.2 } + } + _ => quote! { false }, + }; + let attribute = |name: &ElementAttrName| match name { + ElementAttrName::BuiltIn(name) => match el_name { + ElementName::Ident(_) => quote! { #el_name::#name.0 }, + ElementName::Custom(_) => { + let as_string = name.to_string(); + quote!(#as_string) + } + }, + ElementAttrName::Custom(s) => quote! { #s }, + }; + + let attribute = { + match &attr.value { + ElementAttrValue::AttrLiteral(_) + | ElementAttrValue::AttrExpr(_) + | ElementAttrValue::AttrOptionalExpr { .. } => { + let name = &self.attr.name; + let ns = ns(name); + let volitile = volitile(name); + let attribute = attribute(name); + let value = &self.attr.value; + let value = quote! { #value }; + quote! { + __cx.attr( + #attribute, + #value, + #ns, + #volitile + ) + } + } + ElementAttrValue::EventTokens(tokens) => match &self.attr.name { + ElementAttrName::BuiltIn(name) => { + quote! { + dioxus_elements::events::#name(__cx, #tokens) + } + } + ElementAttrName::Custom(_) => todo!(), + }, + } + }; + + tokens.append_all(attribute); + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub struct ElementAttr { + pub name: ElementAttrName, + pub value: ElementAttrValue, +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum ElementAttrValue { + /// attribute: "value" + AttrLiteral(IfmtInput), + /// attribute: if bool { "value" } + AttrOptionalExpr { + condition: Expr, + value: Box, + }, + /// attribute: true + AttrExpr(Expr), + /// onclick: move |_| {} + EventTokens(Expr), +} + +impl Parse for ElementAttrValue { + fn parse(input: ParseStream) -> syn::Result { + Ok(if input.peek(Token![if]) { + let if_expr = input.parse::()?; + if is_if_chain_terminated(&if_expr) { + ElementAttrValue::AttrExpr(Expr::If(if_expr)) + } else { + ElementAttrValue::AttrOptionalExpr { + condition: *if_expr.cond, + value: { + let stmts = if_expr.then_branch.stmts; + Box::new(syn::parse2(quote! { + #(#stmts)* + })?) + }, + } + } + } else if input.peek(LitStr) { + let value = input.parse()?; + ElementAttrValue::AttrLiteral(value) + } else { + let value = input.parse::()?; + ElementAttrValue::AttrExpr(value) + }) + } +} + +impl ToTokens for ElementAttrValue { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + ElementAttrValue::AttrLiteral(lit) => tokens.append_all(quote! { #lit }), + ElementAttrValue::AttrOptionalExpr { condition, value } => { + tokens.append_all(quote! { if #condition { Some(#value) } else { None } }) + } + ElementAttrValue::AttrExpr(expr) => tokens.append_all(quote! { #expr }), + ElementAttrValue::EventTokens(expr) => tokens.append_all(quote! { #expr }), + } + } +} + +impl ElementAttrValue { + fn to_str_expr(&self) -> Option { + match self { + ElementAttrValue::AttrLiteral(lit) => Some(quote!(#lit.to_string())), + ElementAttrValue::AttrOptionalExpr { value, .. } => value.to_str_expr(), + ElementAttrValue::AttrExpr(expr) => Some(quote!(#expr.to_string())), + _ => None, + } + } + + fn combine(&self, separator: &str, other: &Self) -> Self { + match (self, other) { + (Self::AttrLiteral(lit1), Self::AttrLiteral(lit2)) => { + let fmt = lit1.clone().join(lit2.clone(), separator); + Self::AttrLiteral(fmt) + } + (Self::AttrLiteral(expr1), Self::AttrExpr(expr2)) => { + let mut ifmt = expr1.clone(); + ifmt.push_str(separator); + ifmt.push_expr(expr2.clone()); + Self::AttrLiteral(ifmt) + } + (Self::AttrExpr(expr1), Self::AttrLiteral(expr2)) => { + let mut ifmt = expr2.clone(); + ifmt.push_str(separator); + ifmt.push_expr(expr1.clone()); + Self::AttrLiteral(ifmt) + } + (Self::AttrExpr(expr1), Self::AttrExpr(expr2)) => { + let mut ifmt = IfmtInput::default(); + ifmt.push_expr(expr1.clone()); + ifmt.push_str(separator); + ifmt.push_expr(expr2.clone()); + Self::AttrLiteral(ifmt) + } + ( + Self::AttrOptionalExpr { + condition: condition1, + value: value1, + }, + Self::AttrOptionalExpr { + condition: condition2, + value: value2, + }, + ) => { + let first_as_string = value1.to_str_expr(); + let second_as_string = value2.to_str_expr(); + Self::AttrExpr(parse_quote! { + { + let mut __combined = String::new(); + if #condition1 { + __combined.push_str(&#first_as_string); + } + if #condition2 { + if __combined.len() > 0 { + __combined.push_str(&#separator); + } + __combined.push_str(&#second_as_string); + } + __combined + } + }) + } + (Self::AttrOptionalExpr { condition, value }, other) => { + let first_as_string = value.to_str_expr(); + let second_as_string = other.to_str_expr(); + Self::AttrExpr(parse_quote! { + { + let mut __combined = #second_as_string; + if #condition { + __combined.push_str(&#separator); + __combined.push_str(&#first_as_string); + } + __combined + } + }) + } + (other, Self::AttrOptionalExpr { condition, value }) => { + let first_as_string = other.to_str_expr(); + let second_as_string = value.to_str_expr(); + Self::AttrExpr(parse_quote! { + { + let mut __combined = #first_as_string; + if #condition { + __combined.push_str(&#separator); + __combined.push_str(&#second_as_string); + } + __combined + } + }) + } + _ => todo!(), + } + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum ElementAttrName { + BuiltIn(Ident), + Custom(LitStr), +} + +impl ElementAttrName { + fn multi_attribute_separator(&self) -> Option<&'static str> { + match self { + ElementAttrName::BuiltIn(i) => match i.to_string().as_str() { + "class" => Some(" "), + "style" => Some(";"), + _ => None, + }, + ElementAttrName::Custom(_) => None, + } + } + + pub fn start(&self) -> Span { + match self { + ElementAttrName::BuiltIn(i) => i.span(), + ElementAttrName::Custom(s) => s.span(), + } + } +} + +impl ToTokens for ElementAttrName { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + ElementAttrName::BuiltIn(i) => tokens.append_all(quote! { #i }), + ElementAttrName::Custom(s) => tokens.append_all(quote! { #s }), + } + } +} + +impl Display for ElementAttrName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ElementAttrName::BuiltIn(i) => write!(f, "{}", i), + ElementAttrName::Custom(s) => write!(f, "{}", s.value()), + } + } +} + +impl ElementAttr { + pub fn start(&self) -> Span { + self.name.start() + } + + pub fn is_expr(&self) -> bool { + matches!( + self, + ElementAttr { + value: ElementAttrValue::AttrExpr(_) | ElementAttrValue::EventTokens(_), + .. + } + ) + } +} diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 8f45d91ff..86d530d33 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -8,7 +8,7 @@ use syn::{ parse::{Parse, ParseBuffer, ParseStream}, punctuated::Punctuated, spanned::Spanned, - Error, Expr, Ident, LitStr, Result, Token, + Ident, LitStr, Result, Token, }; // ======================================= @@ -19,6 +19,7 @@ pub struct Element { pub name: ElementName, pub key: Option, pub attributes: Vec, + pub merged_attributes: Vec, pub children: Vec, pub brace: syn::token::Brace, } @@ -34,7 +35,6 @@ impl Parse for Element { let mut attributes: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; - let mut _el_ref = None; // parse fields with commas // break when we don't get this pattern anymore @@ -49,19 +49,14 @@ impl Parse for Element { content.parse::()?; - if content.peek(LitStr) { - let value = content.parse()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrText { name, value }, - }); - } else { - let value = content.parse::()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrExpression { name, value }, - }); - } + let value = content.parse::()?; + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr { + name: ElementAttrName::Custom(name), + value, + }, + }); if content.is_empty() { break; @@ -86,9 +81,9 @@ impl Parse for Element { if name_str.starts_with("on") { attributes.push(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: content.parse()?, + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value: ElementAttrValue::EventTokens(content.parse()?), }, }); } else { @@ -96,29 +91,15 @@ impl Parse for Element { "key" => { key = Some(content.parse()?); } - "classes" => todo!("custom class list not supported yet"), - // "namespace" => todo!("custom namespace not supported yet"), - "node_ref" => { - _el_ref = Some(content.parse::()?); - } _ => { - if content.peek(LitStr) { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name, - value: content.parse()?, - }, - }); - } else { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::AttrExpression { - name, - value: content.parse()?, - }, - }); - } + let value = content.parse::()?; + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value, + }, + }); } } } @@ -137,6 +118,23 @@ impl Parse for Element { break; } + // Deduplicate any attributes that can be combined + // For example, if there are two `class` attributes, combine them into one + let mut merged_attributes: Vec = Vec::new(); + for attr in &attributes { + if let Some(old_attr_index) = merged_attributes + .iter() + .position(|a| a.attr.name == attr.attr.name) + { + let old_attr = &mut merged_attributes[old_attr_index]; + if let Some(combined) = old_attr.try_combine(attr) { + *old_attr = combined; + } + } else { + merged_attributes.push(attr.clone()); + } + } + while !content.is_empty() { if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { attr_after_element!(content.span()); @@ -158,6 +156,7 @@ impl Parse for Element { key, name: el_name, attributes, + merged_attributes, children, brace, }) @@ -175,14 +174,14 @@ impl ToTokens for Element { }; let listeners = self - .attributes + .merged_attributes .iter() - .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); + .filter(|f| matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); let attr = self - .attributes + .merged_attributes .iter() - .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); + .filter(|f| !matches!(f.attr.value, ElementAttrValue::EventTokens { .. })); tokens.append_all(quote! { __cx.element( @@ -264,136 +263,3 @@ impl ToTokens for ElementName { } } } - -#[derive(PartialEq, Eq, Clone, Debug, Hash)] -pub enum ElementAttr { - /// `attribute: "value"` - AttrText { name: Ident, value: IfmtInput }, - - /// `attribute: true` - AttrExpression { name: Ident, value: Expr }, - - /// `"attribute": "value"` - CustomAttrText { name: LitStr, value: IfmtInput }, - - /// `"attribute": true` - CustomAttrExpression { name: LitStr, value: Expr }, - - // /// onclick: move |_| {} - // EventClosure { name: Ident, closure: ExprClosure }, - /// onclick: {} - EventTokens { name: Ident, tokens: Expr }, -} - -impl ElementAttr { - pub fn start(&self) -> Span { - match self { - ElementAttr::AttrText { name, .. } => name.span(), - ElementAttr::AttrExpression { name, .. } => name.span(), - ElementAttr::CustomAttrText { name, .. } => name.span(), - ElementAttr::CustomAttrExpression { name, .. } => name.span(), - ElementAttr::EventTokens { name, .. } => name.span(), - } - } - - pub fn is_expr(&self) -> bool { - matches!( - self, - ElementAttr::AttrExpression { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } - ) - } -} - -#[derive(PartialEq, Eq, Clone, Debug, Hash)] -pub struct ElementAttrNamed { - pub el_name: ElementName, - pub attr: ElementAttr, -} - -impl ToTokens for ElementAttrNamed { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let ElementAttrNamed { el_name, attr } = self; - - let ns = |name| match el_name { - ElementName::Ident(i) => quote! { dioxus_elements::#i::#name.1 }, - ElementName::Custom(_) => quote! { None }, - }; - let volitile = |name| match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.2 }, - ElementName::Custom(_) => quote! { false }, - }; - let attribute = |name: &Ident| match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.0 }, - ElementName::Custom(_) => { - let as_string = name.to_string(); - quote!(#as_string) - } - }; - - let attribute = match attr { - ElementAttr::AttrText { name, value } => { - let ns = ns(name); - let volitile = volitile(name); - let attribute = attribute(name); - quote! { - __cx.attr( - #attribute, - #value, - #ns, - #volitile - ) - } - } - ElementAttr::AttrExpression { name, value } => { - let ns = ns(name); - let volitile = volitile(name); - let attribute = attribute(name); - quote! { - __cx.attr( - #attribute, - #value, - #ns, - #volitile - ) - } - } - ElementAttr::CustomAttrText { name, value } => { - quote! { - __cx.attr( - #name, - #value, - None, - false - ) - } - } - ElementAttr::CustomAttrExpression { name, value } => { - quote! { - __cx.attr( - #name, - #value, - None, - false - ) - } - } - ElementAttr::EventTokens { name, tokens } => { - quote! { - dioxus_elements::events::#name(__cx, #tokens) - } - } - }; - - tokens.append_all(attribute); - } -} - -// ::dioxus::core::Attribute { -// name: stringify!(#name), -// namespace: None, -// volatile: false, -// mounted_node: Default::default(), -// value: ::dioxus::core::AttributeValue::Text(#value), -// } diff --git a/packages/rsx/src/errors.rs b/packages/rsx/src/errors.rs index f89b28500..6cc34e1fb 100644 --- a/packages/rsx/src/errors.rs +++ b/packages/rsx/src/errors.rs @@ -1,12 +1,12 @@ macro_rules! missing_trailing_comma { ($span:expr) => { - return Err(Error::new($span, "missing trailing comma")); + return Err(syn::Error::new($span, "missing trailing comma")); }; } macro_rules! attr_after_element { ($span:expr) => { - return Err(Error::new($span, "expected element\n = help move the attribute above all the children and text elements")); + return Err(syn::Error::new($span, "expected element\n = help move the attribute above all the children and text elements")); }; } diff --git a/packages/rsx/src/ifmt.rs b/packages/rsx/src/ifmt.rs index 6c72f5025..fa764bf2e 100644 --- a/packages/rsx/src/ifmt.rs +++ b/packages/rsx/src/ifmt.rs @@ -13,7 +13,7 @@ pub fn format_args_f_impl(input: IfmtInput) -> Result { } #[allow(dead_code)] // dumb compiler does not see the struct being used... -#[derive(Debug, PartialEq, Eq, Clone, Hash)] +#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] pub struct IfmtInput { pub source: Option, pub segments: Vec, @@ -27,8 +27,29 @@ impl IfmtInput { } } + pub fn join(mut self, other: Self, separator: &str) -> Self { + if !self.segments.is_empty() { + self.segments.push(Segment::Literal(separator.to_string())); + } + self.segments.extend(other.segments); + self + } + + pub fn push_expr(&mut self, expr: Expr) { + self.segments.push(Segment::Formatted(FormattedSegment { + format_args: String::new(), + segment: FormattedSegmentType::Expr(Box::new(expr)), + })); + } + + pub fn push_str(&mut self, s: &str) { + self.segments.push(Segment::Literal(s.to_string())); + } + pub fn is_static(&self) -> bool { - matches!(self.segments.as_slice(), &[Segment::Literal(_)] | &[]) + self.segments + .iter() + .all(|seg| matches!(seg, Segment::Literal(_))) } } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 5db6baee9..f1464f0f2 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -16,6 +16,7 @@ #[macro_use] mod errors; +mod attribute; mod component; mod element; #[cfg(feature = "hot_reload")] @@ -26,6 +27,7 @@ mod node; use std::{fmt::Debug, hash::Hash}; // Re-export the namespaces into each other +pub use attribute::*; pub use component::*; #[cfg(feature = "hot_reload")] use dioxus_core::{Template, TemplateAttribute, TemplateNode}; @@ -204,12 +206,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); @@ -247,14 +254,13 @@ impl<'a> ToTokens for TemplateRenderer<'a> { node_paths: &[ #(#node_paths),* ], attr_paths: &[ #(#attr_paths),* ], }; - ::dioxus::core::VNode { - parent: None, - key: #key_tokens, - template: std::cell::Cell::new(TEMPLATE), - root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()).into(), - dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]), - dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]), - } + ::dioxus::core::VNode::new( + #key_tokens, + TEMPLATE, + dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()), + __cx.bump().alloc([ #( #node_printer ),* ]), + __cx.bump().alloc([ #( #dyn_attr_printer ),* ]), + ) }); } } @@ -309,17 +315,10 @@ impl DynamicMapping { fn add_node(&mut self, node: BodyNode) { match node { BodyNode::Element(el) => { - for attr in el.attributes { - match &attr.attr { - ElementAttr::CustomAttrText { value, .. } - | ElementAttr::AttrText { value, .. } - if value.is_static() => {} - - ElementAttr::AttrExpression { .. } - | ElementAttr::AttrText { .. } - | ElementAttr::CustomAttrText { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } => { + for attr in el.merged_attributes { + match &attr.attr.value { + ElementAttrValue::AttrLiteral(input) if input.is_static() => {} + _ => { self.insert_attribute(attr.attr); } } @@ -367,10 +366,11 @@ impl<'a> DynamicContext<'a> { let element_name_rust = el.name.to_string(); let mut static_attrs = Vec::new(); - for attr in &el.attributes { - match &attr.attr { - ElementAttr::AttrText { name, value } if value.is_static() => { + for attr in &el.merged_attributes { + match &attr.attr.value { + ElementAttrValue::AttrLiteral(value) if value.is_static() => { let value = value.source.as_ref().unwrap(); + let name = &attr.attr.name; let attribute_name_rust = name.to_string(); let (name, namespace) = Ctx::map_attribute(&element_name_rust, &attribute_name_rust) @@ -382,20 +382,7 @@ impl<'a> DynamicContext<'a> { }) } - ElementAttr::CustomAttrText { name, value } if value.is_static() => { - let value = value.source.as_ref().unwrap(); - static_attrs.push(TemplateAttribute::Static { - name: intern(name.value().as_str()), - namespace: None, - value: intern(value.value().as_str()), - }) - } - - ElementAttr::AttrExpression { .. } - | ElementAttr::AttrText { .. } - | ElementAttr::CustomAttrText { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } => { + _ => { let idx = match mapping { Some(mapping) => mapping.get_attribute_idx(&attr.attr)?, None => self.dynamic_attributes.len(), @@ -467,54 +454,47 @@ impl<'a> DynamicContext<'a> { ElementName::Ident(i) => quote! { dioxus_elements::#i::#name }, ElementName::Custom(_) => quote! { None }, }; - let static_attrs = el.attributes.iter().map(|attr| match &attr.attr { - ElementAttr::AttrText { name, value } if value.is_static() => { - let value = value.to_static().unwrap(); - let ns = ns(quote!(#name.1)); - let name = match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.0 }, - ElementName::Custom(_) => { - let as_string = name.to_string(); - quote! { #as_string } - } - }; - quote! { - ::dioxus::core::TemplateAttribute::Static { - name: #name, - namespace: #ns, - value: #value, + let static_attrs = el + .merged_attributes + .iter() + .map(|attr| match &attr.attr.value { + ElementAttrValue::AttrLiteral(value) if value.is_static() => { + let value = value.to_static().unwrap(); + let ns = { + match &attr.attr.name { + ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)), + ElementAttrName::Custom(_) => quote!(None), + } + }; + let name = &attr.attr.name; + let name = match (el_name, name) { + (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => { + quote! { #el_name::#name.0 } + } + _ => { + let as_string = name.to_string(); + quote! { #as_string } + } + }; + quote! { + ::dioxus::core::TemplateAttribute::Static { + name: #name, + namespace: #ns, + value: #value, - // todo: we don't diff these so we never apply the volatile flag - // volatile: dioxus_elements::#el_name::#name.2, + // todo: we don't diff these so we never apply the volatile flag + // volatile: dioxus_elements::#el_name::#name.2, + } } } - } - ElementAttr::CustomAttrText { name, value } if value.is_static() => { - let value = value.to_static().unwrap(); - quote! { - ::dioxus::core::TemplateAttribute::Static { - name: #name, - namespace: None, - value: #value, - - // todo: we don't diff these so we never apply the volatile flag - // volatile: dioxus_elements::#el_name::#name.2, - } + _ => { + let ct = self.dynamic_attributes.len(); + self.dynamic_attributes.push(attr); + self.attr_paths.push(self.current_path.clone()); + quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } } } - } - - ElementAttr::AttrExpression { .. } - | ElementAttr::AttrText { .. } - | ElementAttr::CustomAttrText { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } => { - let ct = self.dynamic_attributes.len(); - self.dynamic_attributes.push(attr); - self.attr_paths.push(self.current_path.clone()); - quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } } - } - }); + }); let attrs = quote! { #(#static_attrs),*}; diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index ed5e0bff4..c71611082 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -126,7 +126,7 @@ impl ToTokens for BodyNode { }), BodyNode::RawExpr(exp) => tokens.append_all(quote! { { - let ___nodes = (#exp).into_vnode(__cx); + let ___nodes = (#exp).into_dyn_node(__cx); ___nodes } }), @@ -145,7 +145,7 @@ impl ToTokens for BodyNode { // And then we can return them into the dyn loop tokens.append_all(quote! { { - let ___nodes =(#expr).into_iter().map(|#pat| { #renderer }).into_vnode(__cx); + let ___nodes =(#expr).into_iter().map(|#pat| { #renderer }).into_dyn_node(__cx); ___nodes } }) @@ -154,7 +154,7 @@ impl ToTokens for BodyNode { if is_if_chain_terminated(chain) { tokens.append_all(quote! { { - let ___nodes = (#chain).into_vnode(__cx); + let ___nodes = (#chain).into_dyn_node(__cx); ___nodes } }); @@ -211,7 +211,7 @@ impl ToTokens for BodyNode { tokens.append_all(quote! { { - let ___nodes = (#body).into_vnode(__cx); + let ___nodes = (#body).into_dyn_node(__cx); ___nodes } }); @@ -260,7 +260,7 @@ impl Parse for ForLoop { } } -fn is_if_chain_terminated(chain: &ExprIf) -> bool { +pub(crate) fn is_if_chain_terminated(chain: &ExprIf) -> bool { let mut current = chain; loop { if let Some((_, else_block)) = ¤t.else_branch { 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 31c7f33f5..2db5fdc66 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -256,7 +256,7 @@ fn to_string_works() { assert_eq!( item.1.segments, vec![ - PreRendered("
Hello world 1 -->123<-- Hello world 2
nest 1
nest 2
</diiiiiiiiv>
finalize 0
finalize 1
finalize 2
finalize 3
finalize 4
"); + 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] = &[ diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index b2a8e94c8..26235c925 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -13,8 +13,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { workspace = true, features = ["serialize"] } dioxus-html = { workspace = true, features = ["wasm-bind"] } dioxus-interpreter-js = { workspace = true, features = [ - "sledgehammer", "minimal_bindings", + "web", ] } js-sys = "0.3.56" 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 c535d961b..982368de7 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -24,8 +24,8 @@ pub struct WebsysDom { document: Document, #[allow(dead_code)] pub(crate) root: Element, - templates: FxHashMap, - max_template_id: u32, + templates: FxHashMap, + max_template_id: u16, pub(crate) interpreter: Channel, event_channel: mpsc::UnboundedSender, } @@ -98,7 +98,7 @@ impl WebsysDom { } })); - dioxus_interpreter_js::initilize( + dioxus_interpreter_js::initialize( root.clone().unchecked_into(), handler.as_ref().unchecked_ref(), ); @@ -183,7 +183,7 @@ impl WebsysDom { let mut to_mount = Vec::new(); for edit in &edits { match edit { - AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32), + AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u16), AssignId { path, id } => { i.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32) } @@ -194,15 +194,15 @@ impl WebsysDom { } LoadTemplate { name, index, id } => { if let Some(tmpl_id) = self.templates.get(*name) { - i.load_template(*tmpl_id, *index as u32, id.0 as u32) + i.load_template(*tmpl_id, *index as u16, id.0 as u32) } } - ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u32), + ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u16), ReplacePlaceholder { path, m } => { - i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u32) + i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u16) } - InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u32), - InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u32), + InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u16), + InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u16), SetAttribute { name, value, diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 90959a517..364f4aaa7 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -56,6 +56,7 @@ // - Do DOM work in the next requestAnimationFrame callback pub use crate::cfg::Config; +#[cfg(feature = "file_engine")] pub use crate::file_engine::WebFileEngineExt; use dioxus_core::{Element, Scope, VirtualDom}; use futures_util::{