diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 968d5f103..f69053742 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: # NOTE: Delete when the previous one is enabled - name: Setup mdBook run: | - cargo install mdbook --git https://github.com/Ruin0x11/mdBook.git --branch localization --rev e74fdb1 + cargo install mdbook --git https://github.com/Demonthos/mdBook.git --branch master - uses: actions/checkout@v3 - name: Build diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 6ae99612f..0dd8a1842 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -36,8 +36,8 @@ jobs: # There's a limit of 60 concurrent jobs across all repos in the rust-lang organization. # In order to prevent overusing too much of that 60 limit, we throttle the # number of rustfmt jobs that will run concurrently. - max-parallel: 2 - fail-fast: false + # max-parallel: + # fail-fast: false matrix: target: [x86_64-pc-windows-gnu, x86_64-pc-windows-msvc] cfg_release_channel: [stable] @@ -64,10 +64,21 @@ jobs: if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly' shell: bash - - name: checkout - uses: actions/checkout@v3 + # - name: checkout + # uses: actions/checkout@v3 + # with: + # path: C:/dioxus.git + # fetch-depth: 1 + + # we need to use the C drive as the working directory + + - name: Checkout + run: | + mkdir C:/dioxus.git + git clone https://github.com/dioxuslabs/dioxus.git C:/dioxus.git --depth 1 - name: test + working-directory: C:/dioxus.git run: | rustc -Vv cargo -V diff --git a/Cargo.toml b/Cargo.toml index 606aef6be..7d8cc6871 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "packages/native-core", "packages/native-core-macro", "packages/rsx-rosetta", + "packages/signals", "docs/guide", ] @@ -40,9 +41,10 @@ publish = false [dev-dependencies] dioxus = { path = "./packages/dioxus" } -dioxus-desktop = { path = "./packages/desktop" } +dioxus-desktop = { path = "./packages/desktop", features = ["transparent"] } dioxus-ssr = { path = "./packages/ssr" } dioxus-router = { path = "./packages/router" } +dioxus-signals = { path = "./packages/signals" } fermi = { path = "./packages/fermi" } futures-util = "0.3.21" log = "0.4.14" diff --git a/docs/guide/Cargo.toml b/docs/guide/Cargo.toml index 0f544e071..eb8060552 100644 --- a/docs/guide/Cargo.toml +++ b/docs/guide/Cargo.toml @@ -11,6 +11,7 @@ dioxus-desktop = { path = "../../packages/desktop" } dioxus-web = { path = "../../packages/web" } dioxus-ssr = { path = "../../packages/ssr" } dioxus-router = { path = "../../packages/router" } +dioxus-liveview = { path = "../../packages/liveview", features = ["axum"] } dioxus-tui = { path = "../../packages/tui" } fermi = { path = "../../packages/fermi" } @@ -19,3 +20,4 @@ fermi = { path = "../../packages/fermi" } serde = { version = "1.0.138", features=["derive"] } reqwest = { version = "0.11.11", features = ["json"] } tokio = { version = "1.19.2" , features=[]} +axum = { version = "0.6.1", features = ["ws"] } \ No newline at end of file diff --git a/docs/guide/book.toml b/docs/guide/book.toml index b0bae8d52..1f926256e 100644 --- a/docs/guide/book.toml +++ b/docs/guide/book.toml @@ -24,6 +24,8 @@ edit-url-template = "https://github.com/DioxusLabs/dioxus/edit/master/docs/guide [output.html.playground] editable = true line-numbers = true +# running examples will not work because dioxus is not a included in the playground +runnable = false [output.html.search] limit-results = 20 diff --git a/docs/guide/examples/boolean_attribute.rs b/docs/guide/examples/boolean_attribute.rs index 14a824b69..685103ccb 100644 --- a/docs/guide/examples/boolean_attribute.rs +++ b/docs/guide/examples/boolean_attribute.rs @@ -5,13 +5,14 @@ fn main() { dioxus_desktop::launch(App); } +#[rustfmt::skip] fn App(cx: Scope) -> Element { // ANCHOR: boolean_attribute - cx.render(rsx! { - div { - hidden: "false", - "hello" - } - }) +cx.render(rsx! { + div { + hidden: "false", + "hello" + } +}) // ANCHOR_END: boolean_attribute } diff --git a/docs/guide/examples/component_props_options.rs b/docs/guide/examples/component_props_options.rs index 8963f6374..df2ebf1db 100644 --- a/docs/guide/examples/component_props_options.rs +++ b/docs/guide/examples/component_props_options.rs @@ -5,49 +5,50 @@ fn main() { dioxus_desktop::launch(App); } +#[rustfmt::skip] fn App(cx: Scope) -> Element { cx.render(rsx! { // ANCHOR: OptionalProps_usage - Title { - title: "Some Title", - }, - Title { - title: "Some Title", - subtitle: "Some Subtitle", - }, - // Providing an Option explicitly won't compile though: - // Title { - // title: "Some Title", - // subtitle: None, - // }, +Title { + title: "Some Title", +}, +Title { + title: "Some Title", + subtitle: "Some Subtitle", +}, +// Providing an Option explicitly won't compile though: +// Title { +// title: "Some Title", +// subtitle: None, +// }, // ANCHOR_END: OptionalProps_usage // ANCHOR: ExplicitOption_usage - ExplicitOption { - title: "Some Title", - subtitle: None, - }, - ExplicitOption { - title: "Some Title", - subtitle: Some("Some Title"), - }, - // This won't compile: - // ExplicitOption { - // title: "Some Title", - // }, +ExplicitOption { + title: "Some Title", + subtitle: None, +}, +ExplicitOption { + title: "Some Title", + subtitle: Some("Some Title"), +}, +// This won't compile: +// ExplicitOption { +// title: "Some Title", +// }, // ANCHOR_END: ExplicitOption_usage // ANCHOR: DefaultComponent_usage - DefaultComponent { - number: 5, - }, - DefaultComponent {}, +DefaultComponent { + number: 5, +}, +DefaultComponent {}, // ANCHOR_END: DefaultComponent_usage // ANCHOR: IntoComponent_usage - IntoComponent { - string: "some &str", - }, +IntoComponent { + string: "some &str", +}, // ANCHOR_END: IntoComponent_usage }) } diff --git a/docs/guide/examples/conditional_rendering.rs b/docs/guide/examples/conditional_rendering.rs index 0b3ebfbfe..c6095df98 100644 --- a/docs/guide/examples/conditional_rendering.rs +++ b/docs/guide/examples/conditional_rendering.rs @@ -1,3 +1,4 @@ +#![allow(unused)] #![allow(non_snake_case)] use dioxus::prelude::*; @@ -16,6 +17,7 @@ pub fn App(cx: Scope) -> Element { } #[inline_props] +#[rustfmt::skip] fn LogIn<'a>( cx: Scope<'a>, is_logged_in: bool, @@ -23,38 +25,73 @@ fn LogIn<'a>( on_log_out: EventHandler<'a>, ) -> Element<'a> { // ANCHOR: if_else - if *is_logged_in { - cx.render(rsx! { - div { - "Welcome!", - button { - onclick: move |_| on_log_out.call(()), - "Log Out", - } - } - }) - } else { - cx.render(rsx! { - button { - onclick: move |_| on_log_in.call(()), - "Log In", - } - }) - } +if *is_logged_in { + cx.render(rsx! { + "Welcome!" + button { + onclick: move |_| on_log_out.call(()), + "Log Out", + } + }) +} else { + cx.render(rsx! { + button { + onclick: move |_| on_log_in.call(()), + "Log In", + } + }) +} // ANCHOR_END: if_else } #[inline_props] +#[rustfmt::skip] +fn LogInImproved<'a>( + cx: Scope<'a>, + is_logged_in: bool, + on_log_in: EventHandler<'a>, + on_log_out: EventHandler<'a>, +) -> Element<'a> { + // ANCHOR: if_else_improved +cx.render(rsx! { + // We only render the welcome message if we are logged in + // You can use if statements in the middle of a render block to conditionally render elements + if *is_logged_in { + // Notice the body of this if statment is rsx code, not an expression + "Welcome!" + } + button { + // depending on the value of `is_logged_in`, we will call a different event handler + onclick: move |_| if *is_logged_in { + on_log_in.call(()) + } + else{ + on_log_out.call(()) + }, + if *is_logged_in { + // if we are logged in, the button should say "Log Out" + "Log Out" + } else { + // if we are not logged in, the button should say "Log In" + "Log In" + } + } +}) + // ANCHOR_END: if_else_improved +} + +#[inline_props] +#[rustfmt::skip] fn LogInWarning(cx: Scope, is_logged_in: bool) -> Element { // ANCHOR: conditional_none - if *is_logged_in { - return cx.render(rsx!(())); - } +if *is_logged_in { + return None; +} - cx.render(rsx! { - a { - "You must be logged in to comment" - } - }) +cx.render(rsx! { + a { + "You must be logged in to comment" + } +}) // ANCHOR_END: conditional_none } diff --git a/docs/guide/examples/dangerous_inner_html.rs b/docs/guide/examples/dangerous_inner_html.rs index f5f976e59..fe8a52663 100644 --- a/docs/guide/examples/dangerous_inner_html.rs +++ b/docs/guide/examples/dangerous_inner_html.rs @@ -5,15 +5,16 @@ fn main() { dioxus_desktop::launch(App); } +#[rustfmt::skip] fn App(cx: Scope) -> Element { // ANCHOR: dangerous_inner_html - // this should come from a trusted source - let contents = "live dangerously"; +// this should come from a trusted source +let contents = "live dangerously"; - cx.render(rsx! { - div { - dangerous_inner_html: "{contents}", - } - }) +cx.render(rsx! { + div { + dangerous_inner_html: "{contents}", + } +}) // ANCHOR_END: dangerous_inner_html } diff --git a/docs/guide/examples/event_click.rs b/docs/guide/examples/event_click.rs index d6ccc0682..bd25c5f36 100644 --- a/docs/guide/examples/event_click.rs +++ b/docs/guide/examples/event_click.rs @@ -5,13 +5,14 @@ fn main() { dioxus_desktop::launch(App); } +#[rustfmt::skip] fn App(cx: Scope) -> Element { // ANCHOR: rsx - cx.render(rsx! { - button { - onclick: move |event| println!("Clicked! Event: {event:?}"), - "click me!" - } - }) +cx.render(rsx! { + button { + onclick: move |event| println!("Clicked! Event: {event:?}"), + "click me!" + } +}) // ANCHOR_END: rsx } diff --git a/docs/guide/examples/event_nested.rs b/docs/guide/examples/event_nested.rs index 9e85dfca4..b31e340f1 100644 --- a/docs/guide/examples/event_nested.rs +++ b/docs/guide/examples/event_nested.rs @@ -5,20 +5,21 @@ fn main() { dioxus_desktop::launch(App); } +#[rustfmt::skip] fn App(cx: Scope) -> Element { // ANCHOR: rsx - cx.render(rsx! { - div { - onclick: move |_event| {}, - "outer", - button { - onclick: move |event| { - // now, outer won't be triggered - event.stop_propogation(); - }, - "inner" - } +cx.render(rsx! { + div { + onclick: move |_event| {}, + "outer", + button { + onclick: move |event| { + // now, outer won't be triggered + event.stop_propagation(); + }, + "inner" } - }) + } +}) // ANCHOR_END: rsx } diff --git a/docs/guide/examples/event_prevent_default.rs b/docs/guide/examples/event_prevent_default.rs index 120f74157..b109700bf 100644 --- a/docs/guide/examples/event_prevent_default.rs +++ b/docs/guide/examples/event_prevent_default.rs @@ -5,13 +5,14 @@ fn main() { dioxus_desktop::launch(App); } +#[rustfmt::skip] fn App(cx: Scope) -> Element { // ANCHOR: prevent_default - cx.render(rsx! { - input { - prevent_default: "oninput", - prevent_default: "onclick", - } - }) +cx.render(rsx! { + input { + prevent_default: "oninput", + prevent_default: "onclick", + } +}) // ANCHOR_END: prevent_default } diff --git a/docs/guide/examples/hello_world_desktop.rs b/docs/guide/examples/hello_world_desktop.rs index 91f0db03f..9d5a02051 100644 --- a/docs/guide/examples/hello_world_desktop.rs +++ b/docs/guide/examples/hello_world_desktop.rs @@ -1,12 +1,15 @@ // ANCHOR: all #![allow(non_snake_case)] +// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types use dioxus::prelude::*; fn main() { + // launch the dioxus app in a webview dioxus_desktop::launch(App); } // ANCHOR: component +// define a component that renders a div with the text "Hello, world!" fn App(cx: Scope) -> Element { cx.render(rsx! { div { diff --git a/docs/guide/examples/hello_world_liveview.rs b/docs/guide/examples/hello_world_liveview.rs new file mode 100644 index 000000000..e9170cf62 --- /dev/null +++ b/docs/guide/examples/hello_world_liveview.rs @@ -0,0 +1,60 @@ +// ANCHOR: all +use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; +use dioxus::prelude::*; + +// ANCHOR: glue +#[tokio::main] +async fn main() { + let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); + + let view = dioxus_liveview::LiveViewPool::new(); + + let app = Router::new() + // The root route contains the glue code to connect to the WebSocket + .route( + "/", + get(move || async move { + Html(format!( + r#" + + + Dioxus LiveView with Axum +
+ {glue} + + "#, + // Create the glue code to connect to the WebSocket on the "/ws" route + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws")) + )) + }), + ) + // The WebSocket route is what Dioxus uses to communicate with the browser + .route( + "/ws", + get(move |ws: WebSocketUpgrade| async move { + ws.on_upgrade(move |socket| async move { + // When the WebSocket is upgraded, launch the LiveView with the app component + _ = 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(); +} +// ANCHOR_END: glue + +// ANCHOR: app +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + "Hello, world!" + } + }) +} +// ANCHOR_END: app +// ANCHOR_END: all diff --git a/docs/guide/examples/hello_world_tui.rs b/docs/guide/examples/hello_world_tui.rs index ef9b7ec47..dc4f0ee71 100644 --- a/docs/guide/examples/hello_world_tui.rs +++ b/docs/guide/examples/hello_world_tui.rs @@ -1,20 +1,17 @@ #![allow(non_snake_case)] +// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types use dioxus::prelude::*; fn main() { + // launch the app in the terminal dioxus_tui::launch(App); } +// create a component that renders a div with the text "Hello, world!" fn App(cx: Scope) -> Element { cx.render(rsx! { div { - width: "100%", - height: "10px", - background_color: "red", - justify_content: "center", - align_items: "center", - - "Hello world!" + "Hello, world!" } }) } diff --git a/docs/guide/examples/hello_world_web.rs b/docs/guide/examples/hello_world_web.rs index 5f813bbe1..7b013bc3d 100644 --- a/docs/guide/examples/hello_world_web.rs +++ b/docs/guide/examples/hello_world_web.rs @@ -1,10 +1,13 @@ #![allow(non_snake_case)] +// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types use dioxus::prelude::*; fn main() { + // launch the web app dioxus_web::launch(App); } +// create a component that renders a div with the text "Hello, world!" fn App(cx: Scope) -> Element { cx.render(rsx! { div { diff --git a/docs/guide/examples/hooks_bad.rs b/docs/guide/examples/hooks_bad.rs index ff9d3417a..cbaa54e90 100644 --- a/docs/guide/examples/hooks_bad.rs +++ b/docs/guide/examples/hooks_bad.rs @@ -7,60 +7,61 @@ fn main() { dioxus_desktop::launch(App); } +#[rustfmt::skip] fn App(cx: Scope) -> Element { let you_are_happy = true; let you_know_it = false; // ANCHOR: conditional - // ❌ don't call hooks in conditionals! - // We must ensure that the same hooks will be called every time - // But `if` statements only run if the conditional is true! - // So we might violate rule 2. - if you_are_happy && you_know_it { - let something = use_state(cx, || "hands"); - println!("clap your {something}") - } - - // ✅ instead, *always* call use_state - // You can put other stuff in the conditional though +// ❌ don't call hooks in conditionals! +// We must ensure that the same hooks will be called every time +// But `if` statements only run if the conditional is true! +// So we might violate rule 2. +if you_are_happy && you_know_it { let something = use_state(cx, || "hands"); - if you_are_happy && you_know_it { - println!("clap your {something}") - } + println!("clap your {something}") +} + +// ✅ instead, *always* call use_state +// You can put other stuff in the conditional though +let something = use_state(cx, || "hands"); +if you_are_happy && you_know_it { + println!("clap your {something}") +} // ANCHOR_END: conditional // ANCHOR: closure - // ❌ don't call hooks inside closures! - // We can't guarantee that the closure, if used, will be called at the same time every time - let _a = || { - let b = use_state(cx, || 0); - b.get() - }; - - // ✅ instead, move hook `b` outside +// ❌ don't call hooks inside closures! +// We can't guarantee that the closure, if used, will be called in the same order every time +let _a = || { let b = use_state(cx, || 0); - let _a = || b.get(); + b.get() +}; + +// ✅ instead, move hook `b` outside +let b = use_state(cx, || 0); +let _a = || b.get(); // ANCHOR_END: closure let names: Vec<&str> = vec![]; // ANCHOR: loop - // `names` is a Vec<&str> +// `names` is a Vec<&str> - // ❌ Do not use hooks in loops! - // In this case, if the length of the Vec changes, we break rule 2 - for _name in &names { - let is_selected = use_state(cx, || false); - println!("selected: {is_selected}"); - } +// ❌ Do not use hooks in loops! +// In this case, if the length of the Vec changes, we break rule 2 +for _name in &names { + let is_selected = use_state(cx, || false); + println!("selected: {is_selected}"); +} - // ✅ Instead, use a hashmap with use_ref - let selection_map = use_ref(cx, HashMap::<&str, bool>::new); +// ✅ Instead, use a hashmap with use_ref +let selection_map = use_ref(cx, HashMap::<&str, bool>::new); - for name in &names { - let is_selected = selection_map.read()[name]; - println!("selected: {is_selected}"); - } +for name in &names { + let is_selected = selection_map.read()[name]; + println!("selected: {is_selected}"); +} // ANCHOR_END: loop cx.render(rsx!(())) diff --git a/docs/guide/examples/hooks_counter.rs b/docs/guide/examples/hooks_counter.rs index 1fb2a0bd7..86705f4af 100644 --- a/docs/guide/examples/hooks_counter.rs +++ b/docs/guide/examples/hooks_counter.rs @@ -7,12 +7,25 @@ fn main() { // ANCHOR: component fn App(cx: Scope) -> Element { + // count will be initialized to 0 the first time the component is rendered let mut count = use_state(cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + // changing the count will cause the component to re-render + count += 1 + }, + "Up high!" + } + button { + onclick: move |_| { + // changing the count will cause the component to re-render + count -= 1 + }, + "Down low!" + } )) } // ANCHOR_END: component diff --git a/docs/guide/examples/hooks_use_ref.rs b/docs/guide/examples/hooks_use_ref.rs index 4910325de..00794fbb7 100644 --- a/docs/guide/examples/hooks_use_ref.rs +++ b/docs/guide/examples/hooks_use_ref.rs @@ -8,13 +8,12 @@ fn main() { // ANCHOR: component fn App(cx: Scope) -> Element { let list = use_ref(cx, Vec::new); - let list_formatted = format!("{:?}", *list.read()); cx.render(rsx!( - p { "Current list: {list_formatted}" } + p { "Current list: {list.read():?}" } button { onclick: move |event| { - list.write().push(event) + list.with_mut(|list| list.push(event)); }, "Click me!" } diff --git a/docs/guide/examples/meme_editor_dark_mode.rs b/docs/guide/examples/meme_editor_dark_mode.rs index e394a4a7f..e62a64eb7 100644 --- a/docs/guide/examples/meme_editor_dark_mode.rs +++ b/docs/guide/examples/meme_editor_dark_mode.rs @@ -11,9 +11,10 @@ fn main() { struct DarkMode(bool); // ANCHOR_END: DarkMode_struct +#[rustfmt::skip] pub fn App(cx: Scope) -> Element { // ANCHOR: context_provider - use_shared_state_provider(cx, || DarkMode(false)); +use_shared_state_provider(cx, || DarkMode(false)); // ANCHOR_END: context_provider let is_dark_mode = use_is_dark_mode(cx); @@ -34,9 +35,10 @@ pub fn App(cx: Scope) -> Element { })) } +#[rustfmt::skip] pub fn use_is_dark_mode(cx: &ScopeState) -> bool { // ANCHOR: use_context - let dark_mode_context = use_shared_state::(cx); +let dark_mode_context = use_shared_state::(cx); // ANCHOR_END: use_context dark_mode_context diff --git a/docs/guide/examples/rendering_lists.rs b/docs/guide/examples/rendering_lists.rs index 75dc69b58..b95e712dc 100644 --- a/docs/guide/examples/rendering_lists.rs +++ b/docs/guide/examples/rendering_lists.rs @@ -12,42 +12,80 @@ struct Comment { id: usize, } +#[rustfmt::skip] pub fn App(cx: Scope) -> Element { // ANCHOR: render_list - let comment_field = use_state(cx, String::new); - let mut next_id = use_state(cx, || 0); - let comments = use_ref(cx, Vec::::new); +let comment_field = use_state(cx, String::new); +let mut next_id = use_state(cx, || 0); +let comments = use_ref(cx, Vec::::new); - let comments_lock = comments.read(); - let comments_rendered = comments_lock.iter().map(|comment| { - cx.render(rsx!(CommentComponent { +let comments_lock = comments.read(); +let comments_rendered = comments_lock.iter().map(|comment| { + rsx!(CommentComponent { + key: "{comment.id}", + comment: comment.clone(), + }) +}); + +cx.render(rsx!( + form { + onsubmit: move |_| { + comments.write().push(Comment { + content: comment_field.get().clone(), + id: *next_id.get(), + }); + next_id += 1; + + comment_field.set(String::new()); + }, + input { + value: "{comment_field}", + oninput: |event| comment_field.set(event.value.clone()), + } + input { + r#type: "submit", + } + }, + comments_rendered, +)) + // ANCHOR_END: render_list +} + +#[rustfmt::skip] +pub fn AppForLoop(cx: Scope) -> Element { + // ANCHOR: render_list_for_loop +let comment_field = use_state(cx, String::new); +let mut next_id = use_state(cx, || 0); +let comments = use_ref(cx, Vec::::new); + +cx.render(rsx!( + form { + onsubmit: move |_| { + comments.write().push(Comment { + content: comment_field.get().clone(), + id: *next_id.get(), + }); + next_id += 1; + + comment_field.set(String::new()); + }, + input { + value: "{comment_field}", + oninput: |event| comment_field.set(event.value.clone()), + } + input { + r#type: "submit", + } + }, + for comment in &*comments.read() { + // Notice the body of this for loop is rsx code, not an expression + CommentComponent { key: "{comment.id}", comment: comment.clone(), - })) - }); - - cx.render(rsx!( - form { - onsubmit: move |_| { - comments.write().push(Comment { - content: comment_field.get().clone(), - id: *next_id.get(), - }); - next_id += 1; - - comment_field.set(String::new()); - }, - input { - value: "{comment_field}", - oninput: |event| comment_field.set(event.value.clone()), - } - input { - r#type: "submit", - } - }, - comments_rendered, - )) - // ANCHOR_END: render_list + } + } +)) + // ANCHOR_END: render_list_for_loop } #[inline_props] diff --git a/docs/guide/examples/rsx_overview.rs b/docs/guide/examples/rsx_overview.rs index 64fa24c78..93c991d0d 100644 --- a/docs/guide/examples/rsx_overview.rs +++ b/docs/guide/examples/rsx_overview.rs @@ -19,91 +19,151 @@ pub fn App(cx: Scope) -> Element { )) } +#[rustfmt::skip] pub fn Empty(cx: Scope) -> Element { // ANCHOR: empty - cx.render(rsx!(div {})) +cx.render(rsx!(div { + // attributes / listeners + // children +})) // ANCHOR_END: empty } +#[rustfmt::skip] pub fn Children(cx: Scope) -> Element { // ANCHOR: children - cx.render(rsx!(ol { - li {"First Item"} - li {"Second Item"} - li {"Third Item"} - })) +cx.render(rsx!(ol { + li {"First Item"} + li {"Second Item"} + li {"Third Item"} +})) // ANCHOR_END: children } +#[rustfmt::skip] pub fn Fragments(cx: Scope) -> Element { // ANCHOR: fragments - cx.render(rsx!( - p {"First Item"}, - p {"Second Item"}, - Fragment { - span { "a group" }, - span { "of three" }, - span { "items" }, - } - )) +cx.render(rsx!( + p {"First Item"}, + p {"Second Item"}, + Fragment { + span { "a group" }, + span { "of three" }, + span { "items" }, + } +)) // ANCHOR_END: fragments } +#[rustfmt::skip] +pub fn ManyRoots(cx: Scope) -> Element { + // ANCHOR: manyroots +cx.render(rsx!( + p {"First Item"}, + p {"Second Item"}, +)) + // ANCHOR_END: manyroots +} + +#[rustfmt::skip] pub fn Attributes(cx: Scope) -> Element { // ANCHOR: attributes - cx.render(rsx!(a { - href: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - class: "primary_button", - "Log In" - })) +cx.render(rsx!(a { + href: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + class: "primary_button", + color: "red", +})) // ANCHOR_END: attributes } +#[rustfmt::skip] pub fn VariableAttributes(cx: Scope) -> Element { // ANCHOR: variable_attributes - let written_in_rust = true; - let button_type = "button"; - cx.render(rsx!(button { - disabled: "{written_in_rust}", - class: "{button_type}", - "Rewrite it in rust" - })) +let written_in_rust = true; +let button_type = "button"; +cx.render(rsx!(button { + disabled: "{written_in_rust}", + class: "{button_type}", + "Rewrite it in rust" +})) // ANCHOR_END: variable_attributes } +#[rustfmt::skip] pub fn CustomAttributes(cx: Scope) -> Element { // ANCHOR: custom_attributes cx.render(rsx!(b { "customAttribute": "value", - "Rust is Cool" })) // ANCHOR_END: custom_attributes } +#[rustfmt::skip] pub fn Formatting(cx: Scope) -> Element { // ANCHOR: formatting - let coordinates = (42, 0); - let country = "es"; - cx.render(rsx!(div { - class: "country-{country}", - "Coordinates: {coordinates:?}", - // arbitrary expressions are allowed, - // as long as they don't contain `{}` - div { - "{country.to_uppercase()}" - }, - div { - "{7*6}" - }, - })) - // ANCHOR_END: formatting +let coordinates = (42, 0); +let country = "es"; +cx.render(rsx!(div { + class: "country-{country}", + "position": "{coordinates:?}", + // arbitrary expressions are allowed, + // as long as they don't contain `{}` + div { + "{country.to_uppercase()}" + }, + div { + "{7*6}" + }, + // {} can be escaped with {{}} + div { + "{{}}" + }, +})) +// ANCHOR_END: formatting } +#[rustfmt::skip] pub fn Expression(cx: Scope) -> Element { // ANCHOR: expression - let text = "Dioxus"; - cx.render(rsx!(span { - text.to_uppercase() - })) +let text = "Dioxus"; +cx.render(rsx!(span { + text.to_uppercase(), + // create a list of text from 0 to 9 + (0..10).map(|i| rsx!{ i.to_string() }) +})) // ANCHOR_END: expression } + +#[rustfmt::skip] +pub fn Loops(cx: Scope) -> Element { + // ANCHOR: loops +cx.render(rsx!{ + // use a for loop where the body itself is RSX + div { + // create a list of text from 0 to 9 + for i in 0..3 { + // NOTE: the body of the loop is RSX not a rust statement + div { + "{i}" + } + } + } + // iterator equivalent + div { + (0..3).map(|i| rsx!{ div { "{i}" } }) + } +}) + // ANCHOR_END: loops +} + +#[rustfmt::skip] +pub fn IfStatements(cx: Scope) -> Element { + // ANCHOR: ifstatements +cx.render(rsx!{ + // use if statements without an else + if true { + rsx!(div { "true" }) + } +}) + // ANCHOR_END: ifstatements +} diff --git a/docs/guide/examples/use_future.rs b/docs/guide/examples/use_future.rs index c0d41e5af..01464704a 100644 --- a/docs/guide/examples/use_future.rs +++ b/docs/guide/examples/use_future.rs @@ -12,49 +12,51 @@ struct ApiResponse { image_url: String, } +#[rustfmt::skip] fn App(cx: Scope) -> Element { - // ANCHOR: use_future - let future = use_future(cx, (), |_| async move { - reqwest::get("https://dog.ceo/api/breeds/image/random") - .await - .unwrap() - .json::() - .await - }); - // ANCHOR_END: use_future +// ANCHOR: use_future +let future = use_future(cx, (), |_| async move { + reqwest::get("https://dog.ceo/api/breeds/image/random") + .await + .unwrap() + .json::() + .await +}); +// ANCHOR_END: use_future - // ANCHOR: render - cx.render(match future.value() { - Some(Ok(response)) => rsx! { - button { - onclick: move |_| future.restart(), - "Click to fetch another doggo" +// ANCHOR: render +cx.render(match future.value() { + Some(Ok(response)) => rsx! { + button { + onclick: move |_| future.restart(), + "Click to fetch another doggo" + } + div { + img { + max_width: "500px", + max_height: "500px", + src: "{response.image_url}", } - div { - img { - max_width: "500px", - max_height: "500px", - src: "{response.image_url}", - } - } - }, - Some(Err(_)) => rsx! { div { "Loading dogs failed" } }, - None => rsx! { div { "Loading dogs..." } }, - }) - // ANCHOR_END: render + } + }, + Some(Err(_)) => rsx! { div { "Loading dogs failed" } }, + None => rsx! { div { "Loading dogs..." } }, +}) +// ANCHOR_END: render } +#[rustfmt::skip] #[inline_props] fn RandomDog(cx: Scope, breed: String) -> Element { - // ANCHOR: dependency - let future = use_future(cx, (breed,), |(breed,)| async move { - reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) - .await - .unwrap() - .json::() - .await - }); - // ANCHOR_END: dependency +// ANCHOR: dependency +let future = use_future(cx, (breed,), |(breed,)| async move { + reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) + .await + .unwrap() + .json::() + .await +}); +// ANCHOR_END: dependency cx.render(rsx!(())) } diff --git a/docs/guide/src/en/SUMMARY.md b/docs/guide/src/en/SUMMARY.md index 28ecfe688..83840fe90 100644 --- a/docs/guide/src/en/SUMMARY.md +++ b/docs/guide/src/en/SUMMARY.md @@ -7,6 +7,7 @@ - [Web](getting_started/web.md) - [Hot Reload](getting_started/hot_reload.md) - [Server-Side Rendering](getting_started/ssr.md) + - [Liveview](getting_started/liveview.md) - [Terminal UI](getting_started/tui.md) - [Mobile](getting_started/mobile.md) - [Describing the UI](describing_ui/index.md) diff --git a/docs/guide/src/en/async/spawn.md b/docs/guide/src/en/async/spawn.md index d001204c6..b29f63ebf 100644 --- a/docs/guide/src/en/async/spawn.md +++ b/docs/guide/src/en/async/spawn.md @@ -8,16 +8,6 @@ The `use_future` and `use_coroutine` hooks are useful if you want to uncondition > Note: `spawn` will always spawn a *new* future. You most likely don't want to call it on every render. -The future must be `'static` – so any values captured by the task cannot carry any references to `cx`, such as a `UseState`. - -However, since you'll typically need a way to update the value of a hook, you can use `to_owned` to create a clone of the hook handle. You can then use that clone in the async closure. - -To make this a bit less verbose, Dioxus exports the `to_owned!` macro which will create a binding as shown above, which can be quite helpful when dealing with many values. - -```rust -{{#include ../../../examples/spawn.rs:to_owned_macro}} -``` - Calling `spawn` will give you a `JoinHandle` which lets you cancel or pause the future. ## Spawning Tokio Tasks diff --git a/docs/guide/src/en/async/use_coroutine.md b/docs/guide/src/en/async/use_coroutine.md index 326141b2b..95c932901 100644 --- a/docs/guide/src/en/async/use_coroutine.md +++ b/docs/guide/src/en/async/use_coroutine.md @@ -1,12 +1,12 @@ # Coroutines -Another good tool to keep in your async toolbox are coroutines. Coroutines are futures that can be manually stopped, started, paused, and resumed. +Another tool in your async toolbox are coroutines. Coroutines are futures that can be manually stopped, started, paused, and resumed. -Like regular futures, code in a Dioxus coroutine will run until the next `await` point before yielding. This low-level control over asynchronous tasks is quite powerful, allowing for infinitely looping tasks like WebSocket polling, background timers, and other periodic actions. +Like regular futures, code in a coroutine will run until the next `await` point before yielding. This low-level control over asynchronous tasks is quite powerful, allowing for infinitely looping tasks like WebSocket polling, background timers, and other periodic actions. ## `use_coroutine` -The basic setup for coroutines is the `use_coroutine` hook. Most coroutines we write will be polling loops using async/await. +The `use_coroutine` hook allows you to create a coroutine. Most coroutines we write will be polling loops using async/await. ```rust fn app(cx: Scope) -> Element { @@ -50,11 +50,45 @@ if sync.is_running() { This pattern is where coroutines are extremely useful – instead of writing all the complicated logic for pausing our async tasks like we would with JavaScript promises, the Rust model allows us to just not poll our future. +## Yielding Values + +To yield values from a coroutine, simply bring in a `UseState` handle and set the value whenever your coroutine completes its work. + +The future must be `'static` – so any values captured by the task cannot carry any references to `cx`, such as a `UseState`. + +You can use [to_owned](https://doc.rust-lang.org/std/borrow/trait.ToOwned.html#tymethod.to_owned) to create a clone of the hook handle which can be moved into the async closure. + +```rust +let sync_status = use_state(cx, || Status::Launching); +let sync_task = use_coroutine(cx, |rx: UnboundedReceiver| { + let sync_status = sync_status.to_owned(); + async move { + loop { + delay_ms(1000).await; + sync_status.set(Status::Working); + } + } +}) +``` + +To make this a bit less verbose, Dioxus exports the `to_owned!` macro which will create a binding as shown above, which can be quite helpful when dealing with many values. + +```rust +let sync_status = use_state(cx, || Status::Launching); +let load_status = use_state(cx, || Status::Launching); +let sync_task = use_coroutine(cx, |rx: UnboundedReceiver| { + to_owned![sync_status, load_status]; + async move { + // ... + } +}) +``` + ## Sending Values You might've noticed the `use_coroutine` closure takes an argument called `rx`. What is that? Well, a common pattern in complex apps is to handle a bunch of async code at once. With libraries like Redux Toolkit, managing multiple promises at once can be challenging and a common source of bugs. -With Coroutines, we have the opportunity to centralize our async logic. The `rx` parameter is an Unbounded Channel for code external to the coroutine to send data *into* the coroutine. Instead of looping on an external service, we can loop on the channel itself, processing messages from within our app without needing to spawn a new future. To send data into the coroutine, we would call "send" on the handle. +With Coroutines, we can centralize our async logic. The `rx` parameter is an Channel that allows code external to the coroutine to send data *into* the coroutine. Instead of looping on an external service, we can loop on the channel itself, processing messages from within our app without needing to spawn a new future. To send data into the coroutine, we would call "send" on the handle. ```rust @@ -103,7 +137,7 @@ async fn editor_service(rx: UnboundedReceiver) { } ``` -We can combine coroutines with Fermi to emulate Redux Toolkit's Thunk system with much less headache. This lets us store all of our app's state *within* a task and then simply update the "view" values stored in Atoms. It cannot be understated how powerful this technique is: we get all the perks of native Rust tasks with the optimizations and ergonomics of global state. This means your *actual* state does not need to be tied up in a system like Fermi or Redux – the only Atoms that need to exist are those that are used to drive the display/UI. +We can combine coroutines with [Fermi](https://docs.rs/fermi/latest/fermi/index.html) to emulate Redux Toolkit's Thunk system with much less headache. This lets us store all of our app's state *within* a task and then simply update the "view" values stored in Atoms. It cannot be understated how powerful this technique is: we get all the perks of native Rust tasks with the optimizations and ergonomics of global state. This means your *actual* state does not need to be tied up in a system like Fermi or Redux – the only Atoms that need to exist are those that are used to drive the display/UI. ```rust static USERNAME: Atom = |_| "default".to_string(); @@ -152,27 +186,9 @@ async fn sync_service(mut rx: UnboundedReceiver, atoms: AtomRoot) { } ``` -## Yielding Values - -To yield values from a coroutine, simply bring in a `UseState` handle and set the value whenever your coroutine completes its work. - - -```rust -let sync_status = use_state(cx, || Status::Launching); -let sync_task = use_coroutine(cx, |rx: UnboundedReceiver| { - to_owned![sync_status]; - async move { - loop { - delay_ms(1000).await; - sync_status.set(Status::Working); - } - } -}) -``` - ## Automatic injection into the Context API -Coroutine handles are automatically injected through the context API. `use_coroutine_handle` with the message type as a generic can be used to fetch a handle. +Coroutine handles are automatically injected through the context API. You can use the `use_coroutine_handle` hook with the message type as a generic to fetch a handle. ```rust fn Child(cx: Scope) -> Element { diff --git a/docs/guide/src/en/async/use_future.md b/docs/guide/src/en/async/use_future.md index 0e3071bc1..864dd5acb 100644 --- a/docs/guide/src/en/async/use_future.md +++ b/docs/guide/src/en/async/use_future.md @@ -2,7 +2,7 @@ [`use_future`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_future.html) lets you run an async closure, and provides you with its result. -For example, we can make an API request inside `use_future`: +For example, we can make an API request (using [reqwest](https://docs.rs/reqwest/latest/reqwest/index.html)) inside `use_future`: ```rust {{#include ../../../examples/use_future.rs:use_future}} @@ -25,7 +25,7 @@ The `UseFuture` handle provides a `restart` method. It can be used to execute th ## Dependencies -Often, you will need to run the future again every time some value (e.g. a prop) changes. Rather than `.restart`ing it manually, you can provide a tuple of "dependencies" to the hook. It will automatically re-run the future when any of those dependencies change. Example: +Often, you will need to run the future again every time some value (e.g. a prop) changes. Rather than calling `restart` manually, you can provide a tuple of "dependencies" to the hook. It will automatically re-run the future when any of those dependencies change. Example: ```rust diff --git a/docs/guide/src/en/best_practices/antipatterns.md b/docs/guide/src/en/best_practices/antipatterns.md index 36aee2c79..873af6e39 100644 --- a/docs/guide/src/en/best_practices/antipatterns.md +++ b/docs/guide/src/en/best_practices/antipatterns.md @@ -1,6 +1,6 @@ # Antipatterns -This example shows what not to do and provides a reason why a given pattern is considered an "AntiPattern". Most anti-patterns are considered wrong due performance reasons, or for harming code re-usability. +This example shows what not to do and provides a reason why a given pattern is considered an "AntiPattern". Most anti-patterns are considered wrong for performance or code re-usability reasons. ## Unnecessarily Nested Fragments @@ -14,7 +14,7 @@ Only Component and Fragment nodes are susceptible to this issue. Dioxus mitigate ## Incorrect Iterator Keys -As described in the conditional rendering chapter, list items must have unique keys that are associated with the same items across renders. This helps Dioxus associate state with the contained components, and ensures good diffing performance. Do not omit keys, unless you know that the list is static and will never change. +As described in the [dynamic rendering chapter](../interactivity/dynamic_rendering.md#the-key-attribute), list items must have unique keys that are associated with the same items across renders. This helps Dioxus associate state with the contained components and ensures good diffing performance. Do not omit keys, unless you know that the list will never change. ```rust {{#include ../../../examples/anti_patterns.rs:iter_keys}} diff --git a/docs/guide/src/en/contributing.md b/docs/guide/src/en/contributing.md index 0bfde1a15..35efa11ac 100644 --- a/docs/guide/src/en/contributing.md +++ b/docs/guide/src/en/contributing.md @@ -1,6 +1,6 @@ # Contributing -Development happens in the [Dioxus GitHub repository](https://github.com/DioxusLabs/dioxus). If you've found a bug or have an idea for a feature, please submit an issue (check if someone hasn't [done it already](https://github.com/DioxusLabs/dioxus/issues) though). +Development happens in the [Dioxus GitHub repository](https://github.com/DioxusLabs/dioxus). If you've found a bug or have an idea for a feature, please submit an issue (but first check if someone hasn't [done it already](https://github.com/DioxusLabs/dioxus/issues)). [GitHub discussions](https://github.com/DioxusLabs/dioxus/discussions) can be used as a place to ask for help or talk about features. You can also join [our Discord channel](https://discord.gg/XgGxMSkvUM) where some development discussion happens. @@ -10,11 +10,11 @@ If you'd like to improve the docs, PRs are welcome! Both Rust docs ([source](htt ## Working on the Ecosystem -Part of what makes React great is the rich ecosystem. We'd like the same for Dioxus! So if you have a library in mind that you'd like to write, and that you think many people would benefit from, it will be appreciated. You can [browse npm.js](https://www.npmjs.com/search?q=keywords:react-component) for inspiration. +Part of what makes React great is the rich ecosystem. We'd like the same for Dioxus! So if you have a library in mind that you'd like to write and many people would benefit from, it will be appreciated. You can [browse npm.js](https://www.npmjs.com/search?q=keywords:react-component) for inspiration. ## Bugs & Features -If you've fixed [an issue that's open](https://github.com/DioxusLabs/dioxus/issues), feel free to submit a PR! You can also take a look at [the roadmap](./roadmap.md) and work on something in there. Consider [reaching out](https://discord.gg/XgGxMSkvUM) to the team first to make sure everyone's on the same page, and you don't do useless work! +If you've fixed [an open issue](https://github.com/DioxusLabs/dioxus/issues), feel free to submit a PR! You can also take a look at [the roadmap](./roadmap.md) and work on something in there. Consider [reaching out](https://discord.gg/XgGxMSkvUM) to the team first to make sure everyone's on the same page, and you don't do useless work! All pull requests (including those made by a team member) must be approved by at least one other team member. Larger, more nuanced decisions about design, architecture, breaking changes, trade-offs, etc. are made by team consensus. diff --git a/docs/guide/src/en/custom_renderer/index.md b/docs/guide/src/en/custom_renderer/index.md index 347176b1f..17c2cca07 100644 --- a/docs/guide/src/en/custom_renderer/index.md +++ b/docs/guide/src/en/custom_renderer/index.md @@ -11,46 +11,50 @@ Implementing the renderer is fairly straightforward. The renderer needs to: 1. Handle the stream of edits generated by updates to the virtual DOM 2. Register listeners and pass events into the virtual DOM's event system -Essentially, your renderer needs to implement the `RealDom` trait and generate `EventTrigger` objects to update the VirtualDOM. From there, you'll have everything needed to render the VirtualDOM to the screen. +Essentially, your renderer needs to process edits and generate events to update the VirtualDOM. From there, you'll have everything needed to render the VirtualDOM to the screen. Internally, Dioxus handles the tree relationship, diffing, memory management, and the event system, leaving as little as possible required for renderers to implement themselves. -For reference, check out the javascript interperter or tui renderer as a starting point for your custom renderer. +For reference, check out the [javascript interpreter](https://github.com/DioxusLabs/dioxus/tree/master/packages/interpreter) or [tui renderer](https://github.com/DioxusLabs/dioxus/tree/master/packages/tui) as a starting point for your custom renderer. -## DomEdits +## Templates -The "DomEdit" type is a serialized enum that represents an atomic operation occurring on the RealDom. The variants roughly follow this set: +Dioxus is built around the concept of [Templates](https://docs.rs/dioxus-core/latest/dioxus_core/prelude/struct.Template.html). Templates describe a UI tree known at compile time with dynamic parts filled at runtime. This is useful internally to make skip diffing static nodes, but it is also useful for the renderer to reuse parts of the UI tree. This can be useful for things like a list of items. Each item could contain some static parts and some dynamic parts. The renderer can use the template to create a static part of the UI once, clone it for each element in the list, and then fill in the dynamic parts. + +## Mutations + +The `Mutation` type is a serialized enum that represents an operation that should be applied to update the UI. The variants roughly follow this set: ```rust -enum DomEdit { - PushRoot, +enum Mutation { AppendChildren, + AssignId, + CreatePlaceholder, + CreateTextNode, + HydrateText, + LoadTemplate, ReplaceWith, + ReplacePlaceholder, InsertAfter, InsertBefore, - Remove, - CreateTextNode, - CreateElement, - CreateElementNs, - CreatePlaceholder, + SetAttribute, + SetText, NewEventListener, RemoveEventListener, - SetText, - SetAttribute, - RemoveAttribute, - PopRoot, + Remove, + PushRoot, } ``` The Dioxus diffing mechanism operates as a [stack machine](https://en.wikipedia.org/wiki/Stack_machine) where the "push_root" method pushes a new "real" DOM node onto the stack and "append_child" and "replace_with" both remove nodes from the stack. -### An example +### An Example -For the sake of understanding, lets consider this example – a very simple UI declaration: +For the sake of understanding, let's consider this example – a very simple UI declaration: ```rust -rsx!( h1 {"hello world"} ) +rsx!( h1 {"count {x}"} ) ``` To get things started, Dioxus must first navigate to the container of this h1 tag. To "navigate" here, the internal diffing algorithm generates the DomEdit `PushRoot` where the ID of the root is the container. @@ -66,7 +70,7 @@ stack: [ ] ``` -Next, Dioxus will encounter the h1 node. The diff algorithm decides that this node needs to be created, so Dioxus will generate the DomEdit `CreateElement`. When the renderer receives this instruction, it will create an unmounted node and push into its own stack: +Next, Dioxus will encounter the h1 node. The diff algorithm decides that this node needs to be created, so Dioxus will generate the DomEdit `CreateElement`. When the renderer receives this instruction, it will create an unmounted node and push it into its own stack: ```rust instructions: [ @@ -91,7 +95,7 @@ stack: [ "hello world" ] ``` -Remember, the text node is not attached to anything (it is unmounted) so Dioxus needs to generate an Edit that connects the text node to the h1 element. It depends on the situation, but in this case we use `AppendChildren`. This pops the text node off the stack, leaving the h1 element as the next element in line. +Remember, the text node is not attached to anything (it is unmounted) so Dioxus needs to generate an Edit that connects the text node to the h1 element. It depends on the situation, but in this case, we use `AppendChildren`. This pops the text node off the stack, leaving the h1 element as the next element in line. ```rust instructions: [ @@ -141,13 +145,13 @@ Over time, our stack looked like this: [] ``` -Notice how our stack is empty once UI has been mounted. Conveniently, this approach completely separates the VirtualDOM and the Real DOM. Additionally, these edits are serializable, meaning we can even manage UIs across a network connection. This little stack machine and serialized edits makes Dioxus independent of platform specifics. +Notice how our stack is empty once UI has been mounted. Conveniently, this approach completely separates the Virtual DOM and the Real DOM. Additionally, these edits are serializable, meaning we can even manage UIs across a network connection. This little stack machine and serialized edits make Dioxus independent of platform specifics. Dioxus is also really fast. Because Dioxus splits the diff and patch phase, it's able to make all the edits to the RealDOM in a very short amount of time (less than a single frame) making rendering very snappy. It also allows Dioxus to cancel large diffing operations if higher priority work comes in while it's diffing. It's important to note that there _is_ one layer of connectedness between Dioxus and the renderer. Dioxus saves and loads elements (the PushRoot edit) with an ID. Inside the VirtualDOM, this is just tracked as a u64. -Whenever a `CreateElement` edit is generated during diffing, Dioxus increments its node counter and assigns that new element its current NodeCount. The RealDom is responsible for remembering this ID and pushing the correct node when PushRoot(ID) is generated. Dioxus reclaims IDs of elements when removed. To stay in sync with Dioxus you can use a sparce Vec (Vec>) with possibly unoccupied items. You can use the ids as indexes into the Vec for elements, and grow the Vec when a id does not exist. +Whenever a `CreateElement` edit is generated during diffing, Dioxus increments its node counter and assigns that new element its current NodeCount. The RealDom is responsible for remembering this ID and pushing the correct node when PushRoot(ID) is generated. Dioxus reclaims the IDs of elements when removed. To stay in sync with Dioxus you can use a sparse Vec (Vec>) with possibly unoccupied items. You can use the ids as indexes into the Vec for elements, and grow the Vec when an id does not exist. This little demo serves to show exactly how a Renderer would need to process an edit stream to build UIs. A set of serialized DomEditss for various demos is available for you to test your custom renderer against. @@ -243,16 +247,16 @@ You've probably noticed that many elements in the `rsx!` macros support on-hover # Native Core -If you are creating a renderer in rust, native core provides some utilites to implement a renderer. It provides an abstraction over DomEdits and handles layout for you. +If you are creating a renderer in rust, native-core provides some utilities to implement a renderer. It provides an abstraction over DomEdits and handles the layout for you. ## RealDom -The `RealDom` is a higher level abstraction over updating the Dom. It updates with `DomEdits` and provides a way to incrementally update the state of nodes based on what attributes change. +The `RealDom` is a higher-level abstraction over updating the Dom. It updates with `DomEdits` and provides a way to incrementally update the state of nodes based on what attributes change. ### Example Let's build a toy renderer with borders, size, and text color. -Before we start lets take a look at an example element we can render: +Before we start let's take a look at an example element we can render: ```rust cx.render(rsx!{ div{ @@ -265,7 +269,7 @@ cx.render(rsx!{ }) ``` -In this tree the color depends on the parent's color. The size depends on the childrens size, the current text, and a text size. The border depends on only the current node. +In this tree, the color depends on the parent's color. The size depends on the children's size, the current text, and the text size. The border depends on only the current node. In the following diagram arrows represent dataflow: @@ -312,7 +316,7 @@ In the following diagram arrows represent dataflow: [//]: # " end" [//]: # " end" -To help in building a Dom, native core provides four traits: State, ChildDepState, ParentDepState, and NodeDepState and a RealDom struct. The ChildDepState, ParentDepState, and NodeDepState provide a way to describe how some information in a node relates to that of its relatives. By providing how to build a single node from its relations, native-core will derive a way to update the state of all nodes for you with ```#[derive(State)]```. Once you have a state you can provide it as a generic to RealDom. RealDom provides all of the methods to interact and update your new dom. +To help in building a Dom, native-core provides four traits: State, ChildDepState, ParentDepState, NodeDepState, and a RealDom struct. The ChildDepState, ParentDepState, and NodeDepState provide a way to describe how some information in a node relates to that of its relatives. By providing how to build a single node from its relations, native-core will derive a way to update the state of all nodes for you with ```#[derive(State)]```. Once you have a state you can provide it as a generic to RealDom. RealDom provides all of the methods to interact and update your new dom. ```rust use dioxus_native_core::node_ref::*; @@ -447,7 +451,7 @@ struct ToyState { } ``` -Now that we have our state, we can put it to use in our dom. Re can update the dom with update_state to update the structure of the dom (adding, removing, and chaning properties of nodes) and then apply_mutations to update the ToyState for each of the nodes that changed. +Now that we have our state, we can put it to use in our dom. Re can update the dom with update_state to update the structure of the dom (adding, removing, and changing properties of nodes) and then apply_mutations to update the ToyState for each of the nodes that changed. ```rust fn main(){ fn app(cx: Scope) -> Element { @@ -490,7 +494,7 @@ fn main(){ ``` ## Layout -For most platforms the layout of the Elements will stay the same. The layout_attributes module provides a way to apply html attributes to a stretch layout style. +For most platforms, the layout of the Elements will stay the same. The layout_attributes module provides a way to apply HTML attributes to a stretch layout style. ## Conclusion -That should be it! You should have nearly all the knowledge required on how to implement your own renderer. We're super interested in seeing Dioxus apps brought to custom desktop renderers, mobile renderer, video game UI, and even augmented reality! If you're interesting in contributing to any of the these projects, don't be afraid to reach out or join the community. +That should be it! You should have nearly all the knowledge required on how to implement your own renderer. We're super interested in seeing Dioxus apps brought to custom desktop renderers, mobile renderers, video game UI, and even augmented reality! If you're interested in contributing to any of these projects, don't be afraid to reach out or join the [community](https://discord.gg/XgGxMSkvUM). diff --git a/docs/guide/src/en/describing_ui/component_props.md b/docs/guide/src/en/describing_ui/component_props.md index 4abf10ba0..9c5cd4be3 100644 --- a/docs/guide/src/en/describing_ui/component_props.md +++ b/docs/guide/src/en/describing_ui/component_props.md @@ -9,7 +9,7 @@ Component props are a single struct annotated with `#[derive(Props)]`. For a com There are 2 flavors of Props structs: - Owned props: - Don't have an associated lifetime - - Implement `PartialEq`, allowing for memoization (if the props don't change, Dioxus won't re-render the component) + - Implement `PartialEq`, allow for memoization (if the props don't change, Dioxus won't re-render the component) - Borrowed props: - [Borrow](https://doc.rust-lang.org/beta/rust-by-example/scope/borrow.html) from a parent component - Cannot be memoized due to lifetime constraints @@ -32,7 +32,7 @@ You can then pass prop values to the component the same way you would pass attri ### Borrowed Props -Owning props works well if your props are easy to copy around – like a single number. But what if we need to pass a larger data type, like a String from an `App` Component to a `TitleCard` subcomponent? A naive solution might be to [`.clone()`](https://doc.rust-lang.org/std/clone/trait.Clone.html) the String, creating a copy of it for the subcomponent – but this would be inefficient, especially for larger Strings. +Owned props work well if your props are easy to copy around – like a single number. But what if we need to pass a larger data type, like a String from an `App` Component to a `TitleCard` subcomponent? A naive solution might be to [`.clone()`](https://doc.rust-lang.org/std/clone/trait.Clone.html) the String, creating a copy of it for the subcomponent – but this would be inefficient, especially for larger Strings. Rust allows for something more efficient – borrowing the String as a `&str` – this is what Borrowed Props are for! @@ -47,6 +47,7 @@ We can then use the component like this: ``` ![Screenshot: TitleCard component](./images/component_borrowed_props_screenshot.png) +Borrowed props can be very useful, but they do not allow for memorization so they will *always* rerun when the parent scope is rerendered. Because of this Borrowed Props should be reserved for components that are cheap to rerun or places where cloning data is an issue. Using Borrowed Props everywhere will result in large parts of your app rerunning every interaction. ## Prop Options diff --git a/docs/guide/src/en/describing_ui/components.md b/docs/guide/src/en/describing_ui/components.md index 78eb8ff10..9da0d6605 100644 --- a/docs/guide/src/en/describing_ui/components.md +++ b/docs/guide/src/en/describing_ui/components.md @@ -1,6 +1,6 @@ # Components -Just like you wouldn't want to write a complex program in a single, long, `main` function, you shouldn't build a complex UI in a single `App` function. Instead, it would be better to break down the functionality of an app in logical parts called components. +Just like you wouldn't want to write a complex program in a single, long, `main` function, you shouldn't build a complex UI in a single `App` function. Instead, you should break down the functionality of an app in logical parts called components. A component is a Rust function, named in UpperCammelCase, that takes a `Scope` parameter and returns an `Element` describing the UI it wants to render. In fact, our `App` function is a component! @@ -8,7 +8,7 @@ A component is a Rust function, named in UpperCammelCase, that takes a `Scope` p {{#include ../../../examples/hello_world_desktop.rs:component}} ``` -> You'll probably want to add `#![allow(non_snake_case)]` to the top of your crate to avoid warnings about the function name +> You'll probably want to add `#![allow(non_snake_case)]` to the top of your crate to avoid warnings about UpperCammelCase component names A Component is responsible for some rendering task – typically, rendering an isolated part of the user interface. For example, you could have an `About` component that renders a short description of Dioxus Labs: diff --git a/docs/guide/src/en/describing_ui/index.md b/docs/guide/src/en/describing_ui/index.md index 928b4696a..808e6af3c 100644 --- a/docs/guide/src/en/describing_ui/index.md +++ b/docs/guide/src/en/describing_ui/index.md @@ -1,8 +1,8 @@ # Describing the UI -Dioxus is a *declarative* framework. This means that instead of telling Dioxus what to do (e.g. to "create an element" or "set color to red") we simply *declare* what we want the UI to look like using RSX. +Dioxus is a *declarative* framework. This means that instead of telling Dioxus what to do (e.g. to "create an element" or "set the color to red") we simply *declare* what we want the UI to look like using RSX. -You have already seen a simple example or RSX syntax in the "hello world" application: +You have already seen a simple example of RSX syntax in the "hello world" application: ```rust {{#include ../../../examples/hello_world_desktop.rs:component}} @@ -21,9 +21,51 @@ RSX is very similar to HTML in that it describes elements with attributes and ch
``` + +### Attributes + +Attributes (and [listeners](../interactivity/index.md)) modify the behavior or appearance of the element they are attached to. They are specified inside the `{}` brackets, using the `name: value` syntax. You can provide the value as a literal in the RSX: +```rust +{{#include ../../../examples/rsx_overview.rs:attributes}} +``` +```html + +``` + +> Note: All attributes defined in `dioxus-html` follow the snake_case naming convention. They transform their `snake_case` names to HTML's `camelCase` attributes. + +> Note: Styles can be used directly outside of the `style:` attribute. In the above example, `color: "red"` is turned into `style="color: red"`. + +#### Custom Attributes + +Dioxus has a pre-configured set of attributes that you can use. RSX is validated at compile time to make sure you didn't specify an invalid attribute. If you want to override this behavior with a custom attribute name, specify the attribute in quotes: + +```rust +{{#include ../../../examples/rsx_overview.rs:custom_attributes}} +``` +```html + + +``` + +### Interpolation + +Similarly to how you can [format](https://doc.rust-lang.org/rust-by-example/hello/print/fmt.html) Rust strings, you can also interpolate in RSX text. Use `{variable}` to Display the value of a variable in a string, or `{variable:?}` to use the Debug representation: + +```rust +{{#include ../../../examples/rsx_overview.rs:formatting}} +``` +```html +
+
ES
+
42
+
{}
+
+``` + ### Children -To add children to an element, put them inside the `{}` brackets. They can be either other elements, or text. For example, you could have an `ol` (ordered list) element, containing 3 `li` (list item) elements, each of which contains some text: +To add children to an element, put them inside the `{}` brackets after all attributes and listeners in the element. They can be other elements, text, or [components](components.md). For example, you could have an `ol` (ordered list) element, containing 3 `li` (list item) elements, each of which contains some text: ```rust {{#include ../../../examples/rsx_overview.rs:children}} @@ -38,69 +80,51 @@ To add children to an element, put them inside the `{}` brackets. They can be ei ### Fragments -You can also "group" elements by wrapping them in `Fragment {}`. This will not create any additional elements. - -> Note: you can also render multiple elements at the top level of `rsx!` and they will be automatically grouped – no need for an explicit `Fragment {}` there. +You can render multiple elements at the top level of `rsx!` and they will be automatically grouped. ```rust -{{#include ../../../examples/rsx_overview.rs:fragments}} +{{#include ../../../examples/rsx_overview.rs:manyroots}} ``` ```html

First Item

Second Item

-a group -of three -items -``` - -### Attributes - -Attributes are also specified inside the `{}` brackets, using the `name: value` syntax. You can provide the value as a literal in the RSX: -```rust -{{#include ../../../examples/rsx_overview.rs:attributes}} -``` -```html -Log In -``` - -> Note: All attributes defined in `dioxus-html` follow the snake_case naming convention. They transform their `snake_case` names to HTML's `camelCase` attributes. - -#### Custom Attributes - -Dioxus has a pre-configured set of attributes that you can use. RSX is validated at compile time to make sure you didn't specify an invalid attribute. If you want to override this behavior with a custom attribute name, specify the attribute in quotes: - -```rust -{{#include ../../../examples/rsx_overview.rs:custom_attributes}} -``` -```html - - Rust is cool - -``` - -### Interpolation - -Similarly to how you can [format](https://doc.rust-lang.org/rust-by-example/hello/print/fmt.html) Rust strings, you can also interpolate in RSX text. Use `{variable}` to Display the value of a variable in a string, or `{variable:?}` to use the Debug representation: - -```rust -{{#include ../../../examples/rsx_overview.rs:formatting}} -``` -```html - -
Coordinates: (42, 0) -
ES
-
42
-
``` ### Expressions -You can include arbitrary Rust expressions within RSX, but you must escape them in `[]` brackets: +You can include arbitrary Rust expressions as children within RSX that implements [IntoDynNode](https://docs.rs/dioxus-core/0.3/dioxus_core/trait.IntoDynNode.html). This is useful for displaying data from an [iterator](https://doc.rust-lang.org/stable/book/ch13-02-iterators.html#processing-a-series-of-items-with-iterators): ```rust {{#include ../../../examples/rsx_overview.rs:expression}} ``` ```html -DIOXUS +DIOXUS0123456789 +``` + +### Loops + +In addition to iterators you can also use for loops directly within RSX: + +```rust +{{#include ../../../examples/rsx_overview.rs:loops}} +``` +```html +
0
+
1
+
2
+
0
+
1
+
2
+``` + +### If statements + +You can also use if statements without an else branch within RSX: + +```rust +{{#include ../../../examples/rsx_overview.rs:ifstatements}} +``` +```html +
true
``` \ No newline at end of file diff --git a/docs/guide/src/en/getting_started/desktop.md b/docs/guide/src/en/getting_started/desktop.md index 0a963ff61..c86d7760e 100644 --- a/docs/guide/src/en/getting_started/desktop.md +++ b/docs/guide/src/en/getting_started/desktop.md @@ -1,21 +1,58 @@ -# Desktop Application +# Desktop Overview Build a standalone native desktop app that looks and feels the same across operating systems. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. Examples: -- [File explorer](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) -- [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) +- [File Explorer](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) +- [WiFi Scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) [![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer) ## Support -The desktop is a powerful target for Dioxus, but is currently limited in capability when compared to the Web platform. Currently, desktop apps are rendered with the platform's WebView library, but your Rust code is running natively on a native thread. This means that browser APIs are *not* available, so rendering WebGL, Canvas, etc is not as easy as the Web. However, native system APIs *are* accessible, so streaming, WebSockets, filesystem, etc are all viable APIs. In the future, we plan to move to a custom webrenderer-based DOM renderer with WGPU integrations. +The desktop is a powerful target for Dioxus but is currently limited in capability when compared to the Web platform. Currently, desktop apps are rendered with the platform's WebView library, but your Rust code is running natively on a native thread. This means that browser APIs are *not* available, so rendering WebGL, Canvas, etc is not as easy as the Web. However, native system APIs *are* accessible, so streaming, WebSockets, filesystem, etc are all viable APIs. In the future, we plan to move to a custom web renderer-based DOM renderer with WGPU integrations. Dioxus Desktop is built off [Tauri](https://tauri.app/). Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri – mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. +# Getting started + +## Platform-Specific Dependencies +Dioxus desktop renders through a web view. Depending on your platform, you might need to install some dependancies. + +### Windows + +Windows Desktop apps depend on WebView2 – a library that should be installed in all modern Windows distributions. If you have Edge installed, then Dioxus will work fine. If you *don't* have Webview2, [then you can install it through Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). MS provides 3 options: + +1. A tiny "evergreen" *bootstrapper* that fetches an installer from Microsoft's CDN +2. A tiny *installer* that fetches Webview2 from Microsoft's CDN +3. A statically linked version of Webview2 in your final binary for offline users + +For development purposes, use Option 1. + +### Linux + +Webview Linux apps require WebkitGtk. When distributing, this can be part of your dependency tree in your `.rpm` or `.deb`. However, likely, your users will already have WebkitGtk. + +```bash +sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev +``` + +When using Debian/bullseye `libappindicator3-dev` is no longer available but replaced by `libayatana-appindicator3-dev`. + +```bash +# on Debian/bullseye use: +sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev +``` + +If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/v1/guides/getting-started/prerequisites#setting-up-linux). + + +### MacOS + +Currently – everything for macOS is built right in! However, you might run into an issue if you're using nightly Rust due to some permissions issues in our Tao dependency (which have been resolved but not published). + ## Creating a Project Create a new crate: @@ -25,7 +62,7 @@ cargo new --bin demo cd demo ``` -Add Dioxus and the `desktop` renderer (this will edit `Cargo.toml`): +Add Dioxus and the desktop renderer as dependencies (this will edit your `Cargo.toml`): ```shell cargo add dioxus diff --git a/docs/guide/src/en/getting_started/hot_reload.md b/docs/guide/src/en/getting_started/hot_reload.md index 7434fdb9c..823baa179 100644 --- a/docs/guide/src/en/getting_started/hot_reload.md +++ b/docs/guide/src/en/getting_started/hot_reload.md @@ -1,15 +1,12 @@ # Setting Up Hot Reload -1. Hot reloading allows much faster iteration times inside of rsx calls by interperting them and streaming the edits. +1. Hot reloading allows much faster iteration times inside of rsx calls by interpreting them and streaming the edits. 2. It is useful when changing the styling/layout of a program, but will not help with changing the logic of a program. 3. Currently the cli only implements hot reloading for the web renderer. # Setup Install [dioxus-cli](https://github.com/DioxusLabs/cli). -Enable the hot-reload feature on dioxus: -```toml -dioxus = { version = "*", features = ["hot-reload"] } -``` +Hot reloading is automatically enabled when using the web renderer on debug builds. # Usage 1. run: @@ -21,5 +18,5 @@ dioxus serve --hot-reload 4. save and watch the style change without recompiling # Limitations -1. The interperter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will trigger a full recompile to capture the expression. -2. Components and Iterators can contain abritary rust code, and will trigger a full recompile when changed. \ No newline at end of file +1. The interpreter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will trigger a full recompile to capture the expression. +2. Components and Iterators can contain arbitrary rust code and will trigger a full recompile when changed. \ No newline at end of file diff --git a/docs/guide/src/en/getting_started/index.md b/docs/guide/src/en/getting_started/index.md index 6b4937453..fbf0b2d05 100644 --- a/docs/guide/src/en/getting_started/index.md +++ b/docs/guide/src/en/getting_started/index.md @@ -12,63 +12,23 @@ Dioxus integrates very well with the [Rust-Analyzer LSP plugin](https://rust-ana Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler. -We strongly recommend going through the [official Rust book](https://doc.rust-lang.org/book/ch01-00-getting-started.html) _completely_. However, our hope is that a Dioxus app can serve as a great first Rust project. With Dioxus, you'll learn about: +We strongly recommend going through the [official Rust book](https://doc.rust-lang.org/book/ch01-00-getting-started.html) _completely_. However, we hope that a Dioxus app can serve as a great first Rust project. With Dioxus, you'll learn about: - Error handling - Structs, Functions, Enums - Closures - Macros -We've put a lot of care into making Dioxus syntax familiar and easy to understand, so you won't need deep knowledge on async, lifetimes, or smart pointers until you really start building complex Dioxus apps. - -### Platform-Specific Dependencies - -#### Windows - -Windows Desktop apps depend on WebView2 – a library which should be installed in all modern Windows distributions. If you have Edge installed, then Dioxus will work fine. If you *don't* have Webview2, [then you can install it through Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). MS provides 3 options: - -1. A tiny "evergreen" *bootstrapper* which will fetch an installer from Microsoft's CDN -2. A tiny *installer* which will fetch Webview2 from Microsoft's CDN -3. A statically linked version of Webview2 in your final binary for offline users - -For development purposes, use Option 1. - -#### Linux - -Webview Linux apps require WebkitGtk. When distributing, this can be part of your dependency tree in your `.rpm` or `.deb`. However, it's very likely that your users will already have WebkitGtk. - -```bash -sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev -``` - -When using Debian/bullseye `libappindicator3-dev` is no longer available but replaced by `libayatana-appindicator3-dev`. - -```bash -# on Debian/bullseye use: -sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev -``` - -If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/v1/guides/getting-started/prerequisites#setting-up-linux). - - -#### MacOS - -Currently – everything for macOS is built right in! However, you might run into an issue if you're using nightly Rust due to some permissions issues in our Tao dependency (which have been resolved but not published). - -### Suggested Cargo Extensions - -If you want to keep your traditional `npm install XXX` workflow for adding packages, you might want to install `cargo-edit` and a few other fun `cargo` extensions: - -- [cargo-expand](https://github.com/dtolnay/cargo-expand) for expanding macro calls -- [cargo tree](https://doc.rust-lang.org/cargo/commands/cargo-tree.html) – an integrated cargo command that lets you inspect your dependency tree +We've put a lot of care into making Dioxus syntax familiar and easy to understand, so you won't need deep knowledge of async, lifetimes, or smart pointers until you start building complex Dioxus apps. ## Setup Guides -Dioxus supports multiple platforms. Depending on what you want, the setup is a bit different. +Dioxus supports multiple platforms. Choose the platform you want to target below to get platform-specific setup instructions: -- [Web](web.md): running in the browser using WASM -- [Server Side Rendering](ssr.md): render Dioxus HTML as text -- [Desktop](desktop.md): a standalone app using webview -- [Mobile](mobile.md) -- [Terminal UI](tui.md): terminal text-based graphic interface \ No newline at end of file +- [Web](web.md): runs in the browser through WebAssembly +- [Server Side Rendering](ssr.md): renders to HTML text on the server +- [Liveview](liveview.md): runs on the server, renders in the browser using WebSockets +- [Desktop](desktop.md): runs in a web view on desktop +- [Mobile](mobile.md): runs in a web view on mobile +- [Terminal UI](tui.md): renders text-based graphics in the terminal \ No newline at end of file diff --git a/docs/guide/src/en/getting_started/liveview.md b/docs/guide/src/en/getting_started/liveview.md new file mode 100644 index 000000000..54f792e74 --- /dev/null +++ b/docs/guide/src/en/getting_started/liveview.md @@ -0,0 +1,66 @@ +# Liveview + +Liveview allows apps to *run* on the server and *render* in the browser. It uses WebSockets to communicate between the server and the browser. + +Examples: +- [Axum Example](https://github.com/DioxusLabs/dioxus/tree/master/packages/liveview/examples/axum.rs) +- [Salvo Example](https://github.com/DioxusLabs/dioxus/tree/master/packages/liveview/examples/salvo.rs) +- [Warp Example](https://github.com/DioxusLabs/dioxus/tree/master/packages/liveview/examples/warp.rs) + + +## Support + +Liveview is currently limited in capability when compared to the Web platform. Liveview apps run on the server in a native thread. This means that browser APIs are not available, so rendering WebGL, Canvas, etc is not as easy as the Web. However, native system APIs are accessible, so streaming, WebSockets, filesystem, etc are all viable APIs. + + +## Setup + +For this guide, we're going to show how to use Dioxus Liveview with [Axum](https://docs.rs/axum/latest/axum/). + +Make sure you have Rust and Cargo installed, and then create a new project: + +```shell +cargo new --bin demo +cd app +``` + +Add Dioxus and the liveview renderer with the Axum feature as dependencies: + +```shell +cargo add dioxus +cargo add dioxus-liveview --features axum +``` + +Next, add all the Axum dependencies. This will be different if you're using a different Web Framework + +``` +cargo add tokio --features full +cargo add axum +``` + +Your dependencies should look roughly like this: + +```toml +[dependencies] +axum = "0.4.5" +dioxus = { version = "*" } +dioxus-liveview = { version = "*", features = ["axum"] } +tokio = { version = "1.15.0", features = ["full"] } +``` + +Now, set up your Axum app to respond on an endpoint. + + +```rust +{{#include ../../../examples/hello_world_liveview.rs:glue}} +``` + + +And then add our app component: + +```rust +{{#include ../../../examples/hello_world_liveview.rs:app}} +``` + +And that's it! + diff --git a/docs/guide/src/en/getting_started/mobile.md b/docs/guide/src/en/getting_started/mobile.md index f8dd5f4ff..2f93b807c 100644 --- a/docs/guide/src/en/getting_started/mobile.md +++ b/docs/guide/src/en/getting_started/mobile.md @@ -5,12 +5,12 @@ Build a mobile app with Dioxus! Example: [Todo App](https://github.com/DioxusLabs/example-projects/blob/master/ios_demo) ## Support -Mobile is currently the least-supported renderer target for Dioxus. Mobile apps are rendered with either the platform's WebView or experimentally through WGPU. WebView doesn't support animations, transparency, and native widgets. +Mobile is currently the least-supported renderer target for Dioxus. Mobile apps are rendered with either the platform's WebView or experimentally through [WGPU](https://github.com/DioxusLabs/blitz). WebView doesn't support animations, transparency, and native widgets. Mobile support is currently best suited for CRUD-style apps, ideally for internal teams who need to develop quickly but don't care much about animations or native widgets. -This guide is primarily targetted for iOS apps, however, you can follow it while using the `android` guide in `cargo-mobile`. +This guide is primarily targeted at iOS apps, however, you can follow it while using the `android` guide in `cargo-mobile`. ## Getting Set up @@ -35,7 +35,7 @@ We're going to completely clear out the `dependencies` it generates for us, swap [package] name = "dioxus-ios-demo" version = "0.1.0" -authors = ["Jonathan Kelley "] +authors = [] edition = "2018" diff --git a/docs/guide/src/en/getting_started/ssr.md b/docs/guide/src/en/getting_started/ssr.md index 7e28cd304..9654003cf 100644 --- a/docs/guide/src/en/getting_started/ssr.md +++ b/docs/guide/src/en/getting_started/ssr.md @@ -15,19 +15,7 @@ When working with web frameworks that require `Send`, it is possible to render a ## Setup -If you just want to render `rsx!` or a VirtualDom to HTML, check out the API docs. It's pretty simple: - -```rust -// We can render VirtualDoms -let mut vdom = VirtualDom::new(app); -let _ = vdom.rebuild(); -println!("{}", dioxus_ssr::render_vdom(&vdom)); - -// Or we can render rsx! calls directly -println!( "{}", dioxus_ssr::render_lazy(rsx! { h1 { "Hello, world!" } } ); -``` - -However, for this guide, we're going to show how to use Dioxus SSR with `Axum`. +For this guide, we're going to show how to use Dioxus SSR with [Axum](https://docs.rs/axum/latest/axum/). Make sure you have Rust and Cargo installed, and then create a new project: @@ -36,7 +24,7 @@ cargo new --bin demo cd app ``` -Add Dioxus and the `ssr` renderer feature: +Add Dioxus and the ssr renderer as dependencies: ```shell cargo add dioxus @@ -86,8 +74,9 @@ And then add our endpoint. We can either render `rsx!` directly: ```rust async fn app_endpoint() -> Html { + // render the rsx! macro to HTML Html(dioxus_ssr::render_lazy(rsx! { - h1 { "hello world!" } + div { "hello world!" } })) } ``` @@ -96,12 +85,16 @@ Or we can render VirtualDoms. ```rust async fn app_endpoint() -> Html { + // create a component that renders a div with the text "hello world" fn app(cx: Scope) -> Element { - cx.render(rsx!(h1 { "hello world" })) + cx.render(rsx!(div { "hello world" })) } + // create a VirtualDom with the app component let mut app = VirtualDom::new(app); + // rebuild the VirtualDom before rendering let _ = app.rebuild(); + // render the VirtualDom to HTML Html(dioxus_ssr::render_vdom(&app)) } ``` diff --git a/docs/guide/src/en/getting_started/tui.md b/docs/guide/src/en/getting_started/tui.md index 980af5706..c776e24d9 100644 --- a/docs/guide/src/en/getting_started/tui.md +++ b/docs/guide/src/en/getting_started/tui.md @@ -8,12 +8,19 @@ You can build a text-based interface that will run in the terminal using Dioxus. ## Support -TUI support is currently quite experimental. Even the project name will change. But, if you're willing to venture into the realm of the unknown, this guide will get you started. +TUI support is currently quite experimental. But, if you're willing to venture into the realm of the unknown, this guide will get you started. + +- It uses flexbox for the layout +- It only supports a subset of the attributes and elements +- Regular widgets will not work in the tui render, but the tui renderer has its own widget components that start with a capital letter. See the [widgets example](https://github.com/DioxusLabs/dioxus/blob/master/packages/tui/examples/tui_widgets.rs) +- 1px is one character line height. Your regular CSS px does not translate +- If your app panics, your terminal is wrecked. This will be fixed eventually + ## Getting Set up -Start by making a new package and adding our TUI renderer. +Start by making a new package and adding Dioxus and the TUI renderer as dependancies. ```shell cargo new --bin demo @@ -34,15 +41,8 @@ To run our app: cargo run ``` -Press "ctrl-c" to close the app. To switch from "ctrl-c" to just "q" to quit you can launch the app with a configuration to disable the default quit and use the root TuiContext to quit on your own. +Press "ctrl-c" to close the app. To switch from "ctrl-c" to just "q" to quit you can launch the app with a configuration to disable the default quit and use the root TuiContext to quit on your own. ```rust {{#include ../../../examples/hello_world_tui_no_ctrl_c.rs}} ``` - -## Notes - -- Our TUI package uses flexbox for layout -- Regular widgets will not work in the tui render, but the tui renderer has it's own widget components (see [the widgets example](https://github.com/DioxusLabs/dioxus/blob/master/packages/tui/examples/tui_widgets.rs)). -- 1px is one character lineheight. Your regular CSS px does not translate. -- If your app panics, your terminal is wrecked. This will be fixed eventually. diff --git a/docs/guide/src/en/getting_started/web.md b/docs/guide/src/en/getting_started/web.md index 311cf277a..76e3b9541 100644 --- a/docs/guide/src/en/getting_started/web.md +++ b/docs/guide/src/en/getting_started/web.md @@ -1,8 +1,8 @@ # Web -Build single-page applications that run in the browser with Dioxus. To run on the Web, your app must be compiled to WebAssembly and depend on the `dioxus` crate with the `web` feature enabled. +Build single-page applications that run in the browser with Dioxus. To run on the Web, your app must be compiled to WebAssembly and depend on the `dioxus` and `dioxus-web` crates. -A build of Dioxus for the web will be roughly equivalent to the size of a React build (70kb vs 65kb), but will load significantly faster due to [WebAssembly's StreamingCompile](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/) option. +A build of Dioxus for the web will be roughly equivalent to the size of a React build (70kb vs 65kb) but it will load significantly faster because [WebAssembly can be compiled as it is streamed](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/). Examples: - [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc) @@ -10,21 +10,24 @@ Examples: [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) -> Note: Because of the limitations of Wasm, not every crate will work with your web apps, so you'll need to make sure that your crates work without native system calls (timers, IO, etc). +> Note: Because of the limitations of Wasm, [not every crate will work](https://rustwasm.github.io/docs/book/reference/which-crates-work-with-wasm.html) with your web apps, so you'll need to make sure that your crates work without native system calls (timers, IO, etc). ## Support The Web is the best-supported target platform for Dioxus. +- Because your app will be compiled to WASM you have access to browser APIs through [wasm-bingen](https://rustwasm.github.io/docs/wasm-bindgen/introduction.html). +- Dioxus provides hydration to resume apps that are rendered on the server. See the [hydration example](https://github.com/DioxusLabs/dioxus/blob/master/packages/web/examples/hydrate.rs) for more details. + ## Tooling -To develop your Dioxus app for the web, you'll need a tool to build and serve your assets. We recommend using [trunk](https://trunkrs.dev) which includes a build system, Wasm optimization, a dev server, and support for SASS/CSS: +To develop your Dioxus app for the web, you'll need a tool to build and serve your assets. We recommend using [dioxus-cli](https://github.com/DioxusLabs/cli) which includes a build system, Wasm optimization, a dev server, and support hot reloading: ```shell -cargo install trunk +cargo install dioxus-cli ``` -Make sure the `wasm32-unknown-unknown` target is installed: +Make sure the `wasm32-unknown-unknown` target for rust is installed: ```shell rustup target add wasm32-unknown-unknown ``` @@ -38,28 +41,13 @@ cargo new --bin demo cd demo ``` -Add Dioxus as a dependency and add the web renderer: +Add Dioxus and the web renderer as dependencies (this will edit your `Cargo.toml`): ```bash cargo add dioxus cargo add dioxus-web ``` -Add an `index.html` for Trunk to use. Make sure your "mount point" element has an ID of "main": - -```html - - - - - - - -
- - -``` - Edit your `main.rs`: ```rust {{#include ../../../examples/hello_world_web.rs}} @@ -69,5 +57,5 @@ Edit your `main.rs`: And to serve our app: ```bash -trunk serve +dioxus serve ``` diff --git a/docs/guide/src/en/index.md b/docs/guide/src/en/index.md index 770be440e..332d5957b 100644 --- a/docs/guide/src/en/index.md +++ b/docs/guide/src/en/index.md @@ -27,7 +27,7 @@ Dioxus is heavily inspired by React. If you know React, getting started with Dio - Comprehensive inline documentation – hover and guides for all HTML elements, listeners, and events. - Extremely memory efficient – 0 global allocations for steady-state components. - Multi-channel asynchronous scheduler for first-class async support. -- And more! Read the [full release post](https://dioxuslabs.com/blog/introducing-dioxus/. +- And more! Read the [full release post](https://dioxuslabs.com/blog/introducing-dioxus/). ### Multiplatform diff --git a/docs/guide/src/en/interactivity/dynamic_rendering.md b/docs/guide/src/en/interactivity/dynamic_rendering.md index 591868958..290871e9c 100644 --- a/docs/guide/src/en/interactivity/dynamic_rendering.md +++ b/docs/guide/src/en/interactivity/dynamic_rendering.md @@ -12,10 +12,19 @@ To render different elements based on a condition, you could use an `if-else` st > You could also use `match` statements, or any Rust function to conditionally render different things. +### Improving the `if-else` Example + +You may have noticed some repeated code in the `if-else` example above. Repeating code like this is both bad for maintainability and performance. Dioxus will skip diffing static elements like the button, but when switching between multiple `rsx` calls it cannot perform this optimization. For this example either approach is fine, but for components with large parts that are reused between conditionals, it can be more of an issue. + +We can improve this example by splitting up the dynamic parts and inserting them where they are needed. + +```rust +{{#include ../../../examples/conditional_rendering.rs:if_else_improved}} +``` ### Inspecting `Element` props -Since `Element` is a `Option`, components accepting `Element` as a prop can actually inspect its contents, and render different things based on that. Example: +Since `Element` is a `Option`, components accepting `Element` as a prop can inspect its contents, and render different things based on that. Example: ```rust {{#include ../../../examples/component_children_inspect.rs:Clickable}} @@ -43,9 +52,9 @@ Often, you'll want to render a collection of components. For example, you might For this, Dioxus accepts iterators that produce `Element`s. So we need to: - Get an iterator over all of our items (e.g., if you have a `Vec` of comments, iterate over it with `iter()`) -- `.map` the iterator to convert each item into a rendered `Element` using `cx.render(rsx!(...))` +- `.map` the iterator to convert each item into a `LazyNode` using `rsx!(...)` - Add a unique `key` attribute to each iterator item -- Include this iterator in the final RSX +- Include this iterator in the final RSX (or use it inline) Example: suppose you have a list of comments you want to render. Then, you can render them like this: @@ -53,21 +62,26 @@ Example: suppose you have a list of comments you want to render. Then, you can r {{#include ../../../examples/rendering_lists.rs:render_list}} ``` +### Inline for loops + +Because of how common it is to render a list of items, Dioxus provides a shorthand for this. Instead of using `.iter, `.map`, and `rsx`, you can use a `for` loop with a body of rsx code: + +```rust +{{#include ../../../examples/rendering_lists.rs:render_list_for_loop}} +``` + ### The `key` Attribute -Every time you re-render your list, Dioxus needs to keep track of which item went where, because the order of items in a list might change – items might be added, removed or swapped. Despite that, Dioxus needs to: - -- Keep track of component state -- Efficiently figure out what updates need to be made to the UI +Every time you re-render your list, Dioxus needs to keep track of which items go where to determine what updates need to be made to the UI. For example, suppose the `CommentComponent` had some state – e.g. a field where the user typed in a reply. If the order of comments suddenly changes, Dioxus needs to correctly associate that state with the same comment – otherwise, the user will end up replying to a different comment! -To help Dioxus keep track of list items, we need to associate each item with a unique key. In the example above, we dynamically generated the unique key. In real applications, it's more likely that the key will come from e.g. a database ID. It doesn't really matter where you get the key from, as long as it meets the requirements +To help Dioxus keep track of list items, we need to associate each item with a unique key. In the example above, we dynamically generated the unique key. In real applications, it's more likely that the key will come from e.g. a database ID. It doesn't matter where you get the key from, as long as it meets the requirements: - Keys must be unique in a list - The same item should always get associated with the same key - Keys should be relatively small (i.e. converting the entire Comment structure to a String would be a pretty bad key) so they can be compared efficiently -You might be tempted to use an item's index in the list as its key. In fact, that’s what Dioxus will use if you don’t specify a key at all. This is only acceptable if you can guarantee that the list is constant – i.e., no re-ordering, additions or deletions. +You might be tempted to use an item's index in the list as its key. That’s what Dioxus will use if you don’t specify a key at all. This is only acceptable if you can guarantee that the list is constant – i.e., no re-ordering, additions, or deletions. > Note that if you pass the key to a component you've made, it won't receive the key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop. diff --git a/docs/guide/src/en/interactivity/event_handlers.md b/docs/guide/src/en/interactivity/event_handlers.md index 77054f3fe..5e12ce259 100644 --- a/docs/guide/src/en/interactivity/event_handlers.md +++ b/docs/guide/src/en/interactivity/event_handlers.md @@ -1,10 +1,10 @@ # Event Handlers -Events are interesting things that happen, usually related to something the user has done. For example, some events happen when the user clicks, scrolls, moves the mouse, or types a character. To respond to these actions and make the UI interactive, we need to handle those events. +Event handlers are used to respond to user actions. For example, an event handler could be triggered when the user clicks, scrolls, moves the mouse, or types a character. -Events are associated with elements. For example, we usually don't care about all the clicks that happen within an app, only those on a particular button. To handle events that happen on an element, we must attach the desired event handler to it. +Event handlers are attached to elements. For example, we usually don't care about all the clicks that happen within an app, only those on a particular button. -Event handlers are similar to regular attributes, but their name usually starts with `on`- and they accept closures as values. The closure will be called whenever the event is triggered, and will be passed that event. +Event handlers are similar to regular attributes, but their name usually starts with `on`- and they accept closures as values. The closure will be called whenever the event it listens for is triggered and will be passed that event. For example, to handle clicks on an element, we can specify an `onclick` handler: @@ -14,20 +14,24 @@ For example, to handle clicks on an element, we can specify an `onclick` handler ## The `Event` object -Event handlers receive an [`UiEvent`](https://docs.rs/dioxus-core/latest/dioxus_core/struct.UiEvent.html) object containing information about the event. Different types of events contain different types of data. For example, mouse-related events contain [`MouseData`](https://docs.rs/dioxus/latest/dioxus/events/struct.MouseData.html), which tells you things like where the mouse was clicked and what mouse buttons were used. +Event handlers receive an [`Event`](https://docs.rs/dioxus-core/latest/dioxus_core/struct.Event.html) object containing information about the event. Different types of events contain different types of data. For example, mouse-related events contain [`MouseData`](https://docs.rs/dioxus/latest/dioxus/events/struct.MouseData.html), which tells you things like where the mouse was clicked and what mouse buttons were used. In the example above, this event data was logged to the terminal: ``` -Clicked! Event: UiEvent { data: MouseData { coordinates: Coordinates { screen: (468.0, 109.0), client: (73.0, 25.0), element: (63.0, 15.0), page: (73.0, 25.0) }, modifiers: (empty), held_buttons: EnumSet(), trigger_button: Some(Primary) } } -Clicked! Event: UiEvent { data: MouseData { coordinates: Coordinates { screen: (468.0, 109.0), client: (73.0, 25.0), element: (63.0, 15.0), page: (73.0, 25.0) }, modifiers: (empty), held_buttons: EnumSet(), trigger_button: Some(Primary) } } +Clicked! Event: UiEvent { bubble_state: Cell { value: true }, data: MouseData { coordinates: Coordinates { screen: (242.0, 256.0), client: (26.0, 17.0), element: (16.0, 7.0), page: (26.0, 17.0) }, modifiers: (empty), held_buttons: EnumSet(), trigger_button: Some(Primary) } } +Clicked! Event: UiEvent { bubble_state: Cell { value: true }, data: MouseData { coordinates: Coordinates { screen: (242.0, 256.0), client: (26.0, 17.0), element: (16.0, 7.0), page: (26.0, 17.0) }, modifiers: (empty), held_buttons: EnumSet(), trigger_button: Some(Primary) } } ``` -To learn what the different event types provide, read the [events module docs](https://docs.rs/dioxus/latest/dioxus/events/index.html). +To learn what the different event types for HTML provide, read the [events module docs](https://docs.rs/dioxus-html/latest/dioxus_html/events/index.html). -### Stopping propagation +### Event propagation -When you have e.g. a `button` inside a `div`, any click on the `button` is also a click on the `div`. For this reason, Dioxus propagates the click event: first, it is triggered on the target element, then on parent elements. If you want to prevent this behavior, you can call `cancel_bubble()` on the event: +Some events will trigger first on the element the event originated at upward. For example, a click event on a `button` inside a `div` would first trigger the button's event listener and then the div's event listener. + +> For more information about event propigation see [the mdn docs on event bubling](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling) + +If you want to prevent this behavior, you can call `stop_propogation()` on the event: ```rust {{#include ../../../examples/event_nested.rs:rsx}} @@ -45,7 +49,7 @@ In some instances, might want to avoid this default behavior. For this, you can Any event handlers will still be called. -> Normally, in React or JavaScript, you'd call "preventDefault" on the event in the callback. Dioxus does *not* currently support this behavior. Note: this means you cannot conditionally prevent default behavior. +> Normally, in React or JavaScript, you'd call "preventDefault" on the event in the callback. Dioxus does *not* currently support this behavior. Note: this means you cannot conditionally prevent default behavior based on the data in the event. ## Handler Props diff --git a/docs/guide/src/en/interactivity/hooks.md b/docs/guide/src/en/interactivity/hooks.md index 8ce661c99..3539e5884 100644 --- a/docs/guide/src/en/interactivity/hooks.md +++ b/docs/guide/src/en/interactivity/hooks.md @@ -1,12 +1,12 @@ # Hooks and Component State -So far our components, being Rust functions, had no state – they were always rendering the same thing. However, in a UI component, it is often useful to have stateful functionality to build user interactions. For example, you might want to track whether the user has openend a drop-down, and render different things accordingly. +So far our components have had no state like a normal rust functions. However, in a UI component, it is often useful to have stateful functionality to build user interactions. For example, you might want to track whether the user has opened a drop-down, and render different things accordingly. -For stateful logic, you can use hooks. Hooks are Rust functions that take a reference to `ScopeState` (in a component, you can pass `&cx`), and provide you with functionality and state. +Hooks allow us to create state in our components. Hooks are Rust functions that take a reference to `ScopeState` (in a component, you can pass `cx`), and provide you with functionality and state. ## `use_state` Hook -[`use_state`](https://docs.rs/dioxus/latest/dioxus/hooks/fn.use_state.html) is one of the simplest hooks. +[`use_state`](https://docs.rs/dioxus/latest/dioxus/prelude/fn.use_state.html) is one of the simplest hooks. - You provide a closure that determines the initial value - `use_state` gives you the current value, and a way to update it by setting it to something else @@ -21,7 +21,7 @@ For example, you might have seen the counter example, in which state (a number) Every time the component's state changes, it re-renders, and the component function is called, so you can describe what you want the new UI to look like. You don't have to worry about "changing" anything – just describe what you want in terms of the state, and Dioxus will take care of the rest! -> `use_state` returns your value wrapped in a smart pointer of type [`UseState`](https://docs.rs/dioxus/latest/dioxus/hooks/struct.UseState.html). This is why you can both read the value and update it, even within a handler. +> `use_state` returns your value wrapped in a smart pointer of type [`UseState`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.UseState.html). This is why you can both read the value and update it, even within an event handler. You can use multiple hooks in the same component if you want: @@ -40,13 +40,13 @@ But how can Dioxus differentiate between multiple hooks in the same component? A {{#include ../../../examples/hooks_counter_two_state.rs:use_state_calls}} ``` -This is only possible because the two hooks are always called in the same order, so Dioxus knows which is which. So the order you call hooks matters, which is why you must follow certain rules when using hooks: +This is only possible because the two hooks are always called in the same order, so Dioxus knows which is which. Because the order you call hooks matters, you must follow certain rules when using hooks: 1. Hooks may be only used in components or other hooks (we'll get to that later) 2. On every call to the component function 1. The same hooks must be called 2. In the same order -3. Hooks name's must start with `use_` so you don't accidentally confuse them with regular functions +3. Hooks name's should start with `use_` so you don't accidentally confuse them with regular functions These rules mean that there are certain things you can't do with hooks: @@ -73,11 +73,11 @@ For example, suppose we want to maintain a `Vec` of values. If we stored it with Thankfully, there is another hook for that, `use_ref`! It is similar to `use_state`, but it lets you get a mutable reference to the contained data. -Here's a simple example that keeps a list of events in a `use_ref`. We can acquire write access to the state with `.write()`, and then just `.push` a new value to the state: +Here's a simple example that keeps a list of events in a `use_ref`. We can acquire write access to the state with `.with_mut()`, and then just `.push` a new value to the state: ```rust {{#include ../../../examples/hooks_use_ref.rs:component}} ``` -> The return values of `use_state` and `use_ref`, (`UseState` and `UseRef`, respectively) are in some ways similar to [`Cell`](https://doc.rust-lang.org/std/cell/) and [`RefCell`](https://doc.rust-lang.org/std/cell/struct.RefCell.html) – they provide interior mutability. However, these Dioxus wrappers also ensure that the component gets re-rendered whenever you change the state. +> The return values of `use_state` and `use_ref` (`UseState` and `UseRef`, respectively) are in some ways similar to [`Cell`](https://doc.rust-lang.org/std/cell/) and [`RefCell`](https://doc.rust-lang.org/std/cell/struct.RefCell.html) – they provide interior mutability. However, these Dioxus wrappers also ensure that the component gets re-rendered whenever you change the state. diff --git a/docs/guide/src/en/interactivity/router.md b/docs/guide/src/en/interactivity/router.md index cf629f6d2..90a90e8a9 100644 --- a/docs/guide/src/en/interactivity/router.md +++ b/docs/guide/src/en/interactivity/router.md @@ -1,26 +1,23 @@ # Router -In many of your apps, you'll want to have different "scenes". For a webpage, these scenes might be the different webpages with their own content. +In many of your apps, you'll want to have different "scenes". For a webpage, these scenes might be the different webpages with their own content. For a desktop app, these scenes might be different views in your app. -You could write your own scene management solution – quite simply too. However, to save you the effort, Dioxus supports a first-party solution for scene management called Dioxus Router. +To unify these platforms, Dioxus provides a first-party solution for scene management called Dioxus Router. ## What is it? -For an app like the Dioxus landing page (https://dioxuslabs.com), we want to have different pages. A quick sketch of an app would be something like: +For an app like the Dioxus landing page (https://dioxuslabs.com), we want to have several different scenes: - Homepage - Blog -- Example showcase Each of these scenes is independent – we don't want to render both the homepage and blog at the same time. -This is where the router crates come in handy. To make sure we're using the router, simply add the `"router"` feature to your dioxus dependency. +The Dioxus router makes it easy to create these scenes. To make sure we're using the router, add the `dioxus-router` package to your `Cargo.toml`. -```toml -[dependencies] -dioxus = { version = "*" } -dioxus-router = { version = "*" } +```shell +cargo add dioxus-router ``` @@ -30,8 +27,11 @@ Unlike other routers in the Rust ecosystem, our router is built declaratively. T ```rust rsx!{ + // All of our routes will be rendered inside this Router component Router { + // if the current location is "/home", render the Home component Route { to: "/home", Home {} } + // if the current location is "/blog", render the Blog component Route { to: "/blog", Blog {} } } } @@ -48,6 +48,7 @@ rsx!{ Router { Route { to: "/home", Home {} } Route { to: "/blog", Blog {} } + // if the current location doesn't match any of the above routes, render the NotFound component Route { to: "", NotFound {} } } } @@ -61,6 +62,7 @@ rsx!{ Router { Route { to: "/home", Home {} } Route { to: "/blog", Blog {} } + // if the current location doesn't match any of the above routes, redirect to "/home" Redirect { from: "", to: "/home" } } } @@ -82,6 +84,4 @@ rsx!{ ## More reading -This page is just meant to be a very brief overview of the router to show you that there's a powerful solution already built for a very common problem. For more information about the router, definitely check out its book or check out some of the examples. - -The router has its own documentation! [Available here](https://dioxuslabs.com/router/guide/). +This page is just a very brief overview of the router. For more information, check out [the router book](https://dioxuslabs.com/router/guide/) or some of [the router examples](https://github.com/DioxusLabs/dioxus/blob/master/examples/router.rs). diff --git a/docs/guide/src/en/interactivity/sharing_state.md b/docs/guide/src/en/interactivity/sharing_state.md index e5c6ca77a..75ace8973 100644 --- a/docs/guide/src/en/interactivity/sharing_state.md +++ b/docs/guide/src/en/interactivity/sharing_state.md @@ -6,9 +6,9 @@ Often, multiple components need to access the same state. Depending on your need One approach to share state between components is to "lift" it up to the nearest common ancestor. This means putting the `use_state` hook in a parent component, and passing the needed values down as props. -For example, suppose we want to build a meme editor. We want to have an input to edit the meme caption, but also a preview of the meme with the caption. Logically, the meme and the input are 2 separate components, but they need access to the same state (the current caption). +Suppose we want to build a meme editor. We want to have an input to edit the meme caption, but also a preview of the meme with the caption. Logically, the meme and the input are 2 separate components, but they need access to the same state (the current caption). -> Of course, in this simple example, we could write everything in one component – but it is better to split everything out in smaller components to make the code more reusable and easier to maintain (this is even more important for larger, complex apps). +> Of course, in this simple example, we could write everything in one component – but it is better to split everything out in smaller components to make the code more reusable, maintainable, and performant (this is even more important for larger, complex apps). We start with a `Meme` component, responsible for rendering a meme with a given caption: ```rust @@ -57,7 +57,7 @@ As a result, any child component of `App` (direct or not), can access the `DarkM {{#include ../../../examples/meme_editor_dark_mode.rs:use_context}} ``` -> `use_context` returns `Option>` here. If the context has been provided, the value is `Some(UseSharedState)`, which you can call `.read` or `.write` on, similarly to `UseRef`. Otherwise, the value is `None`. +> `use_context` returns `Option>` here. If the context has been provided, the value is `Some(UseSharedState)`, which you can call `.read` or `.write` on, similarly to `UseRef`. Otherwise, the value is `None`. For example, here's how we would implement the dark mode toggle, which both reads the context (to determine what color it should render) and writes to it (to toggle dark mode): ```rust diff --git a/docs/guide/src/en/interactivity/user_input.md b/docs/guide/src/en/interactivity/user_input.md index ed9c97584..e39dbcde5 100644 --- a/docs/guide/src/en/interactivity/user_input.md +++ b/docs/guide/src/en/interactivity/user_input.md @@ -19,7 +19,7 @@ Notice the flexibility – you can: ## Uncontrolled Inputs -As an alternative to controlled inputs, you can simply let the platform keep track of the input values. If we don't tell a HTML input what content it should have, it be editable anyway (this is built into the webview). This approach can be more performant, but less flexible. For example, it's harder to keep the input in sync with another element. +As an alternative to controlled inputs, you can simply let the platform keep track of the input values. If we don't tell a HTML input what content it should have, it will be editable anyway (this is built into the browser). This approach can be more performant, but less flexible. For example, it's harder to keep the input in sync with another element. Since you don't necessarily have the current value of the uncontrolled input in state, you can access it either by listening to `oninput` events (similarly to controlled components), or, if the input is part of a form, you can access the form data in the form events (e.g. `oninput` or `onsubmit`): diff --git a/docs/guide/src/en/roadmap.md b/docs/guide/src/en/roadmap.md index 1c4e9c1bf..5e7318de9 100644 --- a/docs/guide/src/en/roadmap.md +++ b/docs/guide/src/en/roadmap.md @@ -125,8 +125,10 @@ We are currently working on our own build tool called [Dioxus CLI](https://githu - ability to publish to github/netlify/vercel - bundling for iOS/Desktop/etc -### LiveView / Server Component Support +### Server Component Support -The internal architecture of Dioxus was designed from day one to support the `LiveView` use-case, where a web server hosts a running app for each connected user. As of today, there is no first-class LiveView support – you'll need to wire this up yourself. +While not currently fully implemented, the expectation is that LiveView apps can be a hybrid between Wasm and server-rendered where only portions of a page are "live" and the rest of the page is either server-rendered, statically generated, or handled by the host SPA. -While not currently fully implemented, the expectation is that LiveView apps can be a hybrid between Wasm and server-rendered where only portions of a page are "live" and the rest of the page is either server-rendered, statically generated, or handled by the host SPA. \ No newline at end of file +### Native rendering + +We are currently working on a native renderer for Dioxus using WGPU called [Blitz](https://github.com/DioxusLabs/blitz/). This will allow you to build apps that are rendered natively for iOS, Android, and Desktop. diff --git a/docs/guide/src/pt-br/interactivity/roteador.md b/docs/guide/src/pt-br/interactivity/roteador.md new file mode 100644 index 000000000..564445de5 --- /dev/null +++ b/docs/guide/src/pt-br/interactivity/roteador.md @@ -0,0 +1 @@ +# Roteamento diff --git a/docs/nightly/readme.md b/docs/nightly/readme.md deleted file mode 100644 index 99f706e81..000000000 --- a/docs/nightly/readme.md +++ /dev/null @@ -1 +0,0 @@ -this directory is for deploying into diff --git a/examples/clock.rs b/examples/clock.rs index a159ef73a..6b5c59d7e 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -3,29 +3,25 @@ //! The example from the README.md. use dioxus::prelude::*; +use dioxus_signals::{use_init_signal_rt, use_signal}; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - let count = use_ref(cx, || 0); + use_init_signal_rt(cx); - let ct = count.to_owned(); - use_coroutine(cx, |_: UnboundedReceiver<()>| async move { + let mut count = use_signal(cx, || 0); + + use_future!(cx, || async move { loop { - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - - *ct.write() += 1; - - let current = *ct.read(); - - println!("current: {}", current); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + count += 1; + println!("current: {}", count); } }); - let count = count.read(); - cx.render(rsx! { div { "High-Five counter: {count}" } }) diff --git a/examples/compose.rs b/examples/compose.rs new file mode 100644 index 000000000..fa5d6e4c2 --- /dev/null +++ b/examples/compose.rs @@ -0,0 +1,82 @@ +//! 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() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let window = use_window(cx); + let emails_sent = use_ref(cx, || vec![]); + + let tx = use_coroutine(cx, |mut rx: UnboundedReceiver| { + to_owned![emails_sent]; + async move { + while let Some(message) = rx.next().await { + emails_sent.write().push(message); + } + } + }); + + cx.render(rsx! { + div { + h1 { "This is your email" } + + 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. + window.new_window(dom, Default::default()); + }, + "Click to compose a new email" + } + + ul { + emails_sent.read().iter().map(|message| cx.render(rsx! { + li { + h3 { "email" } + span {"{message}"} + } + })) + } + } + }) +} + +struct ComposeProps { + app_tx: Coroutine, +} + +fn compose(cx: Scope) -> Element { + let user_input = use_state(cx, || String::new()); + let window = use_window(cx); + + cx.render(rsx! { + div { + h1 { "Compose a new email" } + + button { + onclick: move |_| { + cx.props.app_tx.send(user_input.get().clone()); + window.close(); + }, + "Click to send" + } + + input { + oninput: move |e| { + user_input.set(e.value.clone()); + }, + value: "{user_input}" + } + } + }) +} diff --git a/examples/custom_assets.rs b/examples/custom_assets.rs index 86d82acf4..ae37ecd4e 100644 --- a/examples/custom_assets.rs +++ b/examples/custom_assets.rs @@ -8,7 +8,8 @@ fn app(cx: Scope) -> Element { cx.render(rsx! { div { "This should show an image:" - img { src: "examples/assets/logo.png", } + img { src: "examples/assets/logo.png" } + img { src: "/Users/jonkelley/Desktop/blitz.png" } } }) } diff --git a/examples/fermi.rs b/examples/fermi.rs index babe3e024..506d9fe98 100644 --- a/examples/fermi.rs +++ b/examples/fermi.rs @@ -10,6 +10,7 @@ fn main() { static NAME: Atom = |_| "world".to_string(); fn app(cx: Scope) -> Element { + use_init_atom_root(&cx); let name = use_read(cx, NAME); cx.render(rsx! { diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs new file mode 100644 index 000000000..943890f3b --- /dev/null +++ b/examples/multiwindow.rs @@ -0,0 +1,28 @@ +use dioxus::prelude::*; +use dioxus_desktop::{use_window, WindowBuilder}; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let window = use_window(cx); + + cx.render(rsx! { + div { + button { + onclick: move |_| { + let dom = VirtualDom::new(popup); + window.new_window(dom, Default::default()); + }, + "New Window" + } + } + }) +} + +fn popup(cx: Scope) -> Element { + cx.render(rsx! { + div { "This is a popup!" } + }) +} diff --git a/examples/nested_listeners.rs b/examples/nested_listeners.rs index 3938959a7..4d68b281b 100644 --- a/examples/nested_listeners.rs +++ b/examples/nested_listeners.rs @@ -16,18 +16,18 @@ fn app(cx: Scope) -> Element { onclick: move |_| println!("clicked! top"), "- div" button { - onclick: move |_| println!("clicked! bottom propoate"), - "Propogate" + onclick: move |_| println!("clicked! bottom propagate"), + "Propagate" } button { onclick: move |evt| { println!("clicked! bottom no bubbling"); - evt.stop_propogation(); + evt.stop_propagation(); }, - "Dont propogate" + "Dont propagate" } button { - "Does not handle clicks - only propogate" + "Does not handle clicks - only propagate" } } }) diff --git a/examples/overlay.rs b/examples/overlay.rs new file mode 100644 index 000000000..359fa5169 --- /dev/null +++ b/examples/overlay.rs @@ -0,0 +1,63 @@ +use dioxus::prelude::*; +use dioxus_desktop::{tao::dpi::PhysicalPosition, use_window, Config, 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%", + height: "100%", + background_color: "red", + border: "1px solid black", + + div { + width: "100%", + height: "10px", + background_color: "black", + onmousedown: move |_| window.drag(), + } + + "This is an overlay!" + } + }) +} + +fn make_config() -> dioxus_desktop::Config { + dioxus_desktop::Config::default() + .with_window(make_window()) + .with_custom_head( + r#" + +"# + .to_owned(), + ) +} + +fn make_window() -> WindowBuilder { + WindowBuilder::new() + .with_transparent(true) + .with_decorations(false) + .with_resizable(false) + .with_always_on_top(true) + .with_position(PhysicalPosition::new(0, 0)) + .with_max_inner_size(LogicalSize::new(100000, 50)) +} diff --git a/examples/signals.rs b/examples/signals.rs new file mode 100644 index 000000000..988b9af41 --- /dev/null +++ b/examples/signals.rs @@ -0,0 +1,30 @@ +use dioxus::prelude::*; +use dioxus_signals::{use_init_signal_rt, use_signal}; +use std::time::Duration; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + use_init_signal_rt(cx); + + let mut count = use_signal(cx, || 0); + + use_future!(cx, || async move { + loop { + count += 1; + tokio::time::sleep(Duration::from_millis(400)).await; + } + }); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + + if count() > 5 { + rsx!{ h2 { "High five!" } } + } + }) +} diff --git a/examples/window_event.rs b/examples/window_event.rs index 44ddebeca..ca9b8bab8 100644 --- a/examples/window_event.rs +++ b/examples/window_event.rs @@ -35,13 +35,13 @@ fn app(cx: Scope) -> Element { nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" } button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", - onmousedown: |evt| evt.stop_propogation(), + onmousedown: |evt| evt.stop_propagation(), onclick: move |_| window.set_minimized(true), "Minimize" } button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", - onmousedown: |evt| evt.stop_propogation(), + onmousedown: |evt| evt.stop_propagation(), onclick: move |_| { window.set_fullscreen(!**fullscreen); @@ -52,7 +52,7 @@ fn app(cx: Scope) -> Element { } button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", - onmousedown: |evt| evt.stop_propogation(), + onmousedown: |evt| evt.stop_propagation(), onclick: move |_| window.close(), "Close" } @@ -66,7 +66,7 @@ fn app(cx: Scope) -> Element { div { button { class: "inline-flex items-center text-white bg-green-500 border-0 py-1 px-3 hover:bg-green-700 rounded", - onmousedown: |evt| evt.stop_propogation(), + onmousedown: |evt| evt.stop_propagation(), onclick: move |_| { window.set_always_on_top(!always_on_top); always_on_top.set(!always_on_top); @@ -77,7 +77,7 @@ fn app(cx: Scope) -> Element { div { button { class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", - onmousedown: |evt| evt.stop_propogation(), + onmousedown: |evt| evt.stop_propagation(), onclick: move |_| { window.set_decorations(!decorations); decorations.set(!decorations); @@ -88,7 +88,7 @@ fn app(cx: Scope) -> Element { div { button { class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", - onmousedown: |evt| evt.stop_propogation(), + onmousedown: |evt| evt.stop_propagation(), onclick: move |_| window.set_title("Dioxus Application"), "Change Title" } diff --git a/packages/autofmt/Cargo.toml b/packages/autofmt/Cargo.toml index fae35afd0..e4c9eb1cd 100644 --- a/packages/autofmt/Cargo.toml +++ b/packages/autofmt/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "dioxus-autofmt" -version = "0.0.0" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-rsx = { path = "../rsx" } +dioxus-rsx = { path = "../rsx", version = "0.0.2"} proc-macro2 = { version = "1.0.6", features = ["span-locations"] } quote = "1.0" syn = { version = "1.0.11", features = ["full", "extra-traits"] } diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs index fd0b6b60d..26c47f54b 100644 --- a/packages/autofmt/src/buffer.rs +++ b/packages/autofmt/src/buffer.rs @@ -1,33 +1,18 @@ -use std::{ - collections::{HashMap, VecDeque}, - fmt::{Result, Write}, -}; +//! The output buffer that supports some helpful methods +//! These are separate from the input so we can lend references between the two +//! +//! +//! -use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, IfmtInput}; -use proc_macro2::{LineColumn, Span}; -use syn::{spanned::Spanned, Expr}; +use std::fmt::{Result, Write}; -#[derive(Default, Debug)] +use dioxus_rsx::IfmtInput; + +/// The output buffer that tracks indent and string +#[derive(Debug, Default)] pub struct Buffer { - pub src: Vec, - pub cached_formats: HashMap, pub buf: String, pub indent: usize, - pub comments: VecDeque, -} - -#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] -pub struct Location { - pub line: usize, - pub col: usize, -} -impl Location { - pub fn new(start: LineColumn) -> Self { - Self { - line: start.line, - col: start.column, - } - } } impl Buffer { @@ -62,160 +47,14 @@ impl Buffer { writeln!(self.buf) } - // Expects to be written directly into place - pub fn write_ident(&mut self, node: &BodyNode) -> Result { - match node { - BodyNode::Element(el) => self.write_element(el), - BodyNode::Component(component) => self.write_component(component), - BodyNode::Text(text) => self.write_text(text), - BodyNode::RawExpr(exp) => self.write_raw_expr(exp), - _ => Ok(()), - } - } - pub fn write_text(&mut self, text: &IfmtInput) -> Result { write!(self.buf, "\"{}\"", text.source.as_ref().unwrap().value()) } - - pub fn consume(self) -> Option { - Some(self.buf) - } - - pub fn write_comments(&mut self, child: Span) -> Result { - // collect all comments upwards - let start = child.start(); - let line_start = start.line - 1; - - for (id, line) in self.src[..line_start].iter().enumerate().rev() { - if line.trim().starts_with("//") || line.is_empty() { - if id != 0 { - self.comments.push_front(id); - } - } else { - break; - } - } - - let mut last_was_empty = false; - while let Some(comment_line) = self.comments.pop_front() { - let line = &self.src[comment_line]; - if line.is_empty() { - if !last_was_empty { - self.new_line()?; - } - last_was_empty = true; - } else { - last_was_empty = false; - self.tabbed_line()?; - write!(self.buf, "{}", self.src[comment_line].trim())?; - } - } - - Ok(()) - } - - // Push out the indent level and write each component, line by line - pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result { - self.indent += 1; - - self.write_body_no_indent(children)?; - - self.indent -= 1; - Ok(()) - } - - pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result { - let last_child = children.len(); - - for (idx, child) in children.iter().enumerate() { - match child { - // check if the expr is a short - BodyNode::RawExpr { .. } => { - self.tabbed_line()?; - self.write_ident(child)?; - if idx != last_child - 1 { - write!(self.buf, ",")?; - } - } - _ => { - if self.current_span_is_primary(child.span()) { - self.write_comments(child.span())?; - } - self.tabbed_line()?; - self.write_ident(child)?; - } - } - } - - Ok(()) - } - - pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize { - let mut total = 0; - - for attr in attributes { - if self.current_span_is_primary(attr.attr.start()) { - 'line: for line in self.src[..attr.attr.start().start().line - 1].iter().rev() { - match (line.trim().starts_with("//"), line.is_empty()) { - (true, _) => return 100000, - (_, true) => continue 'line, - _ => break 'line, - } - } - } - - total += match &attr.attr { - ElementAttr::AttrText { value, name } => { - value.source.as_ref().unwrap().value().len() + name.span().line_length() + 3 - } - ElementAttr::AttrExpression { name, value } => { - value.span().line_length() + name.span().line_length() + 3 - } - ElementAttr::CustomAttrText { value, name } => { - value.source.as_ref().unwrap().value().len() + name.value().len() + 3 - } - ElementAttr::CustomAttrExpression { name, value } => { - name.value().len() + value.span().line_length() + 3 - } - 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() + 3 - } - }; - } - - total - } - - pub fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str { - self.cached_formats - .entry(Location::new(expr.span().start())) - .or_insert_with(|| prettyplease::unparse_expr(expr)) - .as_str() - } } -trait SpanLength { - fn line_length(&self) -> usize; -} -impl SpanLength for Span { - fn line_length(&self) -> usize { - self.end().line - self.start().line +impl std::fmt::Write for Buffer { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.buf.push_str(s); + Ok(()) } } diff --git a/packages/autofmt/src/component.rs b/packages/autofmt/src/component.rs index 0da8a85d7..b3ab7fbbf 100644 --- a/packages/autofmt/src/component.rs +++ b/packages/autofmt/src/component.rs @@ -1,4 +1,4 @@ -use crate::{buffer::Location, Buffer}; +use crate::{writer::Location, Writer}; use dioxus_rsx::*; use quote::ToTokens; use std::fmt::{Result, Write}; @@ -19,7 +19,7 @@ enum ShortOptimization { NoOpt, } -impl Buffer { +impl Writer { pub fn write_component( &mut self, Component { @@ -28,6 +28,7 @@ impl Buffer { children, manual_props, prop_gen_args, + .. }: &Component, ) -> Result { self.write_component_name(name, prop_gen_args)?; @@ -82,46 +83,46 @@ impl Buffer { match opt_level { ShortOptimization::Empty => {} ShortOptimization::Oneliner => { - write!(self.buf, " ")?; + write!(self.out, " ")?; self.write_component_fields(fields, manual_props, true)?; if !children.is_empty() && !fields.is_empty() { - write!(self.buf, ", ")?; + write!(self.out, ", ")?; } for child in children { self.write_ident(child)?; } - write!(self.buf, " ")?; + write!(self.out, " ")?; } ShortOptimization::PropsOnTop => { - write!(self.buf, " ")?; + write!(self.out, " ")?; self.write_component_fields(fields, manual_props, true)?; if !children.is_empty() && !fields.is_empty() { - write!(self.buf, ",")?; + write!(self.out, ",")?; } self.write_body_indented(children)?; - self.tabbed_line()?; + self.out.tabbed_line()?; } ShortOptimization::NoOpt => { self.write_component_fields(fields, manual_props, false)?; if !children.is_empty() && !fields.is_empty() { - write!(self.buf, ",")?; + write!(self.out, ",")?; } self.write_body_indented(children)?; - self.tabbed_line()?; + self.out.tabbed_line()?; } } - write!(self.buf, "}}")?; + write!(self.out, "}}")?; Ok(()) } @@ -133,16 +134,16 @@ impl Buffer { let mut name = name.to_token_stream().to_string(); name.retain(|c| !c.is_whitespace()); - write!(self.buf, "{name}")?; + write!(self.out, "{name}")?; if let Some(generics) = generics { let mut written = generics.to_token_stream().to_string(); written.retain(|c| !c.is_whitespace()); - write!(self.buf, "{}", written)?; + write!(self.out, "{}", written)?; } - write!(self.buf, " {{")?; + write!(self.out, " {{")?; Ok(()) } @@ -157,18 +158,18 @@ impl Buffer { while let Some(field) = field_iter.next() { if !sameline { - self.indented_tabbed_line()?; + self.out.indented_tabbed_line()?; } let name = &field.name; match &field.content { ContentField::ManExpr(exp) => { let out = prettyplease::unparse_expr(exp); - write!(self.buf, "{}: {}", name, out)?; + write!(self.out, "{}: {}", name, out)?; } ContentField::Formatted(s) => { write!( - self.buf, + self.out, "{}: \"{}\"", name, s.source.as_ref().unwrap().value() @@ -178,27 +179,27 @@ impl Buffer { let out = prettyplease::unparse_expr(exp); let mut lines = out.split('\n').peekable(); let first = lines.next().unwrap(); - write!(self.buf, "{}: {}", name, first)?; + write!(self.out, "{}: {}", name, first)?; for line in lines { - self.new_line()?; - self.indented_tab()?; - write!(self.buf, "{}", line)?; + self.out.new_line()?; + self.out.indented_tab()?; + write!(self.out, "{}", line)?; } } } if field_iter.peek().is_some() || manual_props.is_some() { - write!(self.buf, ",")?; + write!(self.out, ",")?; if sameline { - write!(self.buf, " ")?; + write!(self.out, " ")?; } } } if let Some(exp) = manual_props { if !sameline { - self.indented_tabbed_line()?; + self.out.indented_tabbed_line()?; } self.write_manual_props(exp)?; } @@ -258,10 +259,10 @@ impl Buffer { let first_line = lines.next().unwrap(); - write!(self.buf, "..{first_line}")?; + write!(self.out, "..{first_line}")?; for line in lines { - self.indented_tabbed_line()?; - write!(self.buf, "{line}")?; + self.out.indented_tabbed_line()?; + write!(self.out, "{line}")?; } Ok(()) diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 310641121..bd9e83046 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -1,35 +1,56 @@ -use crate::Buffer; +use crate::Writer; use dioxus_rsx::*; use proc_macro2::Span; -use std::{fmt::Result, fmt::Write}; -use syn::{spanned::Spanned, Expr}; +use std::{ + fmt::Result, + fmt::{self, Write}, +}; +use syn::{spanned::Spanned, token::Brace, Expr}; #[derive(Debug)] enum ShortOptimization { - // Special because we want to print the closing bracket immediately + /// Special because we want to print the closing bracket immediately + /// + /// IE + /// `div {}` instead of `div { }` Empty, - // Special optimization to put everything on the same line + /// Special optimization to put everything on the same line and add some buffer spaces + /// + /// IE + /// + /// `div { "asdasd" }` instead of a multiline variant Oneliner, - // Optimization where children flow but props remain fixed on top + /// Optimization where children flow but props remain fixed on top PropsOnTop, - // The noisiest optimization where everything flows + /// The noisiest optimization where everything flows NoOpt, } -impl Buffer { - pub fn write_element( - &mut self, - Element { +/* +// whitespace +div { + // some whitespace + class: "asdasd" + + // whjiot + asdasd // whitespace +} +*/ + +impl Writer { + pub fn write_element(&mut self, el: &Element) -> Result { + let Element { name, key, attributes, children, _is_static, - }: &Element, - ) -> Result { + brace, + } = el; + /* 1. Write the tag 2. Write the key @@ -37,7 +58,7 @@ impl Buffer { 4. Write the children */ - write!(self.buf, "{name} {{")?; + write!(self.out, "{name} {{")?; // decide if we have any special optimizations // Default with none, opt the cases in one-by-one @@ -70,6 +91,9 @@ impl Buffer { // If there's nothing at all, empty optimization if attributes.is_empty() && children.is_empty() && key.is_none() { opt_level = ShortOptimization::Empty; + + // Write comments if they exist + self.write_todo_body(brace)?; } // multiline handlers bump everything down @@ -80,55 +104,56 @@ impl Buffer { match opt_level { ShortOptimization::Empty => {} ShortOptimization::Oneliner => { - write!(self.buf, " ")?; + write!(self.out, " ")?; self.write_attributes(attributes, key, true)?; if !children.is_empty() && (!attributes.is_empty() || key.is_some()) { - write!(self.buf, ", ")?; + write!(self.out, ", ")?; } for (id, child) in children.iter().enumerate() { self.write_ident(child)?; if id != children.len() - 1 && children.len() > 1 { - write!(self.buf, ", ")?; + write!(self.out, ", ")?; } } - write!(self.buf, " ")?; + write!(self.out, " ")?; } ShortOptimization::PropsOnTop => { if !attributes.is_empty() || key.is_some() { - write!(self.buf, " ")?; + write!(self.out, " ")?; } self.write_attributes(attributes, key, true)?; if !children.is_empty() && (!attributes.is_empty() || key.is_some()) { - write!(self.buf, ",")?; + write!(self.out, ",")?; } if !children.is_empty() { self.write_body_indented(children)?; } - self.tabbed_line()?; + self.out.tabbed_line()?; } ShortOptimization::NoOpt => { self.write_attributes(attributes, key, false)?; if !children.is_empty() && (!attributes.is_empty() || key.is_some()) { - write!(self.buf, ",")?; + write!(self.out, ",")?; } if !children.is_empty() { self.write_body_indented(children)?; } - self.tabbed_line()?; + + self.out.tabbed_line()?; } } - write!(self.buf, "}}")?; + write!(self.out, "}}")?; Ok(()) } @@ -143,39 +168,39 @@ impl Buffer { if let Some(key) = key { if !sameline { - self.indented_tabbed_line()?; + self.out.indented_tabbed_line()?; } write!( - self.buf, + self.out, "key: \"{}\"", key.source.as_ref().unwrap().value() )?; if !attributes.is_empty() { - write!(self.buf, ",")?; + write!(self.out, ",")?; if sameline { - write!(self.buf, " ")?; + write!(self.out, " ")?; } } } while let Some(attr) = attr_iter.next() { - self.indent += 1; + self.out.indent += 1; if !sameline { self.write_comments(attr.attr.start())?; } - self.indent -= 1; + self.out.indent -= 1; if !sameline { - self.indented_tabbed_line()?; + self.out.indented_tabbed_line()?; } self.write_attribute(attr)?; if attr_iter.peek().is_some() { - write!(self.buf, ",")?; + write!(self.out, ",")?; if sameline { - write!(self.buf, " ")?; + write!(self.out, " ")?; } } } @@ -187,19 +212,19 @@ impl Buffer { match &attr.attr { ElementAttr::AttrText { name, value } => { write!( - self.buf, + self.out, "{name}: \"{value}\"", value = value.source.as_ref().unwrap().value() )?; } ElementAttr::AttrExpression { name, value } => { let out = prettyplease::unparse_expr(value); - write!(self.buf, "{}: {}", name, out)?; + write!(self.out, "{}: {}", name, out)?; } ElementAttr::CustomAttrText { name, value } => { write!( - self.buf, + self.out, "\"{name}\": \"{value}\"", name = name.value(), value = value.source.as_ref().unwrap().value() @@ -208,7 +233,7 @@ impl Buffer { ElementAttr::CustomAttrExpression { name, value } => { let out = prettyplease::unparse_expr(value); - write!(self.buf, "\"{}\": {}", name.value(), out)?; + write!(self.out, "\"{}\": {}", name.value(), out)?; } ElementAttr::EventTokens { name, tokens } => { @@ -220,17 +245,17 @@ impl Buffer { // a one-liner for whatever reason // Does not need a new line if lines.peek().is_none() { - write!(self.buf, "{}: {}", name, first)?; + write!(self.out, "{}: {}", name, first)?; } else { - writeln!(self.buf, "{}: {}", name, first)?; + writeln!(self.out, "{}: {}", name, first)?; while let Some(line) = lines.next() { - self.indented_tab()?; - write!(self.buf, "{}", line)?; + self.out.indented_tab()?; + write!(self.out, "{}", line)?; if lines.peek().is_none() { - write!(self.buf, "")?; + write!(self.out, "")?; } else { - writeln!(self.buf)?; + writeln!(self.out)?; } } } @@ -254,8 +279,6 @@ impl Buffer { "" }; - // dbg!(beginning); - beginning.is_empty() } @@ -268,6 +291,10 @@ impl Buffer { if children.is_empty() { // todo: allow elements with comments but no children // like div { /* comment */ } + // or + // div { + // // some helpful + // } return Some(0); } @@ -342,6 +369,35 @@ impl Buffer { } } } + + /// empty everything except for some comments + fn write_todo_body(&mut self, brace: &Brace) -> fmt::Result { + let span = brace.span.span(); + let start = span.start(); + let end = span.end(); + + if start.line == end.line { + return Ok(()); + } + + writeln!(self.out)?; + + for idx in start.line..end.line { + let line = &self.src[idx]; + if line.trim().starts_with("//") { + for _ in 0..self.out.indent + 1 { + write!(self.out, " ")? + } + writeln!(self.out, "{}", line.trim()).unwrap(); + } + } + + for _ in 0..self.out.indent { + write!(self.out, " ")? + } + + Ok(()) + } } fn get_expr_length(expr: &Expr) -> Option { diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs index 48ea6fcc7..55a5fa9eb 100644 --- a/packages/autofmt/src/expr.rs +++ b/packages/autofmt/src/expr.rs @@ -1,45 +1,23 @@ //! pretty printer for rsx! use std::fmt::{Result, Write}; -use crate::Buffer; +use crate::Writer; -impl Buffer { +impl Writer { pub fn write_raw_expr(&mut self, exp: &syn::Expr) -> Result { /* We want to normalize the expr to the appropriate indent level. */ - // in a perfect world, just fire up the rust pretty printer - // pretty_print_rust_code_as_if_it_were_rustfmt() - use syn::spanned::Spanned; let placement = exp.span(); let start = placement.start(); let end = placement.end(); - // let num_spaces_desired = (self.indent * 4) as isize; - - // print comments - // let mut queued_comments = vec![]; - // let mut offset = 2; - // loop { - // let line = &self.src[start.line - offset]; - // if line.trim_start().starts_with("//") { - // queued_comments.push(line); - // } else { - // break; - // } - - // offset += 1; - // } - // let had_comments = !queued_comments.is_empty(); - // for comment in queued_comments.into_iter().rev() { - // writeln!(self.buf, "{}", comment)?; - // } // if the expr is on one line, just write it directly if start.line == end.line { write!( - self.buf, + self.out, "{}", &self.src[start.line - 1][start.column - 1..end.column].trim() )?; @@ -50,7 +28,7 @@ impl Buffer { // This involves unshifting the first line if it's aligned let first_line = &self.src[start.line - 1]; write!( - self.buf, + self.out, "{}", &first_line[start.column - 1..first_line.len()].trim() )?; @@ -66,7 +44,7 @@ impl Buffer { }; for (id, line) in self.src[start.line..end.line].iter().enumerate() { - writeln!(self.buf)?; + writeln!(self.out)?; // trim the leading whitespace let line = match id { x if x == (end.line - start.line) - 1 => &line[..end.column], @@ -75,52 +53,17 @@ impl Buffer { if offset < 0 { for _ in 0..-offset { - write!(self.buf, " ")?; + write!(self.out, " ")?; } - write!(self.buf, "{}", line)?; + write!(self.out, "{}", line)?; } else { let offset = offset as usize; let right = &line[offset..]; - write!(self.buf, "{}", right)?; + write!(self.out, "{}", right)?; } } - // let first = &self.src[start.line - 1]; - // let num_spaces_real = first.chars().take_while(|c| c.is_whitespace()).count() as isize; - // let offset = num_spaces_real - num_spaces_desired; - - // for (row, line) in self.src[start.line - 1..end.line].iter().enumerate() { - // let line = match row { - // 0 => &line[start.column - 1..], - // a if a == (end.line - start.line) => &line[..end.column - 1], - // _ => line, - // }; - - // writeln!(self.buf)?; - // // trim the leading whitespace - // if offset < 0 { - // for _ in 0..-offset { - // write!(self.buf, " ")?; - // } - - // write!(self.buf, "{}", line)?; - // } else { - // let offset = offset as usize; - // let right = &line[offset..]; - // write!(self.buf, "{}", right)?; - // } - // } - Ok(()) } } - -// :( -// fn pretty_print_rust_code_as_if_it_were_rustfmt(code: &str) -> String { -// let formatted = prettyplease::unparse_expr(exp); -// for line in formatted.lines() { -// write!(self.buf, "{}", line)?; -// self.new_line()?; -// } -// } diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index a5da71c9b..67f087854 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -1,13 +1,14 @@ use dioxus_rsx::CallBody; -use crate::buffer::*; use crate::util::*; +use crate::writer::*; mod buffer; mod component; mod element; mod expr; mod util; +mod writer; /// A modification to the original file to be applied by an IDE /// @@ -101,10 +102,9 @@ pub fn fmt_file(contents: &str) -> Vec { } pub fn write_block_out(body: CallBody) -> Option { - let mut buf = Buffer { + let mut buf = Writer { src: vec!["".to_string()], - indent: 0, - ..Buffer::default() + ..Writer::default() }; // Oneliner optimization @@ -120,12 +120,13 @@ pub fn write_block_out(body: CallBody) -> Option { pub fn fmt_block(block: &str, indent_level: usize) -> Option { let body = syn::parse_str::(block).ok()?; - let mut buf = Buffer { + let mut buf = Writer { src: block.lines().map(|f| f.to_string()).collect(), - indent: indent_level, - ..Buffer::default() + ..Writer::default() }; + buf.out.indent = indent_level; + // Oneliner optimization if buf.is_short_children(&body.roots).is_some() { buf.write_ident(&body.roots[0]).unwrap(); @@ -134,8 +135,8 @@ pub fn fmt_block(block: &str, indent_level: usize) -> Option { } // writing idents leaves the final line ended at the end of the last ident - if buf.buf.contains('\n') { - buf.new_line().unwrap(); + if buf.out.buf.contains('\n') { + buf.out.new_line().unwrap(); } buf.consume() diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs new file mode 100644 index 000000000..4d4aed4a3 --- /dev/null +++ b/packages/autofmt/src/writer.rs @@ -0,0 +1,188 @@ +use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed}; +use proc_macro2::{LineColumn, Span}; +use std::{ + collections::{HashMap, VecDeque}, + fmt::{Result, Write}, +}; +use syn::{spanned::Spanned, Expr}; + +use crate::buffer::Buffer; + +#[derive(Debug, Default)] +pub struct Writer { + pub src: Vec, + pub cached_formats: HashMap, + pub comments: VecDeque, + pub out: Buffer, +} + +#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] +pub struct Location { + pub line: usize, + pub col: usize, +} +impl Location { + pub fn new(start: LineColumn) -> Self { + Self { + line: start.line, + col: start.column, + } + } +} + +impl Writer { + // Expects to be written directly into place + pub fn write_ident(&mut self, node: &BodyNode) -> Result { + match node { + BodyNode::Element(el) => self.write_element(el), + BodyNode::Component(component) => self.write_component(component), + BodyNode::Text(text) => self.out.write_text(text), + BodyNode::RawExpr(exp) => self.write_raw_expr(exp), + _ => Ok(()), + } + } + + pub fn consume(self) -> Option { + Some(self.out.buf) + } + + pub fn write_comments(&mut self, child: Span) -> Result { + // collect all comments upwards + let start = child.start(); + let line_start = start.line - 1; + + for (id, line) in self.src[..line_start].iter().enumerate().rev() { + if line.trim().starts_with("//") || line.is_empty() { + if id != 0 { + self.comments.push_front(id); + } + } else { + break; + } + } + + let mut last_was_empty = false; + while let Some(comment_line) = self.comments.pop_front() { + let line = &self.src[comment_line]; + if line.is_empty() { + if !last_was_empty { + self.out.new_line()?; + } + last_was_empty = true; + } else { + last_was_empty = false; + self.out.tabbed_line()?; + write!(self.out, "{}", self.src[comment_line].trim())?; + } + } + + Ok(()) + } + + // Push out the indent level and write each component, line by line + pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result { + self.out.indent += 1; + + self.write_body_no_indent(children)?; + + self.out.indent -= 1; + Ok(()) + } + + pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result { + let last_child = children.len(); + let iter = children.iter().peekable().enumerate(); + + for (idx, child) in iter { + if self.current_span_is_primary(child.span()) { + self.write_comments(child.span())?; + } + + match child { + // check if the expr is a short + BodyNode::RawExpr { .. } => { + self.out.tabbed_line()?; + self.write_ident(child)?; + if idx != last_child - 1 { + write!(self.out, ",")?; + } + } + _ => { + self.out.tabbed_line()?; + self.write_ident(child)?; + } + } + } + + Ok(()) + } + + pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize { + let mut total = 0; + + for attr in attributes { + if self.current_span_is_primary(attr.attr.start()) { + 'line: for line in self.src[..attr.attr.start().start().line - 1].iter().rev() { + match (line.trim().starts_with("//"), line.is_empty()) { + (true, _) => return 100000, + (_, true) => continue 'line, + _ => break 'line, + } + } + } + + total += match &attr.attr { + ElementAttr::AttrText { value, name } => { + value.source.as_ref().unwrap().value().len() + name.span().line_length() + 3 + } + ElementAttr::AttrExpression { name, value } => { + value.span().line_length() + name.span().line_length() + 3 + } + ElementAttr::CustomAttrText { value, name } => { + value.source.as_ref().unwrap().value().len() + name.value().len() + 3 + } + ElementAttr::CustomAttrExpression { name, value } => { + name.value().len() + value.span().line_length() + 3 + } + 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() + 3 + } + }; + } + + total + } + + pub fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str { + self.cached_formats + .entry(Location::new(expr.span().start())) + .or_insert_with(|| prettyplease::unparse_expr(expr)) + .as_str() + } +} + +trait SpanLength { + fn line_length(&self) -> usize; +} +impl SpanLength for Span { + fn line_length(&self) -> usize { + self.end().line - self.start().line + } +} diff --git a/packages/autofmt/tests/samples.rs b/packages/autofmt/tests/samples.rs index 1f9c359c1..19588fda0 100644 --- a/packages/autofmt/tests/samples.rs +++ b/packages/autofmt/tests/samples.rs @@ -30,3 +30,6 @@ twoway! ("key" => key); // Disabled because we can't handle comments on exprs yet twoway! ("multirsx" => multirsx); + +// Disabled because we can't handle comments on exprs yet +twoway! ("commentshard" => commentshard); diff --git a/packages/autofmt/tests/samples/commentshard.rsx b/packages/autofmt/tests/samples/commentshard.rsx new file mode 100644 index 000000000..f6c2fc19e --- /dev/null +++ b/packages/autofmt/tests/samples/commentshard.rsx @@ -0,0 +1,42 @@ +rsx! { + // Comments + div { + // Comments + class: "asdasd", + + // Comments + "hello world" + + // Comments + expr1, + + // Comments + expr2, + + // Comments + // Comments + // Comments + // Comments + // Comments + expr3, + + div { + // todo some work in here + } + + div { + // todo some work in here + // todo some work in here + // + // todo some work in here + } + + div { + // todo some work in here + class: "hello world", + + // todo some work in here + class: "hello world" + } + } +} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index e8dd1e1ef..1f8a236de 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core-macro" -version = "0.2.1" +version = "0.3.0" authors = ["Jonathan Kelley"] edition = "2021" description = "Core macro for Dioxus Virtual DOM" @@ -18,7 +18,7 @@ proc-macro = true proc-macro2 = { version = "1.0" } quote = "1.0" syn = { version = "1.0", features = ["full", "extra-traits"] } -dioxus-rsx = { path = "../rsx", version = "0.0.1" } +dioxus-rsx = { path = "../rsx", version = "0.0.2" } # testing [dev-dependencies] diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 47652e262..8f9b71035 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core" -version = "0.2.1" +version = "0.3.0" authors = ["Jonathan Kelley"] edition = "2018" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" @@ -38,7 +38,7 @@ log = "0.4.17" serde = { version = "1", features = ["derive"], optional = true } [dev-dependencies] -tokio = { version = "*", features = ["full"] } +tokio = { version = "1", features = ["full"] } dioxus = { path = "../dioxus" } pretty_assertions = "1.3.0" diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 36323730a..a11fc69ba 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -153,11 +153,17 @@ impl VirtualDom { }); // Now that all the references are gone, we can safely drop our own references in our listeners. - let mut listeners = scope.listeners.borrow_mut(); + let mut listeners = scope.attributes_to_drop.borrow_mut(); listeners.drain(..).for_each(|listener| { let listener = unsafe { &*listener }; - if let AttributeValue::Listener(l) = &listener.value { - _ = l.take(); + match &listener.value { + AttributeValue::Listener(l) => { + _ = l.take(); + } + AttributeValue::Any(a) => { + _ = a.take(); + } + _ => (), } }); } diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs index 44ea4f2d6..0fe7b3867 100644 --- a/packages/core/src/bump_frame.rs +++ b/packages/core/src/bump_frame.rs @@ -1,9 +1,9 @@ use crate::nodes::RenderReturn; use bumpalo::Bump; -use std::cell::Cell; +use std::cell::{Cell, UnsafeCell}; pub(crate) struct BumpFrame { - pub bump: Bump, + pub bump: UnsafeCell, pub node: Cell<*const RenderReturn<'static>>, } @@ -11,7 +11,7 @@ impl BumpFrame { pub(crate) fn new(capacity: usize) -> Self { let bump = Bump::with_capacity(capacity); Self { - bump, + bump: UnsafeCell::new(bump), node: Cell::new(std::ptr::null()), } } @@ -26,4 +26,13 @@ impl BumpFrame { unsafe { std::mem::transmute(&*node) } } + + pub(crate) fn bump(&self) -> &Bump { + unsafe { &*self.bump.get() } + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn bump_mut(&self) -> &mut Bump { + unsafe { &mut *self.bump.get() } + } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index f54684410..8e434d535 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,5 +1,5 @@ use crate::any_props::AnyProps; -use crate::innerlude::{VComponent, VPlaceholder, VText}; +use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText}; use crate::mutations::Mutation; use crate::mutations::Mutation::*; use crate::nodes::VNode; @@ -81,9 +81,7 @@ impl<'b> VirtualDom { // The best renderers will have templates prehydrated and registered // Just in case, let's create the template using instructions anyways - if !self.templates.contains_key(&node.template.get().name) { - self.register_template(node.template.get()); - } + self.register_template(node.template.get()); // we know that this will generate at least one mutation per node self.mutations @@ -285,7 +283,7 @@ impl<'b> VirtualDom { } } - fn write_attribute(&mut self, attribute: &crate::Attribute, id: ElementId) { + fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) { // Make sure we set the attribute's associated id attribute.mounted_element.set(id); @@ -293,9 +291,17 @@ impl<'b> VirtualDom { let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) }; match &attribute.value { - AttributeValue::Text(value) => { + AttributeValue::Listener(_) => { + self.mutations.push(NewEventListener { + // all listeners start with "on" + name: &unbounded_name[2..], + id, + }) + } + _ => { // Safety: we promise not to re-alias this text later on after committing it to the mutation - let unbounded_value: &str = unsafe { std::mem::transmute(*value) }; + let value: BorrowedAttributeValue<'b> = (&attribute.value).into(); + let unbounded_value = unsafe { std::mem::transmute(value) }; self.mutations.push(SetAttribute { name: unbounded_name, @@ -304,22 +310,6 @@ impl<'b> VirtualDom { id, }) } - AttributeValue::Bool(value) => self.mutations.push(SetBoolAttribute { - name: unbounded_name, - value: *value, - id, - }), - AttributeValue::Listener(_) => { - self.mutations.push(NewEventListener { - // all listeners start with "on" - name: &unbounded_name[2..], - id, - }) - } - AttributeValue::Float(_) => todo!(), - AttributeValue::Int(_) => todo!(), - AttributeValue::Any(_) => todo!(), - AttributeValue::None => todo!(), } } @@ -397,30 +387,36 @@ impl<'b> VirtualDom { /// Insert a new template into the VirtualDom's template registry pub(crate) fn register_template(&mut self, mut template: Template<'static>) { - // First, make sure we mark the template as seen, regardless if we process it let (path, byte_index) = template.name.rsplit_once(':').unwrap(); let byte_index = byte_index.parse::().unwrap(); - - // if hot reloading is enabled, then we need to check for a template that has overriten this one - #[cfg(debug_assertions)] - if let Some(mut new_template) = self + // First, check if we've already seen this template + if self .templates - .get_mut(path) - .and_then(|map| map.remove(&usize::MAX)) + .get(&path) + .filter(|set| set.contains_key(&byte_index)) + .is_none() { - // the byte index of the hot reloaded template could be different - new_template.name = template.name; - template = new_template; - } + // if hot reloading is enabled, then we need to check for a template that has overriten this one + #[cfg(debug_assertions)] + if let Some(mut new_template) = self + .templates + .get_mut(path) + .and_then(|map| map.remove(&usize::MAX)) + { + // the byte index of the hot reloaded template could be different + new_template.name = template.name; + template = new_template; + } - self.templates - .entry(path) - .or_default() - .insert(byte_index, template); + self.templates + .entry(path) + .or_default() + .insert(byte_index, template); - // If it's all dynamic nodes, then we don't need to register it - if !template.is_completely_dynamic() { - self.mutations.templates.push(template); + // If it's all dynamic nodes, then we don't need to register it + if !template.is_completely_dynamic() { + self.mutations.templates.push(template); + } } } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index efd534fe7..7227ee6e7 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,7 +1,7 @@ use crate::{ any_props::AnyProps, arena::ElementId, - innerlude::{DirtyScope, VComponent, VPlaceholder, VText}, + innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText}, mutations::Mutation, nodes::RenderReturn, nodes::{DynamicNode, VNode}, @@ -150,22 +150,16 @@ impl<'b> VirtualDom { }; } - fn update_attribute(&mut self, right_attr: &Attribute, left_attr: &Attribute) { - // todo: add more types of attribute values - match right_attr.value { - AttributeValue::Text(text) => { - let name = unsafe { std::mem::transmute(left_attr.name) }; - let value = unsafe { std::mem::transmute(text) }; - self.mutations.push(Mutation::SetAttribute { - id: left_attr.mounted_element.get(), - ns: right_attr.namespace, - name, - value, - }); - } - // todo: more types of attribute values - _ => todo!("other attribute types"), - } + fn update_attribute(&mut self, right_attr: &'b Attribute<'b>, left_attr: &'b Attribute) { + let name = unsafe { std::mem::transmute(left_attr.name) }; + let value: BorrowedAttributeValue<'b> = (&right_attr.value).into(); + let value = unsafe { std::mem::transmute(value) }; + self.mutations.push(Mutation::SetAttribute { + id: left_attr.mounted_element.get(), + ns: right_attr.namespace, + name, + value, + }); } fn diff_vcomponent( diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 9ab01e4d9..849dd4d24 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -23,7 +23,7 @@ use std::{ pub struct Event { /// The data associated with this event pub data: Rc, - pub(crate) propogates: Rc>, + pub(crate) propagates: Rc>, } impl Event { @@ -40,9 +40,9 @@ impl Event { /// } /// } /// ``` - #[deprecated = "use stop_propogation instead"] + #[deprecated = "use stop_propagation instead"] pub fn cancel_bubble(&self) { - self.propogates.set(false); + self.propagates.set(false); } /// Prevent this event from continuing to bubble up the tree to parent elements. @@ -58,8 +58,8 @@ impl Event { /// } /// } /// ``` - pub fn stop_propogation(&self) { - self.propogates.set(false); + pub fn stop_propagation(&self) { + self.propagates.set(false); } /// Get a reference to the inner data from this event @@ -84,7 +84,7 @@ impl Event { impl Clone for Event { fn clone(&self) -> Self { Self { - propogates: self.propogates.clone(), + propagates: self.propagates.clone(), data: self.data.clone(), } } @@ -100,7 +100,7 @@ impl std::ops::Deref for Event { impl std::fmt::Debug for Event { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("UiEvent") - .field("bubble_state", &self.propogates) + .field("bubble_state", &self.propagates) .field("data", &self.data) .finish() } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index f13c20b43..c4f162580 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -70,10 +70,10 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - fc_to_builder, Attribute, AttributeValue, CapturedError, Component, DynamicNode, Element, - ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, - RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, - TemplateAttribute, TemplateNode, VComponent, VNode, VText, VirtualDom, + fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError, + Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, + Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext, + TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VText, VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types @@ -81,9 +81,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties, - Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, - Throw, VNode, VirtualDom, + fc_to_builder, AnyValue, Component, Element, Event, EventHandler, Fragment, + IntoAttributeValue, LazyNodes, Properties, 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 f8553cd46..551069c45 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,6 +1,6 @@ use rustc_hash::FxHashSet; -use crate::{arena::ElementId, ScopeId, Template}; +use crate::{arena::ElementId, innerlude::BorrowedAttributeValue, ScopeId, Template}; /// A container for all the relevant steps to modify the Real DOM /// @@ -61,7 +61,7 @@ impl<'a> Mutations<'a> { derive(serde::Serialize, serde::Deserialize), serde(tag = "type") )] -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq)] pub enum Mutation<'a> { /// Add these m children to the target element AppendChildren { @@ -193,8 +193,9 @@ pub enum Mutation<'a> { SetAttribute { /// The name of the attribute to set. name: &'a str, + /// The value of the attribute. - value: &'a str, + value: BorrowedAttributeValue<'a>, /// The ID of the node to set the attribute of. id: ElementId, @@ -204,18 +205,6 @@ pub enum Mutation<'a> { ns: Option<&'a str>, }, - /// Set the value of a node's attribute. - SetBoolAttribute { - /// The name of the attribute to set. - name: &'a str, - - /// The value of the attribute. - value: bool, - - /// The ID of the node to set the attribute of. - id: ElementId, - }, - /// Set the textcontent of a node. SetText { /// The textcontent of the node diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index dee2f32b3..dd7e245ba 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -6,7 +6,7 @@ use bumpalo::Bump; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell, UnsafeCell}, - fmt::Arguments, + fmt::{Arguments, Debug}, future::Future, }; @@ -523,13 +523,108 @@ pub enum AttributeValue<'a> { Listener(RefCell>>), /// An arbitrary value that implements PartialEq and is static - Any(BumpBox<'a, dyn AnyValue>), + Any(RefCell>>), /// A "none" value, resulting in the removal of an attribute from the dom None, } -type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event) + 'a>; +pub type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event) + 'a>; + +/// Any of the built-in values that the Dioxus VirtualDom supports as dynamic attributes on elements that are borrowed +/// +/// These varients are used to communicate what the value of an attribute is that needs to be updated +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", serde(untagged))] +pub enum BorrowedAttributeValue<'a> { + /// Text attribute + Text(&'a str), + + /// A float + Float(f64), + + /// Signed integer + Int(i64), + + /// Boolean + Bool(bool), + + /// An arbitrary value that implements PartialEq and is static + #[cfg_attr( + feature = "serialize", + serde( + deserialize_with = "deserialize_any_value", + serialize_with = "serialize_any_value" + ) + )] + Any(std::cell::Ref<'a, dyn AnyValue>), + + /// A "none" value, resulting in the removal of an attribute from the dom + None, +} + +impl<'a> From<&'a AttributeValue<'a>> for BorrowedAttributeValue<'a> { + fn from(value: &'a AttributeValue<'a>) -> Self { + match value { + AttributeValue::Text(value) => BorrowedAttributeValue::Text(value), + AttributeValue::Float(value) => BorrowedAttributeValue::Float(*value), + AttributeValue::Int(value) => BorrowedAttributeValue::Int(*value), + AttributeValue::Bool(value) => BorrowedAttributeValue::Bool(*value), + AttributeValue::Listener(_) => { + panic!("A listener cannot be turned into a borrowed value") + } + AttributeValue::Any(value) => { + let value = value.borrow(); + BorrowedAttributeValue::Any(std::cell::Ref::map(value, |value| { + &**value.as_ref().unwrap() + })) + } + AttributeValue::None => BorrowedAttributeValue::None, + } + } +} + +impl Debug for BorrowedAttributeValue<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Text(arg0) => f.debug_tuple("Text").field(arg0).finish(), + Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(), + Self::Int(arg0) => f.debug_tuple("Int").field(arg0).finish(), + Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(), + Self::Any(_) => f.debug_tuple("Any").field(&"...").finish(), + Self::None => write!(f, "None"), + } + } +} + +impl PartialEq for BorrowedAttributeValue<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Text(l0), Self::Text(r0)) => l0 == r0, + (Self::Float(l0), Self::Float(r0)) => l0 == r0, + (Self::Int(l0), Self::Int(r0)) => l0 == r0, + (Self::Bool(l0), Self::Bool(r0)) => l0 == r0, + (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(&**r0), + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + +#[cfg(feature = "serialize")] +fn serialize_any_value(_: &std::cell::Ref<'_, dyn AnyValue>, _: S) -> Result +where + S: serde::Serializer, +{ + panic!("Any cannot be serialized") +} + +#[cfg(feature = "serialize")] +fn deserialize_any_value<'de, 'a, D>(_: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + panic!("Any cannot be deserialized") +} impl<'a> std::fmt::Debug for AttributeValue<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -553,29 +648,36 @@ impl<'a> PartialEq for AttributeValue<'a> { (Self::Int(l0), Self::Int(r0)) => l0 == r0, (Self::Bool(l0), Self::Bool(r0)) => l0 == r0, (Self::Listener(_), Self::Listener(_)) => true, - (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(r0.as_ref()), - _ => core::mem::discriminant(self) == core::mem::discriminant(other), + (Self::Any(l0), Self::Any(r0)) => { + let l0 = l0.borrow(); + let r0 = r0.borrow(); + l0.as_ref().unwrap().any_cmp(&**r0.as_ref().unwrap()) + } + _ => false, } } } #[doc(hidden)] -pub trait AnyValue { +pub trait AnyValue: 'static { fn any_cmp(&self, other: &dyn AnyValue) -> bool; - fn our_typeid(&self) -> TypeId; + fn as_any(&self) -> &dyn Any; + fn type_id(&self) -> TypeId { + self.as_any().type_id() + } } -impl AnyValue for T { +impl AnyValue for T { fn any_cmp(&self, other: &dyn AnyValue) -> bool { - if self.type_id() != other.our_typeid() { - return false; + if let Some(other) = other.as_any().downcast_ref() { + self == other + } else { + false } - - self == unsafe { &*(other as *const _ as *const T) } } - fn our_typeid(&self) -> TypeId { - self.type_id() + fn as_any(&self) -> &dyn Any { + self } } @@ -717,7 +819,6 @@ impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> { } // Note that we're using the E as a generic but this is never crafted anyways. -#[doc(hidden)] pub struct FromNodeIterator; impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T where @@ -742,26 +843,36 @@ pub trait IntoAttributeValue<'a> { fn into_value(self, bump: &'a Bump) -> AttributeValue<'a>; } +impl<'a> IntoAttributeValue<'a> for AttributeValue<'a> { + fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { + self + } +} + impl<'a> IntoAttributeValue<'a> for &'a str { fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { AttributeValue::Text(self) } } + impl<'a> IntoAttributeValue<'a> for f64 { fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { AttributeValue::Float(self) } } + impl<'a> IntoAttributeValue<'a> for i64 { fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { AttributeValue::Int(self) } } + impl<'a> IntoAttributeValue<'a> for bool { fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { AttributeValue::Bool(self) } } + impl<'a> IntoAttributeValue<'a> for Arguments<'_> { fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> { use bumpalo::core_alloc::fmt::Write; @@ -770,3 +881,18 @@ impl<'a> IntoAttributeValue<'a> for Arguments<'_> { AttributeValue::Text(str_buf.into_bump_str()) } } + +impl<'a> IntoAttributeValue<'a> for BumpBox<'a, dyn AnyValue> { + fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Any(RefCell::new(Some(self))) + } +} + +impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option { + fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> { + match self { + Some(val) => val.into_value(bump), + None => AttributeValue::None, + } + } +} diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index 39c770a06..997953f8e 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -59,9 +59,9 @@ impl Scheduler { /// Drop the future with the given TaskId /// - /// This does nto abort the task, so you'll want to wrap it in an aborthandle if that's important to you + /// 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().remove(id.0); + self.tasks.borrow_mut().try_remove(id.0); } } diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 9268cfeb0..ac7cbcacc 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -31,10 +31,11 @@ impl VirtualDom { // If the task completes... if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { // Remove it from the scope so we dont try to double drop it when the scope dropes - self.scopes[task.scope.0].spawned_tasks.remove(&id); + let scope = &self.scopes[task.scope.0]; + scope.spawned_tasks.borrow_mut().remove(&id); // Remove it from the scheduler - tasks.remove(id.0); + tasks.try_remove(id.0); } } @@ -63,10 +64,10 @@ impl VirtualDom { if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { let fiber = self.acquire_suspense_boundary(leaf.scope_id); - let scope = &mut self.scopes[scope_id.0]; + let scope = &self.scopes[scope_id.0]; let arena = scope.current_frame(); - let ret = arena.bump.alloc(match new_nodes { + let ret = arena.bump().alloc(match new_nodes { Some(new) => RenderReturn::Ready(new), None => RenderReturn::default(), }); diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 8d3de2b45..41656fe9d 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -7,7 +7,6 @@ use crate::{ scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; -use bumpalo::Bump; use futures_util::FutureExt; use std::{ mem, @@ -44,15 +43,14 @@ impl VirtualDom { hook_idx: Default::default(), shared_contexts: Default::default(), borrowed_props: Default::default(), - listeners: Default::default(), + attributes_to_drop: Default::default(), })) } - fn acquire_current_scope_raw(&mut self) -> Option<*mut ScopeState> { - self.scope_stack - .last() - .copied() - .and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _)) + fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> { + let id = self.scope_stack.last().copied()?; + let scope = self.scopes.get(id.0)?; + Some(scope.as_ref()) } pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { @@ -62,17 +60,10 @@ impl VirtualDom { self.ensure_drop_safety(scope_id); let mut new_nodes = unsafe { - let scope = self.scopes[scope_id.0].as_mut(); + self.scopes[scope_id.0].previous_frame().bump_mut().reset(); - // if this frame hasn't been intialized yet, we can guess the size of the next frame to be more efficient - if scope.previous_frame().bump.allocated_bytes() == 0 { - scope.previous_frame_mut().bump = - Bump::with_capacity(scope.current_frame().bump.allocated_bytes()); - } else { - scope.previous_frame_mut().bump.reset(); - } + let scope = &self.scopes[scope_id.0]; - // Make sure to reset the hook counter so we give out hooks in the right order scope.hook_idx.set(0); // safety: due to how we traverse the tree, we know that the scope is not currently aliased @@ -142,7 +133,7 @@ impl VirtualDom { let frame = scope.previous_frame(); // set the new head of the bump frame - let allocated = &*frame.bump.alloc(new_nodes); + let allocated = &*frame.bump().alloc(new_nodes); frame.node.set(allocated); // And move the render generation forward by one diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index f6c81db28..2f18d14bd 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -7,7 +7,7 @@ use crate::{ innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, lazynodes::LazyNodes, nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn}, - Attribute, AttributeValue, Element, Event, Properties, TaskId, + AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId, }; use bumpalo::{boxed::Box as BumpBox, Bump}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -73,7 +73,7 @@ pub struct ScopeState { pub(crate) node_arena_1: BumpFrame, pub(crate) node_arena_2: BumpFrame, - pub(crate) parent: Option<*mut ScopeState>, + pub(crate) parent: Option<*const ScopeState>, pub(crate) id: ScopeId, pub(crate) height: u32, @@ -85,10 +85,10 @@ pub struct ScopeState { pub(crate) shared_contexts: RefCell>>, pub(crate) tasks: Rc, - pub(crate) spawned_tasks: FxHashSet, + pub(crate) spawned_tasks: RefCell>, pub(crate) borrowed_props: RefCell>>, - pub(crate) listeners: RefCell>>, + pub(crate) attributes_to_drop: RefCell>>, pub(crate) props: Option>>, pub(crate) placeholder: Cell>, @@ -111,14 +111,6 @@ impl<'src> ScopeState { } } - pub(crate) fn previous_frame_mut(&mut self) -> &mut BumpFrame { - match self.render_cnt.get() % 2 { - 1 => &mut self.node_arena_1, - 0 => &mut self.node_arena_2, - _ => unreachable!(), - } - } - /// Get the name of this component pub fn name(&self) -> &str { self.name @@ -139,7 +131,7 @@ impl<'src> ScopeState { /// If you need to allocate items that need to be dropped, use bumpalo's box. pub fn bump(&self) -> &Bump { // note that this is actually the previous frame since we use that as scratch space while the component is rendering - &self.previous_frame().bump + self.previous_frame().bump() } /// Get a handle to the currently active head node arena for this Scope @@ -374,11 +366,15 @@ impl<'src> ScopeState { pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> { let element = rsx.call(self); - let mut listeners = self.listeners.borrow_mut(); + let mut listeners = self.attributes_to_drop.borrow_mut(); for attr in element.dynamic_attrs { - if let AttributeValue::Listener(_) = attr.value { - let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; - listeners.push(unbounded); + match attr.value { + AttributeValue::Any(_) | AttributeValue::Listener(_) => { + let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; + listeners.push(unbounded); + } + + _ => (), } } @@ -499,7 +495,7 @@ impl<'src> ScopeState { BumpBox::from_raw(self.bump().alloc(move |event: Event| { if let Ok(data) = event.data.downcast::() { callback(Event { - propogates: event.propogates, + propagates: event.propagates, data, }) } @@ -509,6 +505,17 @@ impl<'src> ScopeState { AttributeValue::Listener(RefCell::new(Some(boxed))) } + /// Create a new [`AttributeValue`] with a value that implements [`AnyValue`] + pub fn any_value(&'src self, value: T) -> AttributeValue<'src> { + // safety: there's no other way to create a dynamicly-dispatched bump box other than alloc + from-raw + // This is the suggested way to build a bumpbox + // + // In theory, we could just use regular boxes + let boxed: BumpBox<'src, dyn AnyValue> = + unsafe { BumpBox::from_raw(self.bump().alloc(value)) }; + 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 @@ -541,7 +548,7 @@ impl<'src> ScopeState { #[allow(clippy::mut_from_ref)] pub fn use_hook(&self, initializer: impl FnOnce() -> State) -> &mut State { let cur_hook = self.hook_idx.get(); - let mut hook_list = self.hook_list.borrow_mut(); + let mut hook_list = self.hook_list.try_borrow_mut().expect("The hook list is already borrowed: This error is likely caused by trying to use a hook inside a hook which violates the rules of hooks."); if cur_hook >= hook_list.len() { hook_list.push(self.hook_arena.alloc(initializer())); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 47cbe7245..7369386b5 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -347,7 +347,7 @@ impl VirtualDom { // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required let uievent = Event { - propogates: Rc::new(Cell::new(bubbles)), + propagates: Rc::new(Cell::new(bubbles)), data, }; @@ -388,7 +388,7 @@ impl VirtualDom { cb(uievent.clone()); } - if !uievent.propogates.get() { + if !uievent.propagates.get() { return; } } @@ -549,7 +549,7 @@ impl VirtualDom { loop { // first, unload any complete suspense trees for finished_fiber in self.finished_fibers.drain(..) { - let scope = &mut self.scopes[finished_fiber.0]; + let scope = &self.scopes[finished_fiber.0]; let context = scope.has_context::>().unwrap(); self.mutations @@ -641,10 +641,7 @@ impl VirtualDom { /// Swap the current mutations with a new fn finalize(&mut self) -> Mutations { - // todo: make this a routine - let mut out = Mutations::default(); - std::mem::swap(&mut self.mutations, &mut out); - out + std::mem::take(&mut self.mutations) } } diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs index 8889f77c1..2e3cf5d17 100644 --- a/packages/core/tests/attr_cleanup.rs +++ b/packages/core/tests/attr_cleanup.rs @@ -2,8 +2,10 @@ //! //! This tests to ensure we clean it up +use bumpalo::Bump; use dioxus::core::{ElementId, Mutation::*}; use dioxus::prelude::*; +use dioxus_core::{AttributeValue, BorrowedAttributeValue}; #[test] fn attrs_cycle() { @@ -22,6 +24,8 @@ fn attrs_cycle() { } }); + let bump = Bump::new(); + assert_eq!( dom.rebuild().santize().edits, [ @@ -36,8 +40,18 @@ fn attrs_cycle() { [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, AssignId { path: &[0,], id: ElementId(3,) }, - SetAttribute { name: "class", value: "1", id: ElementId(3,), ns: None }, - SetAttribute { name: "id", value: "1", id: ElementId(3,), ns: None }, + SetAttribute { + name: "class", + value: (&*bump.alloc("1".into_value(&bump))).into(), + id: ElementId(3,), + ns: None + }, + SetAttribute { + name: "id", + value: (&*bump.alloc("1".into_value(&bump))).into(), + id: ElementId(3,), + ns: None + }, ReplaceWith { id: ElementId(1,), m: 1 }, ] ); @@ -57,8 +71,18 @@ fn attrs_cycle() { [ LoadTemplate { name: "template", index: 0, id: ElementId(2) }, AssignId { path: &[0], id: ElementId(3) }, - SetAttribute { name: "class", value: "3", id: ElementId(3), ns: None }, - SetAttribute { name: "id", value: "3", id: ElementId(3), ns: None }, + SetAttribute { + name: "class", + value: BorrowedAttributeValue::Text("3"), + id: ElementId(3), + ns: None + }, + SetAttribute { + name: "id", + value: BorrowedAttributeValue::Text("3"), + id: ElementId(3), + ns: None + }, ReplaceWith { id: ElementId(1), m: 1 } ] ); diff --git a/packages/core/tests/boolattrs.rs b/packages/core/tests/boolattrs.rs index 5cd2ad1ad..0d02bf8af 100644 --- a/packages/core/tests/boolattrs.rs +++ b/packages/core/tests/boolattrs.rs @@ -1,15 +1,23 @@ +use bumpalo::Bump; use dioxus::core::{ElementId, Mutation::*}; use dioxus::prelude::*; #[test] fn bool_test() { let mut app = VirtualDom::new(|cx| cx.render(rsx!(div { hidden: false }))); + let bump = Bump::new(); + assert_eq!( app.rebuild().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, - SetBoolAttribute { name: "hidden", value: false, id: ElementId(1,) }, + SetAttribute { + name: "hidden", + value: (&*bump.alloc(false.into_value(&bump))).into(), + id: ElementId(1,), + ns: None + }, AppendChildren { m: 1, id: ElementId(0) }, ] - ) + ); } diff --git a/packages/core/tests/borrowedstate.rs b/packages/core/tests/borrowedstate.rs index 0aace9c97..1b83789ec 100644 --- a/packages/core/tests/borrowedstate.rs +++ b/packages/core/tests/borrowedstate.rs @@ -18,7 +18,7 @@ fn test_borrowed_state() { ReplacePlaceholder { path: &[0,], m: 1 }, AppendChildren { m: 1, id: ElementId(0) }, ] - ) + ); } fn Parent(cx: Scope) -> Element { diff --git a/packages/core/tests/bubble_error.rs b/packages/core/tests/bubble_error.rs index 9c165c52f..7d6966c46 100644 --- a/packages/core/tests/bubble_error.rs +++ b/packages/core/tests/bubble_error.rs @@ -20,7 +20,9 @@ fn app(cx: Scope) -> Element { fn bubbles_error() { let mut dom = VirtualDom::new(app); - let _edits = dom.rebuild().santize(); + { + let _edits = dom.rebuild().santize(); + } dom.mark_dirty(ScopeId(0)); diff --git a/packages/core/tests/cycle.rs b/packages/core/tests/cycle.rs index 133b04658..10498f5cc 100644 --- a/packages/core/tests/cycle.rs +++ b/packages/core/tests/cycle.rs @@ -12,14 +12,16 @@ fn cycling_elements() { }) }); - let edits = dom.rebuild().santize(); - assert_eq!( - edits.edits, - [ - LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, - AppendChildren { m: 1, id: ElementId(0) }, - ] - ); + { + let edits = dom.rebuild().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + AppendChildren { m: 1, id: ElementId(0) }, + ] + ); + } dom.mark_dirty(ScopeId(0)); assert_eq!( diff --git a/packages/core/tests/diff_component.rs b/packages/core/tests/diff_component.rs index 2b74f8302..69ab6de6d 100644 --- a/packages/core/tests/diff_component.rs +++ b/packages/core/tests/diff_component.rs @@ -59,19 +59,21 @@ fn component_swap() { } let mut dom = VirtualDom::new(app); - let edits = dom.rebuild().santize(); - assert_eq!( - edits.edits, - [ - LoadTemplate { name: "template", index: 0, id: ElementId(1) }, - LoadTemplate { name: "template", index: 0, id: ElementId(2) }, - LoadTemplate { name: "template", index: 0, id: ElementId(3) }, - LoadTemplate { name: "template", index: 0, id: ElementId(4) }, - ReplacePlaceholder { path: &[1], m: 3 }, - LoadTemplate { name: "template", index: 0, id: ElementId(5) }, - AppendChildren { m: 2, id: ElementId(0) } - ] - ); + { + let edits = dom.rebuild().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + ReplacePlaceholder { path: &[1], m: 3 }, + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + AppendChildren { m: 2, id: ElementId(0) } + ] + ); + } dom.mark_dirty(ScopeId(0)); assert_eq!( diff --git a/packages/core/tests/diff_keyed_list.rs b/packages/core/tests/diff_keyed_list.rs index 81013849d..be26e00c0 100644 --- a/packages/core/tests/diff_keyed_list.rs +++ b/packages/core/tests/diff_keyed_list.rs @@ -20,22 +20,24 @@ fn keyed_diffing_out_of_order() { cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) }); - assert_eq!( - dom.rebuild().santize().edits, - [ - LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(3,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(4,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(5,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(6,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(7,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(8,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(9,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(10,) }, - AppendChildren { m: 10, id: ElementId(0) }, - ] - ); + { + assert_eq!( + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(3,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(5,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(6,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(7,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(8,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(9,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(10,) }, + AppendChildren { m: 10, id: ElementId(0) }, + ] + ); + } dom.mark_dirty(ScopeId(0)); assert_eq!( @@ -44,7 +46,7 @@ fn keyed_diffing_out_of_order() { PushRoot { id: ElementId(7,) }, InsertBefore { id: ElementId(5,), m: 1 }, ] - ) + ); } /// Should result in moves only @@ -70,7 +72,7 @@ fn keyed_diffing_out_of_order_adds() { PushRoot { id: ElementId(4,) }, InsertBefore { id: ElementId(1,), m: 2 }, ] - ) + ); } /// Should result in moves only diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs index 09bf3b215..4934f6080 100644 --- a/packages/core/tests/diff_unkeyed_list.rs +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -315,66 +315,76 @@ fn remove_many() { }) }); - let edits = dom.rebuild().santize(); - assert!(edits.templates.is_empty()); - assert_eq!( - edits.edits, - [ - CreatePlaceholder { id: ElementId(1,) }, - AppendChildren { id: ElementId(0), m: 1 }, - ] - ); + { + let edits = dom.rebuild().santize(); + assert!(edits.templates.is_empty()); + assert_eq!( + edits.edits, + [ + CreatePlaceholder { id: ElementId(1,) }, + AppendChildren { id: ElementId(0), m: 1 }, + ] + ); + } - dom.mark_dirty(ScopeId(0)); - let edits = dom.render_immediate().santize(); - assert_eq!( - edits.edits, - [ - LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, - HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) }, - ReplaceWith { id: ElementId(1,), m: 1 }, - ] - ); + { + dom.mark_dirty(ScopeId(0)); + let edits = dom.render_immediate().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); + } - dom.mark_dirty(ScopeId(0)); - let edits = dom.render_immediate().santize(); - assert_eq!( - edits.edits, - [ - LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, - HydrateText { path: &[0,], value: "hello 1", id: ElementId(4,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(5,) }, - HydrateText { path: &[0,], value: "hello 2", id: ElementId(6,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(7,) }, - HydrateText { path: &[0,], value: "hello 3", id: ElementId(8,) }, - LoadTemplate { name: "template", index: 0, id: ElementId(9,) }, - HydrateText { path: &[0,], value: "hello 4", id: ElementId(10,) }, - InsertAfter { id: ElementId(2,), m: 4 }, - ] - ); + { + dom.mark_dirty(ScopeId(0)); + let edits = dom.render_immediate().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + HydrateText { path: &[0,], value: "hello 1", id: ElementId(4,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(5,) }, + HydrateText { path: &[0,], value: "hello 2", id: ElementId(6,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(7,) }, + HydrateText { path: &[0,], value: "hello 3", id: ElementId(8,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(9,) }, + HydrateText { path: &[0,], value: "hello 4", id: ElementId(10,) }, + InsertAfter { id: ElementId(2,), m: 4 }, + ] + ); + } - dom.mark_dirty(ScopeId(0)); - let edits = dom.render_immediate().santize(); - assert_eq!( - edits.edits, - [ - CreatePlaceholder { id: ElementId(11,) }, - Remove { id: ElementId(9,) }, - Remove { id: ElementId(7,) }, - Remove { id: ElementId(5,) }, - Remove { id: ElementId(1,) }, - ReplaceWith { id: ElementId(2,), m: 1 }, - ] - ); + { + dom.mark_dirty(ScopeId(0)); + let edits = dom.render_immediate().santize(); + assert_eq!( + edits.edits, + [ + CreatePlaceholder { id: ElementId(11,) }, + Remove { id: ElementId(9,) }, + Remove { id: ElementId(7,) }, + Remove { id: ElementId(5,) }, + Remove { id: ElementId(1,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, + ] + ); + } - dom.mark_dirty(ScopeId(0)); - let edits = dom.render_immediate().santize(); - assert_eq!( - edits.edits, - [ - LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, - HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) }, - ReplaceWith { id: ElementId(11,), m: 1 }, - ] - ) + { + dom.mark_dirty(ScopeId(0)); + let edits = dom.render_immediate().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) }, + ReplaceWith { id: ElementId(11,), m: 1 }, + ] + ) + } } diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index fca6fcbaa..191d1da7c 100644 --- a/packages/core/tests/kitchen_sink.rs +++ b/packages/core/tests/kitchen_sink.rs @@ -1,3 +1,4 @@ +use bumpalo::Bump; use dioxus::core::{ElementId, Mutation}; use dioxus::prelude::*; @@ -26,17 +27,22 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element { #[test] fn dual_stream() { let mut dom = VirtualDom::new(basic_syntax_is_a_template); + let bump = Bump::new(); let edits = dom.rebuild().santize(); use Mutation::*; - assert_eq!( - edits.edits, + assert_eq!(edits.edits, { [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, - SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None }, + SetAttribute { + name: "class", + value: (&*bump.alloc("123".into_value(&bump))).into(), + id: ElementId(1), + ns: None, + }, NewEventListener { name: "click", id: ElementId(1) }, HydrateText { path: &[0, 0], value: "123", id: ElementId(2) }, - AppendChildren { id: ElementId(0), m: 1 } - ], - ); + AppendChildren { id: ElementId(0), m: 1 }, + ] + }); } diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 9fac13937..06bb7b1a4 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -5,40 +5,48 @@ use std::future::IntoFuture; use std::rc::Rc; use std::time::Duration; -#[tokio::test] -async fn it_works() { - let mut dom = VirtualDom::new(app); - - let mutations = dom.rebuild().santize(); - - // We should at least get the top-level template in before pausing for the children - // note: we dont test template edits anymore - // assert_eq!( - // mutations.templates, - // [ - // CreateElement { name: "div" }, - // CreateStaticText { value: "Waiting for child..." }, - // CreateStaticPlaceholder, - // AppendChildren { m: 2 }, - // SaveTemplate { name: "template", m: 1 } - // ] - // ); - - // And we should load it in and assign the placeholder properly - assert_eq!( - mutations.edits, - [ - LoadTemplate { name: "template", index: 0, id: ElementId(1) }, - // hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly? - // can we even? - AssignId { path: &[1], id: ElementId(3) }, - AppendChildren { m: 1, id: ElementId(0) }, - ] - ); - +#[test] +fn it_works() { // wait just a moment, not enough time for the boundary to resolve - dom.wait_for_work().await; + tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap() + .block_on(async { + let mut dom = VirtualDom::new(app); + + { + let mutations = dom.rebuild().santize(); + + // We should at least get the top-level template in before pausing for the children + // note: we dont test template edits anymore + // assert_eq!( + // mutations.templates, + // [ + // CreateElement { name: "div" }, + // CreateStaticText { value: "Waiting for child..." }, + // CreateStaticPlaceholder, + // AppendChildren { m: 2 }, + // SaveTemplate { name: "template", m: 1 } + // ] + // ); + + // And we should load it in and assign the placeholder properly + assert_eq!( + mutations.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + // hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly? + // can we even? + AssignId { path: &[1], id: ElementId(3) }, + AppendChildren { m: 1, id: ElementId(0) }, + ] + ); + } + + dom.wait_for_work().await; + }); } fn app(cx: Scope) -> Element { diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 8e8cc1a90..5f1ea27d7 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-desktop" -version = "0.2.3" +version = "0.3.0" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for a remote webview instance" @@ -12,9 +12,9 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] } -dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" } -dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1" } +dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] } +dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" } +dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" } serde = "1.0.136" serde_json = "1.0.79" diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index cb5a62635..d3b9216b0 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,14 +1,22 @@ +use std::cell::RefCell; use std::rc::Rc; +use std::rc::Weak; +use crate::create_new_window; use crate::eval::EvalResult; use crate::events::IpcMessage; +use crate::Config; +use crate::WebviewHandler; use dioxus_core::ScopeState; +use dioxus_core::VirtualDom; use serde_json::Value; use wry::application::event_loop::EventLoopProxy; +use wry::application::event_loop::EventLoopWindowTarget; #[cfg(target_os = "ios")] use wry::application::platform::ios::WindowExtIOS; use wry::application::window::Fullscreen as WryFullscreen; use wry::application::window::Window; +use wry::application::window::WindowId; use wry::webview::WebView; pub type ProxyType = EventLoopProxy; @@ -20,6 +28,8 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { .unwrap() } +pub(crate) type WebviewQueue = Rc>>; + /// An imperative interface to the current window. /// /// To get a handle to the current window, use the [`use_window`] hook. @@ -43,6 +53,10 @@ pub struct DesktopContext { /// The receiver for eval results since eval is async pub(super) eval: tokio::sync::broadcast::Sender, + pub(super) pending_windows: WebviewQueue, + + pub(crate) event_loop: EventLoopWindowTarget, + #[cfg(target_os = "ios")] pub(crate) views: Rc>>, } @@ -57,16 +71,56 @@ impl std::ops::Deref for DesktopContext { } impl DesktopContext { - pub(crate) fn new(webview: Rc, proxy: ProxyType) -> Self { + pub(crate) fn new( + webview: Rc, + proxy: ProxyType, + event_loop: EventLoopWindowTarget, + webviews: WebviewQueue, + ) -> Self { Self { webview, proxy, + event_loop, eval: tokio::sync::broadcast::channel(8).0, + pending_windows: webviews, #[cfg(target_os = "ios")] views: Default::default(), } } + /// Create a new window using the props and window builder + /// + /// Returns the webview handle for the new window. + /// + /// You can use this to control other windows from the current window. + /// + /// Be careful to not create a cycle of windows, or you might leak memory. + pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak { + let window = create_new_window( + cfg, + &self.event_loop, + &self.proxy, + dom, + &self.pending_windows, + ); + + let id = window.webview.window().id(); + + self.proxy + .send_event(UserWindowEvent(EventData::NewWindow, id)) + .unwrap(); + + self.proxy + .send_event(UserWindowEvent(EventData::Poll, id)) + .unwrap(); + + let webview = window.webview.clone(); + + self.pending_windows.borrow_mut().push(window); + + Rc::downgrade(&webview) + } + /// trigger the drag-window event /// /// Moves the window with the left mouse button until the button is released. @@ -79,7 +133,9 @@ impl DesktopContext { let window = self.webview.window(); // if the drag_window has any errors, we don't do anything - window.fullscreen().is_none().then(|| window.drag_window()); + if window.fullscreen().is_none() { + window.drag_window().unwrap(); + } } /// Toggle whether the window is maximized or not @@ -91,7 +147,16 @@ impl DesktopContext { /// close window pub fn close(&self) { - let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); + let _ = self + .proxy + .send_event(UserWindowEvent(EventData::CloseWindow, self.id())); + } + + /// close window + pub fn close_window(&self, id: WindowId) { + let _ = self + .proxy + .send_event(UserWindowEvent(EventData::CloseWindow, id)); } /// change window to fullscreen @@ -188,12 +253,17 @@ impl DesktopContext { } } -#[derive(Debug)] -pub enum UserWindowEvent { +#[derive(Debug, Clone)] +pub struct UserWindowEvent(pub EventData, pub WindowId); + +#[derive(Debug, Clone)] +pub enum EventData { Poll, Ipc(IpcMessage), + NewWindow, + CloseWindow, } diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 50d3fbf3d..7059a7c85 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct IpcMessage { method: String, params: serde_json::Value, diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index e3002f539..a75e6bbbb 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -16,8 +16,8 @@ mod webview; mod hot_reload; pub use cfg::Config; -use desktop_context::UserWindowEvent; pub use desktop_context::{use_window, DesktopContext}; +use desktop_context::{EventData, UserWindowEvent, WebviewQueue}; use dioxus_core::*; use dioxus_html::HtmlEvent; pub use eval::{use_eval, EvalResult}; @@ -26,6 +26,7 @@ use std::collections::HashMap; use std::rc::Rc; use std::task::Waker; pub use tao::dpi::{LogicalSize, PhysicalSize}; +use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use tao::window::WindowBuilder; use tao::{ event::{Event, StartCause, WindowEvent}, @@ -33,6 +34,8 @@ use tao::{ }; pub use wry; pub use wry::application as tao; +use wry::application::window::WindowId; +use wry::webview::WebView; /// Launch the WebView and run the event loop. /// @@ -101,10 +104,8 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// }) /// } /// ``` -pub fn launch_with_props(root: Component

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

, props: P, cfg: Config) { + let event_loop = EventLoop::::with_user_event(); let proxy = event_loop.create_proxy(); @@ -118,23 +119,35 @@ pub fn launch_with_props(root: Component

, props: P, mut cfg: Conf // We enter the runtime but we poll futures manually, circumventing the per-task runtime budget let _guard = rt.enter(); - // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both - let waker = waker::tao_waker(&proxy); - // We only have one webview right now, but we'll have more later // Store them in a hashmap so we can remove them when they're closed - let mut webviews = HashMap::new(); + let mut webviews = HashMap::::new(); - event_loop.run(move |window_event, event_loop, control_flow| { + let queue = WebviewQueue::default(); + + // By default, we'll create a new window when the app starts + queue.borrow_mut().push(create_new_window( + cfg, + &event_loop, + &proxy, + VirtualDom::new_with_props(root, props), + &queue, + )); + + event_loop.run(move |window_event, _event_loop, control_flow| { *control_flow = ControlFlow::Wait; match window_event { - Event::UserEvent(UserWindowEvent::CloseWindow) => *control_flow = ControlFlow::Exit, - Event::WindowEvent { event, window_id, .. } => match event { - WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::CloseRequested => { + webviews.remove(&window_id); + + if webviews.is_empty() { + *control_flow = ControlFlow::Exit + } + } WindowEvent::Destroyed { .. } => { webviews.remove(&window_id); @@ -145,78 +158,125 @@ pub fn launch_with_props(root: Component

, props: P, mut cfg: Conf _ => {} }, - Event::NewEvents(StartCause::Init) => { - let window = webview::build(&mut cfg, event_loop, proxy.clone()); - - dom.base_scope() - .provide_context(DesktopContext::new(window.clone(), proxy.clone())); - - webviews.insert(window.window().id(), window); - - _ = proxy.send_event(UserWindowEvent::Poll); - } - - Event::UserEvent(UserWindowEvent::Poll) => { - poll_vdom(&waker, &mut dom, &mut webviews); - } - - Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "user_event" => { - let evt = match serde_json::from_value::(msg.params()) { - Ok(value) => value, - Err(_) => return, - }; - - dom.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles); - - send_edits(dom.render_immediate(), &mut webviews); - } - - Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "initialize" => { - send_edits(dom.rebuild(), &mut webviews); - } - - // When the webview chirps back with the result of the eval, we send it to the active receiver - // - // This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once, - // you might the wrong result. This should be fixed - Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "eval_result" => { - dom.base_scope() - .consume_context::() - .unwrap() - .eval - .send(msg.params()) - .unwrap(); - } - - Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "browser_open" => { - if let Some(temp) = msg.params().as_object() { - if temp.contains_key("href") { - let open = webbrowser::open(temp["href"].as_str().unwrap()); - if let Err(e) = open { - log::error!("Open Browser error: {:?}", e); - } - } + Event::NewEvents(StartCause::Init) + | Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => { + for handler in queue.borrow_mut().drain(..) { + let id = handler.webview.window().id(); + webviews.insert(id, handler); + _ = proxy.send_event(UserWindowEvent(EventData::Poll, id)); } } + Event::UserEvent(event) => match event.0 { + EventData::CloseWindow => { + webviews.remove(&event.1); + + if webviews.is_empty() { + *control_flow = ControlFlow::Exit + } + } + + EventData::Poll => { + if let Some(view) = webviews.get_mut(&event.1) { + poll_vdom(view); + } + } + + EventData::Ipc(msg) if msg.method() == "user_event" => { + let evt = match serde_json::from_value::(msg.params()) { + Ok(value) => value, + Err(_) => return, + }; + + let view = webviews.get_mut(&event.1).unwrap(); + + view.dom + .handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles); + + send_edits(view.dom.render_immediate(), &view.webview); + } + + EventData::Ipc(msg) if msg.method() == "initialize" => { + let view = webviews.get_mut(&event.1).unwrap(); + send_edits(view.dom.rebuild(), &view.webview); + } + + // When the webview chirps back with the result of the eval, we send it to the active receiver + // + // This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once, + // you might the wrong result. This should be fixed + EventData::Ipc(msg) if msg.method() == "eval_result" => { + webviews[&event.1] + .dom + .base_scope() + .consume_context::() + .unwrap() + .eval + .send(msg.params()) + .unwrap(); + } + + EventData::Ipc(msg) if msg.method() == "browser_open" => { + if let Some(temp) = msg.params().as_object() { + if temp.contains_key("href") { + let open = webbrowser::open(temp["href"].as_str().unwrap()); + if let Err(e) = open { + log::error!("Open Browser error: {:?}", e); + } + } + } + } + + _ => {} + }, _ => {} } }) } -type Webviews = HashMap>; +fn create_new_window( + mut cfg: Config, + event_loop: &EventLoopWindowTarget, + proxy: &EventLoopProxy, + dom: VirtualDom, + queue: &WebviewQueue, +) -> WebviewHandler { + let webview = webview::build(&mut cfg, event_loop, proxy.clone()); + + dom.base_scope().provide_context(DesktopContext::new( + webview.clone(), + proxy.clone(), + event_loop.clone(), + queue.clone(), + )); + + let id = webview.window().id(); + + // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both + WebviewHandler { + webview, + dom, + waker: waker::tao_waker(proxy, id), + } +} + +struct WebviewHandler { + dom: VirtualDom, + webview: Rc, + waker: Waker, +} /// Poll the virtualdom until it's pending /// /// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again /// /// All IO is done on the tokio runtime we started earlier -fn poll_vdom(waker: &Waker, dom: &mut VirtualDom, webviews: &mut Webviews) { - let mut cx = std::task::Context::from_waker(waker); +fn poll_vdom(view: &mut WebviewHandler) { + let mut cx = std::task::Context::from_waker(&view.waker); loop { { - let fut = dom.wait_for_work(); + let fut = view.dom.wait_for_work(); pin_mut!(fut); match fut.poll_unpin(&mut cx) { @@ -225,16 +285,14 @@ fn poll_vdom(waker: &Waker, dom: &mut VirtualDom, webviews: &mut Webviews) { } } - send_edits(dom.render_immediate(), webviews); + send_edits(view.dom.render_immediate(), &view.webview); } } /// Send a list of mutations to the webview -fn send_edits(edits: Mutations, webviews: &mut Webviews) { +fn send_edits(edits: Mutations, webview: &WebView) { let serialized = serde_json::to_string(&edits).unwrap(); - let (_id, view) = webviews.iter_mut().next().unwrap(); - // todo: use SSE and binary data to send the edits with lower overhead - _ = view.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized)); + _ = webview.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized)); } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 8835078f2..067684cd2 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -25,73 +25,62 @@ fn module_loader(root_name: &str) -> String { pub(super) fn desktop_handler( request: &Request>, - asset_root: Option, custom_head: Option, custom_index: Option, root_name: &str, ) -> Result>> { - // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case". - // For now, we only serve two pieces of content which get included as bytes into the final binary. - let path = request.uri().to_string().replace("dioxus://", ""); - - // all assets should be called from index.html - let trimmed = path.trim_start_matches("index.html/"); - - if trimmed.is_empty() { + // 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. - if let Some(custom_index) = custom_index { - let rendered = custom_index + let body = match custom_index { + Some(custom_index) => custom_index .replace("", &format!("{}", module_loader(root_name))) - .into_bytes(); - Response::builder() - .header("Content-Type", "text/html") - .body(rendered) - .map_err(From::from) - } else { - // Otherwise, we'll serve the default index.html and apply a custom head if that's specified. - let mut template = include_str!("./index.html").to_string(); - if let Some(custom_head) = custom_head { - template = template.replace("", &custom_head); + .into_bytes(), + + None => { + // Otherwise, we'll serve the default index.html and apply a custom head if that's specified. + let mut template = include_str!("./index.html").to_string(); + + if let Some(custom_head) = custom_head { + template = template.replace("", &custom_head); + } + + template + .replace("", &module_loader(root_name)) + .into_bytes() } - template = template.replace("", &module_loader(root_name)); + }; - Response::builder() - .header("Content-Type", "text/html") - .body(template.into_bytes()) - .map_err(From::from) - } - } else if trimmed == "index.js" { - Response::builder() - .header("Content-Type", "text/javascript") - .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec()) - .map_err(From::from) - } else { - let asset_root = asset_root - .unwrap_or_else(|| get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf())) - .canonicalize()?; - - let asset = asset_root.join(trimmed).canonicalize()?; - - if !asset.starts_with(asset_root) { - return Response::builder() - .status(StatusCode::FORBIDDEN) - .body(String::from("Forbidden").into_bytes()) - .map_err(From::from); - } - - if !asset.exists() { - return Response::builder() - .status(StatusCode::NOT_FOUND) - .body(String::from("Not Found").into_bytes()) - .map_err(From::from); - } - - Response::builder() - .header("Content-Type", get_mime_from_path(trimmed)?) - .body(std::fs::read(asset)?) - .map_err(From::from) + return Response::builder() + .header("Content-Type", "text/html") + .body(body) + .map_err(From::from); } + + // Else, try to serve a file from the filesystem. + let path = PathBuf::from(request.uri().path().trim_start_matches('/')); + + // 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); + + if !asset.exists() { + asset = PathBuf::from("/").join(path); + } + + if asset.exists() { + return Response::builder() + .header("Content-Type", get_mime_from_path(&asset)?) + .body(std::fs::read(asset)?) + .map_err(From::from); + } + + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(String::from("Not Found").into_bytes()) + .map_err(From::from) } #[allow(unreachable_code)] @@ -128,7 +117,7 @@ fn get_asset_root() -> Option { } /// Get the mime type from a path-like string -fn get_mime_from_path(trimmed: &str) -> Result<&str> { +fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> { if trimmed.ends_with(".svg") { return Ok("image/svg+xml"); } @@ -143,9 +132,8 @@ fn get_mime_from_path(trimmed: &str) -> Result<&str> { } /// Get the mime type from a URI using its extension -fn get_mime_by_ext(trimmed: &str) -> &str { - let suffix = trimmed.split('.').last(); - match suffix { +fn get_mime_by_ext(trimmed: &Path) -> &'static str { + match trimmed.extension().and_then(|e| e.to_str()) { Some("bin") => "application/octet-stream", Some("css") => "text/css", Some("csv") => "text/csv", diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index a2586ffed..1e34baffc 100644 --- a/packages/desktop/src/waker.rs +++ b/packages/desktop/src/waker.rs @@ -1,15 +1,18 @@ -use crate::desktop_context::UserWindowEvent; +use crate::desktop_context::{EventData, UserWindowEvent}; use futures_util::task::ArcWake; use std::sync::Arc; -use wry::application::event_loop::EventLoopProxy; +use wry::application::{event_loop::EventLoopProxy, window::WindowId}; /// Create a waker that will send a poll event to the event loop. /// /// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView. /// /// All other IO lives in the Tokio runtime, -pub fn tao_waker(proxy: &EventLoopProxy) -> std::task::Waker { - struct DomHandle(EventLoopProxy); +pub fn tao_waker(proxy: &EventLoopProxy, id: WindowId) -> std::task::Waker { + struct DomHandle { + proxy: EventLoopProxy, + id: WindowId, + } // this should be implemented by most platforms, but ios is missing this until // https://github.com/tauri-apps/wry/issues/830 is resolved @@ -18,9 +21,14 @@ pub fn tao_waker(proxy: &EventLoopProxy) -> std::task::Waker { impl ArcWake for DomHandle { fn wake_by_ref(arc_self: &Arc) { - _ = arc_self.0.send_event(UserWindowEvent::Poll); + _ = arc_self + .proxy + .send_event(UserWindowEvent(EventData::Poll, arc_self.id)); } } - futures_util::task::waker(Arc::new(DomHandle(proxy.clone()))) + futures_util::task::waker(Arc::new(DomHandle { + id, + proxy: proxy.clone(), + })) } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 4a270bfab..a8f437614 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,5 +1,6 @@ use std::rc::Rc; +use crate::desktop_context::EventData; use crate::protocol; use crate::{desktop_context::UserWindowEvent, Config}; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; @@ -17,7 +18,6 @@ pub fn build( let window = builder.build(event_loop).unwrap(); let file_handler = cfg.file_drop_handler.take(); let custom_head = cfg.custom_head.clone(); - let resource_dir = cfg.resource_dir.clone(); let index_file = cfg.custom_index.clone(); let root_name = cfg.root_name.clone(); @@ -38,20 +38,14 @@ pub fn build( .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() - .with_ipc_handler(move |_window: &Window, payload: String| { + .with_ipc_handler(move |window: &Window, payload: String| { // defer the event to the main thread if let Ok(message) = serde_json::from_str(&payload) { - _ = proxy.send_event(UserWindowEvent::Ipc(message)); + _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id())); } }) .with_custom_protocol(String::from("dioxus"), move |r| { - protocol::desktop_handler( - r, - resource_dir.clone(), - custom_head.clone(), - index_file.clone(), - &root_name, - ) + protocol::desktop_handler(r, custom_head.clone(), index_file.clone(), &root_name) }) .with_file_drop_handler(move |window, evet| { file_handler diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 801ef7ac5..ceab90a1b 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dioxus" -version = "0.2.4" -authors = ["Jonathan Kelley"] +version = "0.3.1" +authors = ["Jonathan Kelley", "Dioxus Labs", "ealmloff"] edition = "2021" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" license = "MIT OR Apache-2.0" @@ -9,14 +9,14 @@ repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] -rust-version = "1.60.0" +rust-version = "1.65.0" [dependencies] -dioxus-core = { path = "../core", version = "^0.2.1" } -dioxus-html = { path = "../html", version = "^0.2.1", optional = true } -dioxus-core-macro = { path = "../core-macro", version = "^0.2.1", optional = true } -dioxus-hooks = { path = "../hooks", version = "^0.2.1", optional = true } -dioxus-rsx = { path = "../rsx", optional = true } +dioxus-core = { path = "../core", version = "^0.3.0" } +dioxus-html = { path = "../html", version = "^0.3.0", optional = true } +dioxus-core-macro = { path = "../core-macro", version = "^0.3.0", optional = true } +dioxus-hooks = { path = "../hooks", version = "^0.3.0", optional = true } +dioxus-rsx = { path = "../rsx", version = "0.0.2", optional = true } [features] default = ["macro", "hooks", "html"] diff --git a/notes/README.md b/packages/dioxus/README.md similarity index 100% rename from notes/README.md rename to packages/dioxus/README.md diff --git a/packages/fermi/Cargo.toml b/packages/fermi/Cargo.toml index 05ae2ec21..5d0222599 100644 --- a/packages/fermi/Cargo.toml +++ b/packages/fermi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fermi" -version = "0.2.1" +version = "0.3.0" authors = ["Jonathan Kelley"] edition = "2018" description = "Global state management for Dioxus" @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version = "^0.2.1" } +dioxus-core = { path = "../core", version = "^0.3.0" } im-rc = { version = "15.0.0", features = ["serde"] } log = "0.4.14" diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index a9cfa340d..e040272b7 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-hooks" -version = "0.2.1" +version = "0.3.0" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for a remote webview instance" @@ -12,11 +12,11 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../../packages/core", version = "^0.2.1" } +dioxus-core = { path = "../../packages/core", version = "^0.3.0" } futures-channel = "0.3.21" log = "0.4" [dev-dependencies] futures-util = { version = "0.3", default-features = false } -dioxus-core = { path = "../../packages/core", version = "^0.2.1" } +dioxus-core = { path = "../../packages/core", version = "^0.3.0" } diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index da221554c..1865dadb3 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -170,7 +170,7 @@ impl UseRef { /// Set the curernt value to `new_value`. This will mark the component as "dirty" /// - /// This change will propogate immediately, so any other contexts that are + /// This change will propagate immediately, so any other contexts that are /// using this RefCell will also be affected. If called during an async context, /// the component will not be re-rendered until the next `.await` call. pub fn set(&self, new: T) { diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index de5db6645..1256a27cc 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-html" -version = "0.2.1" +version = "0.3.0" authors = ["Jonathan Kelley"] edition = "2018" description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" @@ -11,8 +11,8 @@ documentation = "https://docs.rs/dioxus" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version = "^0.2.1" } -dioxus-rsx = { path = "../rsx", optional = true } +dioxus-core = { path = "../core", version = "^0.3.0" } +dioxus-rsx = { path = "../rsx", version = "0.0.2", optional = true } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } wasm-bindgen = { version = "0.2.79", optional = true } @@ -42,10 +42,10 @@ features = [ ] [dev-dependencies] -serde_json = "*" +serde_json = "1" [features] default = ["serialize"] serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"] wasm-bind = ["web-sys", "wasm-bindgen"] -hot-reload-context = ["dioxus-rsx"] \ No newline at end of file +hot-reload-context = ["dioxus-rsx"] diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 931bc3d60..6cf60c6ea 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1222,7 +1222,7 @@ builder_constructors! { rows: usize DEFAULT, spellcheck: BoolOrDefault DEFAULT, wrap: Wrap DEFAULT, - value: Strign volatile, + value: String volatile, }; diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 5f0e5b2a3..196099444 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -4,6 +4,7 @@ use crate::events::{ }; use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; +use crate::DragData; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; use std::str::FromStr; @@ -40,6 +41,7 @@ uncheck_convert![ CompositionEvent => CompositionData, KeyboardEvent => KeyboardData, MouseEvent => MouseData, + MouseEvent => DragData, TouchEvent => TouchData, PointerEvent => PointerData, WheelEvent => WheelData, @@ -117,6 +119,14 @@ impl From<&MouseEvent> for MouseData { } } +impl From<&MouseEvent> for DragData { + fn from(value: &MouseEvent) -> Self { + Self { + mouse: MouseData::from(value), + } + } +} + impl From<&TouchEvent> for TouchData { fn from(e: &TouchEvent) -> Self { Self { diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 6fcf19f27..a10377f34 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-interpreter-js" -version = "0.2.1" +version = "0.3.0" edition = "2018" authors = ["Jonathan Kelley"] description = "JS Intepreter for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index e8cc4d039..77949a764 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -144,8 +144,13 @@ class Interpreter { this.nodes[root].textContent = text; } SetAttribute(id, field, value, ns) { - const node = this.nodes[id]; - this.SetAttributeInner(node, field, value, ns); + if (value === null) { + this.RemoveAttribute(id, field, ns); + } + else { + const node = this.nodes[id]; + this.SetAttributeInner(node, field, value, ns); + } } SetAttributeInner(node, field, value, ns) { const name = field; @@ -221,7 +226,6 @@ class Interpreter { } MakeTemplateNode(node) { - console.log("making template node", node); switch (node.type) { case "Text": return document.createTextNode(node.text); @@ -334,9 +338,6 @@ class Interpreter { case "SetAttribute": this.SetAttribute(edit.id, edit.name, edit.value, edit.ns); break; - case "SetBoolAttribute": - this.SetAttribute(edit.id, edit.name, edit.value, edit.ns); - break; case "RemoveAttribute": this.RemoveAttribute(edit.id, edit.name, edit.ns); break; diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 99e43da08..0fc26674c 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-liveview" -version = "0.1.0" +version = "0.3.0" edition = "2021" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" @@ -25,9 +25,9 @@ serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" tokio-util = { version = "0.7.4", features = ["full"] } -dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" } -dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" } -dioxus-interpreter-js = { path = "../interpreter" } +dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" } +dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" } +dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" } # warp warp = { version = "0.3.3", optional = true } @@ -49,7 +49,7 @@ anyhow = "1.0.68" [dev-dependencies] tokio = { version = "1.23.0", features = ["full"] } -dioxus = { path = "../dioxus" } +dioxus = { path = "../dioxus", version = "0.3.0" } warp = "0.3.3" axum = { version = "0.6.1", features = ["ws"] } salvo = { version = "0.37.7", features = ["affix", "ws"] } diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index bdf656b50..7dd9758d6 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -12,7 +12,13 @@ class IPC { let ws = new WebSocket(WS_ADDR); + function ping() { + ws.send("__ping__"); + } + ws.onopen = () => { + // we ping every 30 seconds to keep the websocket alive + setInterval(ping, 30000); ws.send(serializeIpcMessage("initialize")); }; @@ -21,8 +27,11 @@ class IPC { }; ws.onmessage = (event) => { - let edits = JSON.parse(event.data); - window.interpreter.handleEdits(edits); + // Ignore pongs + if (event.data != "__pong__") { + let edits = JSON.parse(event.data); + window.interpreter.handleEdits(edits); + } }; this.ws = ws; diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index a25673d84..a20ad029e 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -127,9 +127,13 @@ where _ = vdom.wait_for_work() => {} evt = ws.next() => { - match evt { + match evt.as_ref().map(|o| o.as_deref()) { + // respond with a pong every ping to keep the websocket alive + Some(Ok("__ping__")) => { + ws.send("__pong__".to_string()).await?; + } Some(Ok(evt)) => { - if let Ok(IpcMessage { params }) = serde_json::from_str::(&evt) { + if let Ok(IpcMessage { params }) = serde_json::from_str::(evt) { vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles); } } diff --git a/packages/mobile/Cargo.toml b/packages/mobile/Cargo.toml index 1308c9f23..df23c7c4a 100644 --- a/packages/mobile/Cargo.toml +++ b/packages/mobile/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-mobile" -version = "0.2.0" +version = "0.3.0" authors = ["Jonathan Kelley"] edition = "2018" description = "Mobile-compatible renderer for Dioxus" @@ -12,7 +12,7 @@ license = "MIT/Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-desktop = { path = "../desktop", version = "^0.2.3" } +dioxus-desktop = { path = "../desktop", version = "^0.3.0" } [lib] doctest = false diff --git a/packages/native-core-macro/Cargo.toml b/packages/native-core-macro/Cargo.toml index bff83ec42..759d8ff00 100644 --- a/packages/native-core-macro/Cargo.toml +++ b/packages/native-core-macro/Cargo.toml @@ -18,10 +18,9 @@ proc-macro = true syn = { version = "1.0.11", features = ["extra-traits"] } quote = "1.0" dioxus-native-core = { path = "../native-core", version = "^0.2.0" } - -[dev-dependencies] dioxus = { path = "../dioxus" } +[dev-dependencies] smallvec = "1.6" rustc-hash = "1.1.0" anymap = "0.12.1" diff --git a/packages/native-core-macro/src/lib.rs b/packages/native-core-macro/src/lib.rs index 5bf50afcc..29ee4b055 100644 --- a/packages/native-core-macro/src/lib.rs +++ b/packages/native-core-macro/src/lib.rs @@ -124,6 +124,17 @@ enum DependencyKind { /// # The state attribute /// The state macro declares a member that implements the State trait. This allows you to organize your state into multiple isolated components. /// Unlike the other attributes, the state attribute does not accept any arguments, because a nested state cannot depend on any other part of the state. +/// +/// # Custom values +/// +/// If your state has a custom value type you can specify it with the state attribute. +/// +/// ```rust, ignore +/// #[derive(State)] +/// #[state(custom_value = MyCustomType)] +/// struct MyStruct { +/// // ... +/// } #[proc_macro_derive( State, attributes(node_dep_state, child_dep_state, parent_dep_state, state) @@ -134,6 +145,33 @@ pub fn state_macro_derive(input: TokenStream) -> TokenStream { } fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream { + let custom_type = ast + .attrs + .iter() + .find(|a| a.path.is_ident("state")) + .and_then(|attr| { + // parse custom_type = "MyType" + let assignment = attr.parse_args::().unwrap(); + if let syn::Expr::Assign(assign) = assignment { + let (left, right) = (&*assign.left, &*assign.right); + if let syn::Expr::Path(e) = left { + let path = &e.path; + if let Some(ident) = path.get_ident() { + if ident == "custom_value" { + return match right { + syn::Expr::Path(e) => { + let path = &e.path; + Some(quote! {#path}) + } + _ => None, + }; + } + } + } + } + None + }) + .unwrap_or(quote! {()}); let type_name = &ast.ident; let fields: Vec<_> = match &ast.data { syn::Data::Struct(data) => match &data.fields { @@ -162,12 +200,12 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream { let impl_members = state_strct .state_members .iter() - .map(|m| m.impl_pass(state_strct.ty)); + .map(|m| m.impl_pass(state_strct.ty, &custom_type)); let gen = quote! { #(#impl_members)* - impl State for #type_name { - const PASSES: &'static [dioxus_native_core::AnyPass>] = &[ + impl State<#custom_type> for #type_name { + const PASSES: &'static [dioxus_native_core::AnyPass>] = &[ #(#passes),* ]; const MASKS: &'static [dioxus_native_core::NodeMask] = &[#(#member_types::NODE_MASK),*]; @@ -378,7 +416,11 @@ impl<'a> StateMember<'a> { } /// generate code to call the resolve function for the state. This does not handle checking if resolving the state is necessary, or marking the states that depend on this state as dirty. - fn impl_pass(&self, parent_type: &Ident) -> quote::__private::TokenStream { + fn impl_pass( + &self, + parent_type: &Ident, + custom_type: impl ToTokens, + ) -> quote::__private::TokenStream { let ident = &self.mem.ident; let get_ctx = if let Some(ctx_ty) = &self.ctx_ty { if ctx_ty == &parse_quote!(()) { @@ -393,13 +435,14 @@ impl<'a> StateMember<'a> { let ty = &self.mem.ty; let unit_type = &self.mem.unit_type; - let node_view = quote!(dioxus_native_core::node_ref::NodeView::new(unsafe{&*{&node.node_data as *const _}}, #ty::NODE_MASK)); + let node_view = + quote!(dioxus_native_core::node_ref::NodeView::new(&node.node_data, #ty::NODE_MASK)); let dep_idents = self.dep_mems.iter().map(|m| &m.ident); let impl_specific = match self.dep_kind { DependencyKind::Node => { quote! { - impl dioxus_native_core::NodePass> for #unit_type { - fn pass(&self, node: &mut dioxus_native_core::node::Node<#parent_type>, ctx: &dioxus_native_core::SendAnyMap) -> bool { + impl dioxus_native_core::NodePass> for #unit_type { + fn pass(&self, node: &mut dioxus_native_core::node::Node<#parent_type, #custom_type>, ctx: &dioxus_native_core::SendAnyMap) -> bool { node.state.#ident.reduce(#node_view, (#(&node.state.#dep_idents,)*), #get_ctx) } } @@ -436,11 +479,11 @@ impl<'a> StateMember<'a> { } }; quote!( - impl dioxus_native_core::UpwardPass> for #unit_type{ + impl dioxus_native_core::UpwardPass> for #unit_type{ fn pass<'a>( &self, - node: &mut dioxus_native_core::node::Node<#parent_type>, - children: &mut dyn Iterator>, + node: &mut dioxus_native_core::node::Node<#parent_type, #custom_type>, + children: &mut dyn Iterator>, ctx: &dioxus_native_core::SendAnyMap, ) -> dioxus_native_core::PassReturn { let update = node.state.#ident.reduce(#node_view, children.map(|c| (#(&c.state.#dep_idents,)*)), #get_ctx); @@ -480,8 +523,8 @@ impl<'a> StateMember<'a> { } }; quote!( - impl dioxus_native_core::DownwardPass> for #unit_type { - fn pass(&self, node: &mut dioxus_native_core::node::Node<#parent_type>, parent: Option<&mut dioxus_native_core::node::Node<#parent_type>>, ctx: &dioxus_native_core::SendAnyMap) -> dioxus_native_core::PassReturn{ + impl dioxus_native_core::DownwardPass> for #unit_type { + fn pass(&self, node: &mut dioxus_native_core::node::Node<#parent_type, #custom_type>, parent: Option<&mut dioxus_native_core::node::Node<#parent_type, #custom_type>>, ctx: &dioxus_native_core::SendAnyMap) -> dioxus_native_core::PassReturn{ let update = node.state.#ident.reduce(#node_view, parent.as_ref().map(|p| (#(&p.state.#dep_idents,)*)), #get_ctx); #update } diff --git a/packages/native-core-macro/tests/update_state.rs b/packages/native-core-macro/tests/update_state.rs index e2f04c72c..fc36c42ed 100644 --- a/packages/native-core-macro/tests/update_state.rs +++ b/packages/native-core-macro/tests/update_state.rs @@ -43,7 +43,6 @@ impl ParentDepState for ParentDepCallCounter { const NODE_MASK: NodeMask = NodeMask::ALL; fn reduce(&mut self, _node: NodeView, _parent: Option<(&Self,)>, _ctx: &Self::Ctx) -> bool { self.0 += 1; - println!("ParentDepCallCounter::reduce on {:?}\n{}", _node, self.0); true } } diff --git a/packages/native-core/Cargo.toml b/packages/native-core/Cargo.toml index 2c09ecf4c..c414258c7 100644 --- a/packages/native-core/Cargo.toml +++ b/packages/native-core/Cargo.toml @@ -11,21 +11,24 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version = "^0.2.1" } -dioxus-html = { path = "../html", version = "^0.2.1" } -dioxus-core-macro = { path = "../core-macro", version = "^0.2.1" } +dioxus-core = { path = "../core", version = "^0.3.0" } +dioxus-html = { path = "../html", version = "^0.3.0" } +dioxus-core-macro = { path = "../core-macro", version = "^0.3.0" } taffy = "0.2.1" smallvec = "1.6" rustc-hash = "1.1.0" -anymap = "0.12.1" +anymap = "1.0.0-beta.2" slab = "0.4" parking_lot = "0.12.1" crossbeam-deque = "0.8.2" dashmap = "5.4.0" +# for parsing attributes +lightningcss = "1.0.0-alpha.39" + [dev-dependencies] rand = "0.8.5" -dioxus = { path = "../dioxus", version = "^0.2.1" } +dioxus = { path = "../dioxus", version = "^0.3.0" } dioxus-native-core-macro = { path = "../native-core-macro" } tokio = { version = "*", features = ["full"] } diff --git a/packages/native-core/src/layout_attributes.rs b/packages/native-core/src/layout_attributes.rs index b59177e4e..d149a11ac 100644 --- a/packages/native-core/src/layout_attributes.rs +++ b/packages/native-core/src/layout_attributes.rs @@ -1,7 +1,7 @@ /* -- [ ] pub display: Display, -- [x] pub position_type: PositionType, --> kinda, taffy doesnt support everything -- [ ] pub direction: Direction, +- [ ] pub display: Display, ----> taffy doesnt support all display types +- [x] pub position_type: PositionType, --> taffy doesnt support everything +- [x] pub direction: Direction, - [x] pub flex_direction: FlexDirection, - [x] pub flex_wrap: FlexWrap, @@ -9,7 +9,7 @@ - [x] pub flex_shrink: f32, - [x] pub flex_basis: Dimension, -- [x] pub overflow: Overflow, ---> kinda implemented... taffy doesnt have support for directional overflow +- [x] pub overflow: Overflow, ---> taffy doesnt have support for directional overflow - [x] pub align_items: AlignItems, - [x] pub align_self: AlignSelf, @@ -19,16 +19,27 @@ - [x] pub padding: Rect, - [x] pub justify_content: JustifyContent, -- [ ] pub position: Rect, +- [x] pub position: Rect, - [x] pub border: Rect, -- [ ] pub size: Size, ----> ??? seems to only be relevant for input? +- [ ] pub size: Size, ----> seems to only be relevant for input? - [ ] pub min_size: Size, - [ ] pub max_size: Size, -- [ ] pub aspect_ratio: Number, +- [ ] pub aspect_ratio: Number, ----> parsing is done, but taffy doesnt support it */ +use lightningcss::properties::{align, display, flex, position, size}; +use lightningcss::{ + properties::{align::GapValue, border::BorderSideWidth, Property, PropertyId}, + stylesheet::ParserOptions, + traits::Parse, + values::{ + length::{Length, LengthPercentageOrAuto, LengthValue}, + percentage::DimensionPercentage, + ratio::Ratio, + }, +}; use taffy::{ prelude::*, style::{FlexDirection, PositionType}, @@ -36,229 +47,311 @@ use taffy::{ /// applies the entire html namespace defined in dioxus-html pub fn apply_layout_attributes(name: &str, value: &str, style: &mut Style) { - match name { - "align-content" - | "align-items" - | "align-self" => apply_align(name, value, style), - - "animation" - | "animation-delay" - | "animation-direction" - | "animation-duration" - | "animation-fill-mode" - | "animation-iteration-count" - | "animation-name" - | "animation-play-state" - | "animation-timing-function" => apply_animation(name, value, style), - - "backface-visibility" => {} - - "border" - | "border-bottom" - | "border-bottom-color" - | "border-bottom-left-radius" - | "border-bottom-right-radius" - | "border-bottom-style" - | "border-bottom-width" - | "border-collapse" - | "border-color" - | "border-image" - | "border-image-outset" - | "border-image-repeat" - | "border-image-slice" - | "border-image-source" - | "border-image-width" - | "border-left" - | "border-left-color" - | "border-left-style" - | "border-left-width" - | "border-radius" - | "border-right" - | "border-right-color" - | "border-right-style" - | "border-right-width" - | "border-spacing" - | "border-style" - | "border-top" - | "border-top-color" - | "border-top-left-radius" - | "border-top-right-radius" - | "border-top-style" - | "border-top-width" - | "border-width" => apply_border(name, value, style), - - "bottom" => {} - "box-shadow" => {} - "box-sizing" => {} - "caption-side" => {} - "clear" => {} - "clip" => {} - - "column-count" - | "column-fill" - | "column-gap" - | "column-rule" - | "column-rule-color" - | "column-rule-style" - | "column-rule-width" - | "column-span" - // add column-width - | "column-width" => apply_column(name, value, style), - - "columns" => {} - - "content" => {} - "counter-increment" => {} - "counter-reset" => {} - - "cursor" => {} - "direction" => {} - - "display" => apply_display(name, value, style), - - "empty-cells" => {} - - "flex" - | "flex-basis" - | "flex-direction" - | "flex-flow" - | "flex-grow" - | "flex-shrink" - | "flex-wrap" => apply_flex(name, value, style), - - "float" => {}, - - "font-style" - | "font-variant" - | "font-weight" - | "font-size" - | "line-height" - | "font-family" => apply_font(name, value, style), - - "height" => { - if let Some(v) = parse_value(value){ - style.size.height = v; + if let Ok(property) = + Property::parse_string(PropertyId::from(name), value, ParserOptions::default()) + { + match property { + Property::Display(display) => match display { + display::Display::Keyword(display::DisplayKeyword::None) => { + style.display = Display::None + } + display::Display::Pair(pair) => { + if let display::DisplayInside::Flex(_) = pair.inside { + style.display = Display::Flex + } + } + _ => (), + }, + Property::Position(position) => { + style.position_type = match position { + position::Position::Relative => PositionType::Relative, + position::Position::Absolute => PositionType::Absolute, + _ => return, + } + } + Property::Top(top) => style.position.top = convert_length_percentage_or_auto(top), + Property::Bottom(bottom) => { + style.position.bottom = convert_length_percentage_or_auto(bottom) + } + Property::Left(left) => style.position.left = convert_length_percentage_or_auto(left), + Property::Right(right) => { + style.position.right = convert_length_percentage_or_auto(right) + } + Property::Inset(inset) => { + style.position.top = convert_length_percentage_or_auto(inset.top); + style.position.bottom = convert_length_percentage_or_auto(inset.bottom); + style.position.left = convert_length_percentage_or_auto(inset.left); + style.position.right = convert_length_percentage_or_auto(inset.right); + } + Property::BorderTopWidth(width) => { + style.border.top = convert_border_side_width(width); + } + Property::BorderBottomWidth(width) => { + style.border.bottom = convert_border_side_width(width); + } + Property::BorderLeftWidth(width) => { + style.border.left = convert_border_side_width(width); + } + Property::BorderRightWidth(width) => { + style.border.right = convert_border_side_width(width); + } + Property::BorderWidth(width) => { + style.border.top = convert_border_side_width(width.top); + style.border.bottom = convert_border_side_width(width.bottom); + style.border.left = convert_border_side_width(width.left); + style.border.right = convert_border_side_width(width.right); + } + Property::Border(border) => { + let width = convert_border_side_width(border.width); + style.border.top = width; + style.border.bottom = width; + style.border.left = width; + style.border.right = width; + } + Property::BorderTop(top) => { + style.border.top = convert_border_side_width(top.width); + } + Property::BorderBottom(bottom) => { + style.border.bottom = convert_border_side_width(bottom.width); + } + Property::BorderLeft(left) => { + style.border.left = convert_border_side_width(left.width); + } + Property::BorderRight(right) => { + style.border.right = convert_border_side_width(right.width); + } + Property::FlexDirection(flex_direction, _) => { + use FlexDirection::*; + style.flex_direction = match flex_direction { + flex::FlexDirection::Row => Row, + flex::FlexDirection::RowReverse => RowReverse, + flex::FlexDirection::Column => Column, + flex::FlexDirection::ColumnReverse => ColumnReverse, + } + } + Property::FlexWrap(wrap, _) => { + use FlexWrap::*; + style.flex_wrap = match wrap { + flex::FlexWrap::NoWrap => NoWrap, + flex::FlexWrap::Wrap => Wrap, + flex::FlexWrap::WrapReverse => WrapReverse, + } + } + Property::FlexGrow(grow, _) => { + style.flex_grow = grow; + } + Property::FlexShrink(shrink, _) => { + style.flex_shrink = shrink; + } + Property::FlexBasis(basis, _) => { + style.flex_basis = convert_length_percentage_or_auto(basis); + } + Property::Flex(flex, _) => { + style.flex_grow = flex.grow; + style.flex_shrink = flex.shrink; + style.flex_basis = convert_length_percentage_or_auto(flex.basis); + } + Property::AlignContent(align, _) => { + use AlignContent::*; + style.align_content = match align { + align::AlignContent::ContentDistribution(distribution) => match distribution { + align::ContentDistribution::SpaceBetween => SpaceBetween, + align::ContentDistribution::SpaceAround => SpaceAround, + align::ContentDistribution::SpaceEvenly => SpaceEvenly, + align::ContentDistribution::Stretch => Stretch, + }, + align::AlignContent::ContentPosition { + value: position, .. + } => match position { + align::ContentPosition::Center => Center, + align::ContentPosition::Start | align::ContentPosition::FlexStart => { + FlexStart + } + align::ContentPosition::End | align::ContentPosition::FlexEnd => FlexEnd, + }, + _ => return, + }; + } + Property::JustifyContent(justify, _) => { + use JustifyContent::*; + style.justify_content = match justify { + align::JustifyContent::ContentDistribution(distribution) => { + match distribution { + align::ContentDistribution::SpaceBetween => SpaceBetween, + align::ContentDistribution::SpaceAround => SpaceAround, + align::ContentDistribution::SpaceEvenly => SpaceEvenly, + _ => return, + } + } + align::JustifyContent::ContentPosition { + value: position, .. + } => match position { + align::ContentPosition::Center => Center, + // start ignores -reverse flex-direction but there is no way to specify that in Taffy + align::ContentPosition::Start | align::ContentPosition::FlexStart => { + FlexStart + } + // end ignores -reverse flex-direction but there is no way to specify that in Taffy + align::ContentPosition::End | align::ContentPosition::FlexEnd => FlexEnd, + }, + _ => return, + }; + } + Property::AlignSelf(align, _) => { + use AlignSelf::*; + style.align_self = match align { + align::AlignSelf::Auto => Auto, + align::AlignSelf::Stretch => Stretch, + align::AlignSelf::BaselinePosition(_) => Baseline, + align::AlignSelf::SelfPosition { + value: position, .. + } => match position { + align::SelfPosition::Center => Center, + align::SelfPosition::Start + | align::SelfPosition::SelfStart + | align::SelfPosition::FlexStart => FlexStart, + align::SelfPosition::End + | align::SelfPosition::SelfEnd + | align::SelfPosition::FlexEnd => FlexEnd, + }, + _ => return, + }; + } + Property::AlignItems(align, _) => { + use AlignItems::*; + style.align_items = match align { + align::AlignItems::BaselinePosition(_) => Baseline, + align::AlignItems::Stretch => Stretch, + align::AlignItems::SelfPosition { + value: position, .. + } => match position { + align::SelfPosition::Center => Center, + align::SelfPosition::FlexStart => FlexStart, + align::SelfPosition::FlexEnd => FlexEnd, + _ => return, + }, + _ => return, + }; + } + Property::RowGap(row_gap) => { + style.gap.width = convert_gap_value(row_gap); + } + Property::ColumnGap(column_gap) => { + style.gap.height = convert_gap_value(column_gap); + } + Property::Gap(gap) => { + style.gap = Size { + width: convert_gap_value(gap.row), + height: convert_gap_value(gap.column), + }; + } + Property::MarginTop(margin) => { + style.margin.top = convert_length_percentage_or_auto(margin); + } + Property::MarginBottom(margin) => { + style.margin.bottom = convert_length_percentage_or_auto(margin); + } + Property::MarginLeft(margin) => { + style.margin.left = convert_length_percentage_or_auto(margin); + } + Property::MarginRight(margin) => { + style.margin.right = convert_length_percentage_or_auto(margin); + } + Property::Margin(margin) => { + style.margin = Rect { + top: convert_length_percentage_or_auto(margin.top), + bottom: convert_length_percentage_or_auto(margin.bottom), + left: convert_length_percentage_or_auto(margin.left), + right: convert_length_percentage_or_auto(margin.right), + }; + } + Property::PaddingTop(padding) => { + style.padding.top = convert_length_percentage_or_auto(padding); + } + Property::PaddingBottom(padding) => { + style.padding.bottom = convert_length_percentage_or_auto(padding); + } + Property::PaddingLeft(padding) => { + style.padding.left = convert_length_percentage_or_auto(padding); + } + Property::PaddingRight(padding) => { + style.padding.right = convert_length_percentage_or_auto(padding); + } + Property::Padding(padding) => { + style.padding = Rect { + top: convert_length_percentage_or_auto(padding.top), + bottom: convert_length_percentage_or_auto(padding.bottom), + left: convert_length_percentage_or_auto(padding.left), + right: convert_length_percentage_or_auto(padding.right), + }; + } + Property::Width(width) => { + style.size.width = convert_size(width); + } + Property::Height(height) => { + style.size.height = convert_size(height); + } + _ => (), + } + // currently not implemented in lightningcss + if name == "aspect-ratio" { + if let Ok(ratio) = Ratio::parse_string(value) { + style.aspect_ratio = Some(ratio.0 / ratio.1); } } - "justify-content" => { - use JustifyContent::*; - style.justify_content = match value { - "flex-start" => FlexStart, - "flex-end" => FlexEnd, - "center" => Center, - "space-between" => SpaceBetween, - "space-around" => SpaceAround, - "space-evenly" => SpaceEvenly, - _ => FlexStart, - }; - } - "left" => {} - "letter-spacing" => {} - - "list-style" - | "list-style-image" - | "list-style-position" - | "list-style-type" => {} - - "margin" - | "margin-bottom" - | "margin-left" - | "margin-right" - | "margin-top" => apply_margin(name, value, style), - - "max-height" => {} - "max-width" => {} - "min-height" => {} - "min-width" => {} - - "opacity" => {} - "order" => {} - "outline" => {} - - "outline-color" - | "outline-offset" - | "outline-style" - | "outline-width" => {} - - "overflow" - | "overflow-x" - | "overflow-y" => apply_overflow(name, value, style), - - "padding" - | "padding-bottom" - | "padding-left" - | "padding-right" - | "padding-top" => apply_padding(name, value, style), - - "page-break-after" - | "page-break-before" - | "page-break-inside" => {} - - "perspective" - | "perspective-origin" => {} - - "position" => { - match value { - "static" => {} - "relative" => style.position_type = PositionType::Relative, - "fixed" => {} - "absolute" => style.position_type = PositionType::Absolute, - "sticky" => {} - _ => {} - } - - } - - "pointer-events" => {} - - "quotes" => {} - "resize" => {} - "right" => {} - "tab-size" => {} - "table-layout" => {} - - "top" => {} - - "transform" - | "transform-origin" - | "transform-style" => apply_transform(name, value, style), - - "transition" - | "transition-delay" - | "transition-duration" - | "transition-property" - | "transition-timing-function" => apply_transition(name, value, style), - - "vertical-align" => {} - "visibility" => {} - "white-space" => {} - "width" => { - if let Some(v) = parse_value(value){ - style.size.width = v; - } - } - "word-break" => {} - "word-spacing" => {} - "word-wrap" => {} - "z-index" => {} - _ => {} } } -fn apply_font(name: &str, value: &str, style: &mut Style) { - match name { - "font-style" => {} - "font-variant" => {} - "font-weight" => {} - "font-size" => {} - "line-height" => { - style.size = Size { - width: style.size.width, - height: parse_value(value).unwrap_or(Dimension::Points(12.0)), - } +fn convert_length_value(length_value: LengthValue) -> Dimension { + match length_value { + LengthValue::Px(value) => Dimension::Points(value), + _ => todo!(), + } +} + +fn convert_dimension_percentage( + dimension_percentage: DimensionPercentage, +) -> Dimension { + match dimension_percentage { + DimensionPercentage::Dimension(value) => convert_length_value(value), + DimensionPercentage::Percentage(percentage) => Dimension::Percent(percentage.0), + _ => todo!(), + } +} + +fn convert_length_percentage_or_auto( + length_percentage_or_auto: LengthPercentageOrAuto, +) -> Dimension { + match length_percentage_or_auto { + LengthPercentageOrAuto::Auto => Dimension::Auto, + LengthPercentageOrAuto::LengthPercentage(percentage) => { + convert_dimension_percentage(percentage) } - "font-family" => {} - _ => {} + } +} + +fn convert_border_side_width(border_side_width: BorderSideWidth) -> Dimension { + match border_side_width { + BorderSideWidth::Length(Length::Value(value)) => convert_length_value(value), + BorderSideWidth::Thick => Dimension::Points(5.0), + BorderSideWidth::Medium => Dimension::Points(3.0), + BorderSideWidth::Thin => Dimension::Points(1.0), + _ => todo!(), + } +} + +fn convert_gap_value(gap_value: GapValue) -> Dimension { + match gap_value { + GapValue::LengthPercentage(dim) => convert_dimension_percentage(dim), + GapValue::Normal => Dimension::Auto, + } +} + +fn convert_size(size: size::Size) -> Dimension { + match size { + size::Size::Auto => Dimension::Auto, + size::Size::LengthPercentage(length) => convert_dimension_percentage(length), + _ => todo!(), } } @@ -280,312 +373,3 @@ pub fn parse_value(value: &str) -> Option { None } } - -fn apply_overflow(_name: &str, _value: &str, _style: &mut Style) { - // todo: add overflow support to taffy -} - -fn apply_display(_name: &str, value: &str, style: &mut Style) { - style.display = match value { - "flex" => Display::Flex, - "block" => Display::None, - _ => Display::Flex, - } - - // TODO: there are way more variants - // taffy needs to be updated to handle them - // - // "block" => Display::Block, - // "inline" => Display::Inline, - // "inline-block" => Display::InlineBlock, - // "inline-table" => Display::InlineTable, - // "list-item" => Display::ListItem, - // "run-in" => Display::RunIn, - // "table" => Display::Table, - // "table-caption" => Display::TableCaption, - // "table-cell" => Display::TableCell, - // "table-column" => Display::TableColumn, - // "table-column-group" => Display::TableColumnGroup, - // "table-footer-group" => Display::TableFooterGroup, - // "table-header-group" => Display::TableHeaderGroup, - // "table-row" => Display::TableRow, - // "table-row-group" => Display::TableRowGroup, - // "none" => Display::None, - // _ => Display::Inline, -} - -fn apply_border(name: &str, value: &str, style: &mut Style) { - match name { - "border" => {} - "border-bottom" => {} - "border-bottom-color" => {} - "border-bottom-left-radius" => {} - "border-bottom-right-radius" => {} - "border-bottom-style" => { - if style.border.bottom == Dimension::default() { - let v = Dimension::Points(1.0); - style.border.bottom = v; - } - } - "border-bottom-width" => { - if let Some(v) = parse_value(value) { - style.border.bottom = v; - } - } - "border-collapse" => {} - "border-color" => {} - "border-image" => {} - "border-image-outset" => {} - "border-image-repeat" => {} - "border-image-slice" => {} - "border-image-source" => {} - "border-image-width" => {} - "border-left" => {} - "border-left-color" => {} - "border-left-style" => { - if style.border.left == Dimension::default() { - let v = Dimension::Points(1.0); - style.border.left = v; - } - } - "border-left-width" => { - if let Some(v) = parse_value(value) { - style.border.left = v; - } - } - "border-radius" => {} - "border-right" => {} - "border-right-color" => {} - "border-right-style" => { - let v = Dimension::Points(1.0); - style.border.right = v; - } - "border-right-width" => { - if let Some(v) = parse_value(value) { - style.border.right = v; - } - } - "border-spacing" => {} - "border-style" => { - if style.border.top == Dimension::default() { - let v = Dimension::Points(1.0); - style.border.top = v; - } - if style.border.bottom == Dimension::default() { - let v = Dimension::Points(1.0); - style.border.bottom = v; - } - if style.border.left == Dimension::default() { - let v = Dimension::Points(1.0); - style.border.left = v; - } - if style.border.right == Dimension::default() { - let v = Dimension::Points(1.0); - style.border.right = v; - } - } - "border-top" => {} - "border-top-color" => {} - "border-top-left-radius" => {} - "border-top-right-radius" => {} - "border-top-style" => { - if style.border.top == Dimension::default() { - let v = Dimension::Points(1.0); - style.border.top = v; - } - } - "border-top-width" => { - if let Some(v) = parse_value(value) { - style.border.top = v; - } - } - "border-width" => { - let values: Vec<_> = value.split(' ').collect(); - if values.len() == 1 { - if let Some(dim) = parse_value(values[0]) { - style.border = Rect { - right: dim, - left: dim, - top: dim, - bottom: dim, - }; - } - } else { - let border_widths = [ - &mut style.border.top, - &mut style.border.bottom, - &mut style.border.left, - &mut style.border.right, - ]; - for (v, width) in values.into_iter().zip(border_widths) { - if let Some(w) = parse_value(v) { - *width = w; - } - } - } - } - _ => (), - } -} - -fn apply_animation(name: &str, _value: &str, _style: &mut Style) { - match name { - "animation" => {} - "animation-delay" => {} - "animation-direction =>{}" => {} - "animation-duration" => {} - "animation-fill-mode" => {} - "animation-itera =>{}tion-count" => {} - "animation-name" => {} - "animation-play-state" => {} - "animation-timing-function" => {} - _ => {} - } -} - -fn apply_column(name: &str, _value: &str, _style: &mut Style) { - match name { - "column-count" => {} - "column-fill" => {} - "column-gap" => {} - "column-rule" => {} - "column-rule-color" => {} - "column-rule-style" => {} - "column-rule-width" => {} - "column-span" => {} - "column-width" => {} - _ => {} - } -} - -fn apply_flex(name: &str, value: &str, style: &mut Style) { - // - [x] pub flex_direction: FlexDirection, - // - [x] pub flex_wrap: FlexWrap, - // - [x] pub flex_grow: f32, - // - [x] pub flex_shrink: f32, - // - [x] pub flex_basis: Dimension, - - match name { - "flex" => {} - "flex-direction" => { - use FlexDirection::*; - style.flex_direction = match value { - "row" => Row, - "row-reverse" => RowReverse, - "column" => Column, - "column-reverse" => ColumnReverse, - _ => Row, - }; - } - "flex-basis" => { - if let Some(v) = parse_value(value) { - style.flex_basis = v; - } - } - "flex-flow" => {} - "flex-grow" => { - if let Ok(val) = value.parse::() { - style.flex_grow = val; - } - } - "flex-shrink" => { - if let Ok(px) = value.parse::() { - style.flex_shrink = px; - } - } - "flex-wrap" => { - use FlexWrap::*; - style.flex_wrap = match value { - "nowrap" => NoWrap, - "wrap" => Wrap, - "wrap-reverse" => WrapReverse, - _ => NoWrap, - }; - } - _ => {} - } -} - -fn apply_padding(name: &str, value: &str, style: &mut Style) { - if let Some(v) = parse_value(value) { - match name { - "padding" => { - style.padding.top = v; - style.padding.bottom = v; - style.padding.left = v; - style.padding.right = v; - } - "padding-bottom" => style.padding.bottom = v, - "padding-left" => style.padding.left = v, - "padding-right" => style.padding.right = v, - "padding-top" => style.padding.top = v, - _ => {} - } - } -} - -fn apply_transform(_name: &str, _value: &str, _style: &mut Style) { - todo!() -} - -fn apply_transition(_name: &str, _value: &str, _style: &mut Style) { - todo!() -} - -fn apply_align(name: &str, value: &str, style: &mut Style) { - match name { - "align-items" => { - use AlignItems::*; - style.align_items = match value { - "flex-start" => FlexStart, - "flex-end" => FlexEnd, - "center" => Center, - "baseline" => Baseline, - "stretch" => Stretch, - _ => FlexStart, - }; - } - "align-content" => { - use AlignContent::*; - style.align_content = match value { - "flex-start" => FlexStart, - "flex-end" => FlexEnd, - "center" => Center, - "space-between" => SpaceBetween, - "space-around" => SpaceAround, - _ => FlexStart, - }; - } - "align-self" => { - use AlignSelf::*; - style.align_self = match value { - "auto" => Auto, - "flex-start" => FlexStart, - "flex-end" => FlexEnd, - "center" => Center, - "baseline" => Baseline, - "stretch" => Stretch, - _ => Auto, - }; - } - _ => {} - } -} - -fn apply_margin(name: &str, value: &str, style: &mut Style) { - if let Some(dim) = parse_value(value) { - match name { - "margin" => { - style.margin.top = dim; - style.margin.bottom = dim; - style.margin.left = dim; - style.margin.right = dim; - } - "margin-top" => style.margin.top = dim, - "margin-bottom" => style.margin.bottom = dim, - "margin-left" => style.margin.left = dim, - "margin-right" => style.margin.right = dim, - _ => {} - } - } -} diff --git a/packages/native-core/src/lib.rs b/packages/native-core/src/lib.rs index ae739af86..1a8bb0ce0 100644 --- a/packages/native-core/src/lib.rs +++ b/packages/native-core/src/lib.rs @@ -1,3 +1,4 @@ +use std::any::Any; use std::hash::BuildHasherDefault; pub use node_ref::NodeMask; @@ -20,7 +21,7 @@ pub mod utils; pub type RealNodeId = NodeId; pub type FxDashMap = dashmap::DashMap>; pub type FxDashSet = dashmap::DashSet>; -pub type SendAnyMap = anymap::Map; +pub type SendAnyMap = anymap::Map; /// Used in derived state macros #[derive(Eq, PartialEq)] diff --git a/packages/native-core/src/node.rs b/packages/native-core/src/node.rs index 4530fd350..cc51d6743 100644 --- a/packages/native-core/src/node.rs +++ b/packages/native-core/src/node.rs @@ -1,43 +1,44 @@ use crate::{state::State, tree::NodeId}; -use dioxus_core::ElementId; +use dioxus_core::{AnyValue, BorrowedAttributeValue, ElementId}; use rustc_hash::{FxHashMap, FxHashSet}; +use std::fmt::Debug; /// The node is stored client side and stores only basic data about the node. #[derive(Debug, Clone)] -pub struct Node { +pub struct Node, V: FromAnyValue + 'static = ()> { /// The transformed state of the node. pub state: S, /// The raw data for the node - pub node_data: NodeData, + pub node_data: NodeData, } #[derive(Debug, Clone)] -pub struct NodeData { +pub struct NodeData { /// The id of the node pub node_id: NodeId, /// The id of the node in the vdom. pub element_id: Option, /// Additional inforation specific to the node type - pub node_type: NodeType, + pub node_type: NodeType, } /// A type of node with data specific to the node type. The types are a subset of the [VNode] types. #[derive(Debug, Clone)] -pub enum NodeType { +pub enum NodeType { Text { text: String, }, Element { tag: String, namespace: Option, - attributes: FxHashMap, + attributes: FxHashMap>, listeners: FxHashSet, }, Placeholder, } -impl Node { - pub(crate) fn new(node_type: NodeType) -> Self { +impl, V: FromAnyValue> Node { + pub(crate) fn new(node_type: NodeType) -> Self { Node { state: S::default(), node_data: NodeData { @@ -64,24 +65,57 @@ pub struct OwnedAttributeDiscription { /// An attribute on a DOM node, such as `id="my-thing"` or /// `href="https://example.com"`. #[derive(Clone, Copy, Debug)] -pub struct OwnedAttributeView<'a> { +pub struct OwnedAttributeView<'a, V: FromAnyValue = ()> { /// The discription of the attribute. pub attribute: &'a OwnedAttributeDiscription, /// The value of the attribute. - pub value: &'a OwnedAttributeValue, + pub value: &'a OwnedAttributeValue, } -#[derive(Clone, Debug)] -pub enum OwnedAttributeValue { +#[derive(Clone)] +pub enum OwnedAttributeValue { Text(String), - Float(f32), - Int(i32), + Float(f64), + Int(i64), Bool(bool), - None, + Custom(V), } -impl OwnedAttributeValue { +pub trait FromAnyValue: Clone { + fn from_any_value(value: &dyn AnyValue) -> Self; +} + +impl FromAnyValue for () { + fn from_any_value(_: &dyn AnyValue) -> Self {} +} + +impl Debug for OwnedAttributeValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Text(arg0) => f.debug_tuple("Text").field(arg0).finish(), + Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(), + Self::Int(arg0) => f.debug_tuple("Int").field(arg0).finish(), + Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(), + Self::Custom(_) => f.debug_tuple("Any").finish(), + } + } +} + +impl From> for OwnedAttributeValue { + fn from(value: BorrowedAttributeValue<'_>) -> Self { + match value { + BorrowedAttributeValue::Text(text) => Self::Text(text.to_string()), + BorrowedAttributeValue::Float(float) => Self::Float(float), + BorrowedAttributeValue::Int(int) => Self::Int(int), + BorrowedAttributeValue::Bool(bool) => Self::Bool(bool), + BorrowedAttributeValue::Any(any) => Self::Custom(V::from_any_value(&*any)), + BorrowedAttributeValue::None => panic!("None attribute values result in removing the attribute, not converting it to a None value.") + } + } +} + +impl OwnedAttributeValue { pub fn as_text(&self) -> Option<&str> { match self { OwnedAttributeValue::Text(text) => Some(text), @@ -89,14 +123,14 @@ impl OwnedAttributeValue { } } - pub fn as_float(&self) -> Option { + pub fn as_float(&self) -> Option { match self { OwnedAttributeValue::Float(float) => Some(*float), _ => None, } } - pub fn as_int(&self) -> Option { + pub fn as_int(&self) -> Option { match self { OwnedAttributeValue::Int(int) => Some(*int), _ => None, @@ -110,9 +144,9 @@ impl OwnedAttributeValue { } } - pub fn as_none(&self) -> Option<()> { + pub fn as_custom(&self) -> Option<&V> { match self { - OwnedAttributeValue::None => Some(()), + OwnedAttributeValue::Custom(custom) => Some(custom), _ => None, } } diff --git a/packages/native-core/src/node_ref.rs b/packages/native-core/src/node_ref.rs index d178c2d18..8d4523efc 100644 --- a/packages/native-core/src/node_ref.rs +++ b/packages/native-core/src/node_ref.rs @@ -1,21 +1,21 @@ use dioxus_core::ElementId; use crate::{ - node::{NodeData, NodeType, OwnedAttributeView}, + node::{FromAnyValue, NodeData, NodeType, OwnedAttributeView}, state::union_ordered_iter, RealNodeId, }; /// A view into a [VNode] with limited access. #[derive(Debug)] -pub struct NodeView<'a> { - inner: &'a NodeData, +pub struct NodeView<'a, V: FromAnyValue = ()> { + inner: &'a NodeData, mask: NodeMask, } -impl<'a> NodeView<'a> { +impl<'a, V: FromAnyValue> NodeView<'a, V> { /// Create a new NodeView from a VNode, and mask. - pub fn new(node: &'a NodeData, view: NodeMask) -> Self { + pub fn new(node: &'a NodeData, view: NodeMask) -> Self { Self { inner: node, mask: view, @@ -55,7 +55,9 @@ impl<'a> NodeView<'a> { } /// Get any attributes that are enabled in the mask - pub fn attributes<'b>(&'b self) -> Option> + 'b> { + pub fn attributes<'b>( + &'b self, + ) -> Option> + 'b> { match &self.inner.node_type { NodeType::Element { attributes, .. } => Some( attributes diff --git a/packages/native-core/src/passes.rs b/packages/native-core/src/passes.rs index bdc7c7e5b..bb0e26e48 100644 --- a/packages/native-core/src/passes.rs +++ b/packages/native-core/src/passes.rs @@ -1,112 +1,100 @@ use crate::tree::{NodeId, TreeView}; -use crate::{FxDashMap, FxDashSet, SendAnyMap}; +use crate::{FxDashSet, SendAnyMap}; use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::BTreeMap; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign}; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct DirtyNodes { - map: BTreeMap>, +#[derive(Default)] +struct DirtyNodes { + passes_dirty: Vec, } impl DirtyNodes { - pub fn insert(&mut self, depth: u16, node_id: NodeId) { - self.map - .entry(depth) - .or_insert_with(FxHashSet::default) - .insert(node_id); - } - - fn pop_front(&mut self) -> Option { - let (&depth, values) = self.map.iter_mut().next()?; - let key = *values.iter().next()?; - let node_id = values.take(&key)?; - if values.is_empty() { - self.map.remove(&depth); + fn add_node(&mut self, node_id: NodeId) { + let node_id = node_id.0; + let index = node_id / 64; + let bit = node_id % 64; + let encoded = 1 << bit; + if let Some(passes) = self.passes_dirty.get_mut(index) { + *passes |= encoded; + } else { + self.passes_dirty.resize(index + 1, 0); + self.passes_dirty[index] |= encoded; } - Some(node_id) } - fn pop_back(&mut self) -> Option { - let (&depth, values) = self.map.iter_mut().rev().next()?; - let key = *values.iter().next()?; - let node_id = values.take(&key)?; - if values.is_empty() { - self.map.remove(&depth); - } - Some(node_id) + fn is_empty(&self) -> bool { + self.passes_dirty.iter().all(|dirty| *dirty == 0) } -} -#[test] -fn dirty_nodes() { - let mut dirty_nodes = DirtyNodes::default(); - - dirty_nodes.insert(1, NodeId(1)); - dirty_nodes.insert(0, NodeId(0)); - dirty_nodes.insert(2, NodeId(3)); - dirty_nodes.insert(1, NodeId(2)); - - assert_eq!(dirty_nodes.pop_front(), Some(NodeId(0))); - assert!(matches!(dirty_nodes.pop_front(), Some(NodeId(1 | 2)))); - assert!(matches!(dirty_nodes.pop_front(), Some(NodeId(1 | 2)))); - assert_eq!(dirty_nodes.pop_front(), Some(NodeId(3))); + fn pop(&mut self) -> Option { + let index = self.passes_dirty.iter().position(|dirty| *dirty != 0)?; + let passes = self.passes_dirty[index]; + let node_id = passes.trailing_zeros(); + let encoded = 1 << node_id; + self.passes_dirty[index] &= !encoded; + Some(NodeId((index * 64) + node_id as usize)) + } } #[derive(Default)] pub struct DirtyNodeStates { - dirty: FxDashMap>, + dirty: BTreeMap>, } impl DirtyNodeStates { - pub fn new(starting_nodes: FxHashMap>) -> Self { - let this = Self::default(); - for (node, nodes) in starting_nodes { - for pass_id in nodes { - this.insert(pass_id, node); - } - } - this - } - - pub fn insert(&self, pass_id: PassId, node_id: NodeId) { - let pass_id = pass_id.0; - let index = pass_id / 64; - let bit = pass_id % 64; - let encoded = 1 << bit; - if let Some(dirty) = self.dirty.get(&node_id) { - if let Some(atomic) = dirty.get(index as usize) { - atomic.fetch_or(encoded, Ordering::Relaxed); + pub fn insert(&mut self, pass_id: PassId, node_id: NodeId, height: u16) { + if let Some(dirty) = self.dirty.get_mut(&height) { + if let Some(entry) = dirty.get_mut(&pass_id) { + entry.add_node(node_id); } else { - drop(dirty); - let mut write = self.dirty.get_mut(&node_id).unwrap(); - write.resize_with(index as usize + 1, || AtomicU64::new(0)); - write[index as usize].fetch_or(encoded, Ordering::Relaxed); + let mut entry = DirtyNodes::default(); + entry.add_node(node_id); + dirty.insert(pass_id, entry); } } else { - let mut v = Vec::with_capacity(index as usize + 1); - v.resize_with(index as usize + 1, || AtomicU64::new(0)); - v[index as usize].fetch_or(encoded, Ordering::Relaxed); - self.dirty.insert(node_id, v); + let mut entry = DirtyNodes::default(); + entry.add_node(node_id); + let mut hm = FxHashMap::default(); + hm.insert(pass_id, entry); + self.dirty.insert(height, hm); } } - fn all_dirty(&self, pass_id: PassId, dirty_nodes: &mut DirtyNodes, tree: &impl TreeView) { - let pass_id = pass_id.0; - let index = pass_id / 64; - let bit = pass_id % 64; - let encoded = 1 << bit; - for entry in self.dirty.iter() { - let node_id = entry.key(); - let dirty = entry.value(); - if let Some(atomic) = dirty.get(index as usize) { - if atomic.load(Ordering::Relaxed) & encoded != 0 { - dirty_nodes.insert(tree.height(*node_id).unwrap(), *node_id); - } - } + fn pop_front(&mut self, pass_id: PassId) -> Option<(u16, NodeId)> { + let (&height, values) = self + .dirty + .iter_mut() + .find(|(_, values)| values.contains_key(&pass_id))?; + let dirty = values.get_mut(&pass_id)?; + let node_id = dirty.pop()?; + if dirty.is_empty() { + values.remove(&pass_id); } + if values.is_empty() { + self.dirty.remove(&height); + } + + Some((height, node_id)) + } + + fn pop_back(&mut self, pass_id: PassId) -> Option<(u16, NodeId)> { + let (&height, values) = self + .dirty + .iter_mut() + .rev() + .find(|(_, values)| values.contains_key(&pass_id))?; + let dirty = values.get_mut(&pass_id)?; + let node_id = dirty.pop()?; + if dirty.is_empty() { + values.remove(&pass_id); + } + if values.is_empty() { + self.dirty.remove(&height); + } + + Some((height, node_id)) } } @@ -174,12 +162,12 @@ pub trait UpwardPass: Pass { fn resolve_upward_pass + ?Sized>( tree: &mut impl TreeView, pass: &P, - mut dirty: DirtyNodes, - dirty_states: &DirtyNodeStates, + dirty_states: &mut DirtyNodeStates, nodes_updated: &FxDashSet, ctx: &SendAnyMap, ) { - while let Some(id) = dirty.pop_back() { + let pass_id = pass.pass_id(); + while let Some((height, id)) = dirty_states.pop_back(pass_id) { let (node, mut children) = tree.parent_child_mut(id).unwrap(); let result = pass.pass(node, &mut children, ctx); drop(children); @@ -188,12 +176,11 @@ fn resolve_upward_pass + ?Sized>( if let Some(id) = tree.parent_id(id) { if result.mark_dirty { for dependant in pass.dependants() { - dirty_states.insert(*dependant, id); + dirty_states.insert(*dependant, id, height - 1); } } - if result.progress { - let height = tree.height(id).unwrap(); - dirty.insert(height, id); + if result.progress && height > 0 { + dirty_states.insert(pass_id, id, height - 1); } } } @@ -207,12 +194,12 @@ pub trait DownwardPass: Pass { fn resolve_downward_pass + ?Sized>( tree: &mut impl TreeView, pass: &P, - mut dirty: DirtyNodes, - dirty_states: &DirtyNodeStates, + dirty_states: &mut DirtyNodeStates, nodes_updated: &FxDashSet, ctx: &SendAnyMap, ) { - while let Some(id) = dirty.pop_front() { + let pass_id = pass.pass_id(); + while let Some((height, id)) = dirty_states.pop_front(pass_id) { let (node, parent) = tree.node_parent_mut(id).unwrap(); let result = pass.pass(node, parent, ctx); if result.mark_dirty { @@ -222,12 +209,11 @@ fn resolve_downward_pass + ?Sized>( for id in tree.children_ids(id).unwrap() { if result.mark_dirty { for dependant in pass.dependants() { - dirty_states.insert(*dependant, *id); + dirty_states.insert(*dependant, *id, height + 1); } } if result.progress { - let height = tree.height(*id).unwrap(); - dirty.insert(height, *id); + dirty_states.insert(pass_id, *id, height + 1); } } } @@ -241,17 +227,17 @@ pub trait NodePass: Pass { fn resolve_node_pass + ?Sized>( tree: &mut impl TreeView, pass: &P, - mut dirty: DirtyNodes, - dirty_states: &DirtyNodeStates, + dirty_states: &mut DirtyNodeStates, nodes_updated: &FxDashSet, ctx: &SendAnyMap, ) { - while let Some(id) = dirty.pop_back() { + let pass_id = pass.pass_id(); + while let Some((height, id)) = dirty_states.pop_back(pass_id) { let node = tree.get_mut(id).unwrap(); if pass.pass(node, ctx) { nodes_updated.insert(id); for dependant in pass.dependants() { - dirty_states.insert(*dependant, id); + dirty_states.insert(*dependant, id, height); } } } @@ -280,90 +266,109 @@ impl AnyPass { } } - fn mask(&self) -> MemberMask { - match self { - Self::Upward(pass) => pass.mask(), - Self::Downward(pass) => pass.mask(), - Self::Node(pass) => pass.mask(), - } - } - fn resolve( &self, tree: &mut impl TreeView, - dirty: DirtyNodes, - dirty_states: &DirtyNodeStates, + dirty_states: &mut DirtyNodeStates, nodes_updated: &FxDashSet, ctx: &SendAnyMap, ) { match self { Self::Downward(pass) => { - resolve_downward_pass(tree, *pass, dirty, dirty_states, nodes_updated, ctx) + resolve_downward_pass(tree, *pass, dirty_states, nodes_updated, ctx) } Self::Upward(pass) => { - resolve_upward_pass(tree, *pass, dirty, dirty_states, nodes_updated, ctx) - } - Self::Node(pass) => { - resolve_node_pass(tree, *pass, dirty, dirty_states, nodes_updated, ctx) + resolve_upward_pass(tree, *pass, dirty_states, nodes_updated, ctx) } + Self::Node(pass) => resolve_node_pass(tree, *pass, dirty_states, nodes_updated, ctx), } } } -struct RawPointer(*mut T); -unsafe impl Send for RawPointer {} -unsafe impl Sync for RawPointer {} +pub fn resolve_passes + Sync + Send>( + tree: &mut Tr, + dirty_nodes: DirtyNodeStates, + passes: Vec<&AnyPass>, + ctx: SendAnyMap, +) -> FxDashSet { + resolve_passes_single_threaded(tree, dirty_nodes, passes, ctx) + // TODO: multithreadeding has some safety issues currently that need to be resolved before it can be used + // let dirty_states = Arc::new(dirty_nodes); + // let mut resolved_passes: FxHashSet = FxHashSet::default(); + // let mut resolving = Vec::new(); + // let nodes_updated = Arc::new(FxDashSet::default()); + // let ctx = Arc::new(ctx); + // while !passes.is_empty() { + // let mut currently_borrowed = MemberMask::default(); + // std::thread::scope(|s| { + // let mut i = 0; + // while i < passes.len() { + // let pass = &passes[i]; + // let pass_id = pass.pass_id(); + // let pass_mask = pass.mask(); + // if pass + // .dependancies() + // .iter() + // .all(|d| resolved_passes.contains(d) || *d == pass_id) + // && !pass_mask.overlaps(currently_borrowed) + // { + // let pass = passes.remove(i); + // resolving.push(pass_id); + // currently_borrowed |= pass_mask; + // let dirty_states = dirty_states.clone(); + // let nodes_updated = nodes_updated.clone(); + // let ctx = ctx.clone(); + // let mut dirty = DirtyNodes::default(); + // // dirty_states.all_dirty(pass_id, &mut dirty, tree); + // // this is safe because the member_mask acts as a per-member mutex and we have verified that the pass does not overlap with any other pass + // let tree_mut_unbounded = unsafe { &mut *(tree as *mut Tr) }; + // s.spawn(move || { + // pass.resolve( + // tree_mut_unbounded, + // dirty, + // &dirty_states, + // &nodes_updated, + // &ctx, + // ); + // }); + // } else { + // i += 1; + // } + // } + // // all passes are resolved at the end of the scope + // }); + // resolved_passes.extend(resolving.iter().copied()); + // resolving.clear() + // } + // std::sync::Arc::try_unwrap(nodes_updated).unwrap() +} -pub fn resolve_passes>( +pub fn resolve_passes_single_threaded>( tree: &mut Tr, dirty_nodes: DirtyNodeStates, mut passes: Vec<&AnyPass>, ctx: SendAnyMap, ) -> FxDashSet { - let dirty_states = Arc::new(dirty_nodes); + let mut dirty_states = dirty_nodes; let mut resolved_passes: FxHashSet = FxHashSet::default(); - let mut resolving = Vec::new(); let nodes_updated = Arc::new(FxDashSet::default()); let ctx = Arc::new(ctx); while !passes.is_empty() { - let mut currently_borrowed = MemberMask::default(); - std::thread::scope(|s| { - let mut i = 0; - while i < passes.len() { - let pass = &passes[i]; - let pass_id = pass.pass_id(); - let pass_mask = pass.mask(); - if pass - .dependancies() - .iter() - .all(|d| resolved_passes.contains(d) || *d == pass_id) - && !pass_mask.overlaps(currently_borrowed) - { - let pass = passes.remove(i); - resolving.push(pass_id); - currently_borrowed |= pass_mask; - let tree_mut = tree as *mut _; - let raw_ptr = RawPointer(tree_mut); - let dirty_states = dirty_states.clone(); - let nodes_updated = nodes_updated.clone(); - let ctx = ctx.clone(); - s.spawn(move || unsafe { - // let tree_mut: &mut Tr = &mut *raw_ptr.0; - let raw = raw_ptr; - // this is safe because the member_mask acts as a per-member mutex and we have verified that the pass does not overlap with any other pass - let tree_mut: &mut Tr = &mut *raw.0; - let mut dirty = DirtyNodes::default(); - dirty_states.all_dirty(pass_id, &mut dirty, tree_mut); - pass.resolve(tree_mut, dirty, &dirty_states, &nodes_updated, &ctx); - }); - } else { - i += 1; - } + for (i, pass) in passes.iter().enumerate() { + let pass_id = pass.pass_id(); + if pass + .dependancies() + .iter() + .all(|d| resolved_passes.contains(d) || *d == pass_id) + { + let pass = passes.remove(i); + let nodes_updated = nodes_updated.clone(); + let ctx = ctx.clone(); + pass.resolve(tree, &mut dirty_states, &nodes_updated, &ctx); + resolved_passes.insert(pass_id); + break; } - // all passes are resolved at the end of the scope - }); - resolved_passes.extend(resolving.iter().copied()); - resolving.clear() + } } std::sync::Arc::try_unwrap(nodes_updated).unwrap() } @@ -401,8 +406,8 @@ fn node_pass() { let add_pass = AnyPass::Node(&AddPass); let passes = vec![&add_pass]; - let dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); - dirty_nodes.insert(PassId(0), tree.root()); + let mut dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); + dirty_nodes.insert(PassId(0), tree.root(), 0); resolve_passes(&mut tree, dirty_nodes, passes, SendAnyMap::new()); assert_eq!(tree.get(tree.root()).unwrap(), &1); @@ -468,8 +473,8 @@ fn dependant_node_pass() { let add_pass = AnyPass::Node(&AddPass); let subtract_pass = AnyPass::Node(&SubtractPass); let passes = vec![&add_pass, &subtract_pass]; - let dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); - dirty_nodes.insert(PassId(1), tree.root()); + let mut dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); + dirty_nodes.insert(PassId(1), tree.root(), 0); resolve_passes(&mut tree, dirty_nodes, passes, SendAnyMap::new()); assert_eq!(*tree.get(tree.root()).unwrap(), 0); @@ -535,9 +540,9 @@ fn independant_node_pass() { let add_pass1 = AnyPass::Node(&AddPass1); let add_pass2 = AnyPass::Node(&AddPass2); let passes = vec![&add_pass1, &add_pass2]; - let dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); - dirty_nodes.insert(PassId(0), tree.root()); - dirty_nodes.insert(PassId(1), tree.root()); + let mut dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); + dirty_nodes.insert(PassId(0), tree.root(), 0); + dirty_nodes.insert(PassId(1), tree.root(), 0); resolve_passes(&mut tree, dirty_nodes, passes, SendAnyMap::new()); assert_eq!(tree.get(tree.root()).unwrap(), &(1, 1)); @@ -590,8 +595,8 @@ fn down_pass() { let add_pass = AnyPass::Downward(&AddPass); let passes = vec![&add_pass]; - let dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); - dirty_nodes.insert(PassId(0), tree.root()); + let mut dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); + dirty_nodes.insert(PassId(0), tree.root(), 0); resolve_passes(&mut tree, dirty_nodes, passes, SendAnyMap::new()); assert_eq!(tree.get(tree.root()).unwrap(), &1); @@ -685,8 +690,8 @@ fn dependant_down_pass() { let add_pass = AnyPass::Downward(&AddPass); let subtract_pass = AnyPass::Downward(&SubtractPass); let passes = vec![&add_pass, &subtract_pass]; - let dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); - dirty_nodes.insert(PassId(1), tree.root()); + let mut dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); + dirty_nodes.insert(PassId(1), tree.root(), 0); resolve_passes(&mut tree, dirty_nodes, passes, SendAnyMap::new()); // Tree before: @@ -775,9 +780,9 @@ fn up_pass() { let add_pass = AnyPass::Upward(&AddPass); let passes = vec![&add_pass]; - let dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); - dirty_nodes.insert(PassId(0), grandchild1); - dirty_nodes.insert(PassId(0), grandchild2); + let mut dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); + dirty_nodes.insert(PassId(0), grandchild1, tree.height(grandchild1).unwrap()); + dirty_nodes.insert(PassId(0), grandchild2, tree.height(grandchild2).unwrap()); resolve_passes(&mut tree, dirty_nodes, passes, SendAnyMap::new()); assert_eq!(tree.get(tree.root()).unwrap(), &2); @@ -875,9 +880,9 @@ fn dependant_up_pass() { let add_pass = AnyPass::Upward(&AddPass); let subtract_pass = AnyPass::Upward(&SubtractPass); let passes = vec![&add_pass, &subtract_pass]; - let dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); - dirty_nodes.insert(PassId(1), grandchild1); - dirty_nodes.insert(PassId(1), grandchild2); + let mut dirty_nodes: DirtyNodeStates = DirtyNodeStates::default(); + dirty_nodes.insert(PassId(1), grandchild1, tree.height(grandchild1).unwrap()); + dirty_nodes.insert(PassId(1), grandchild2, tree.height(grandchild2).unwrap()); resolve_passes(&mut tree, dirty_nodes, passes, SendAnyMap::new()); // Tree before: diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 00b9afb03..1aa75c2d8 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -1,9 +1,9 @@ -use dioxus_core::{ElementId, Mutations, TemplateNode}; +use dioxus_core::{BorrowedAttributeValue, ElementId, Mutations, TemplateNode}; use rustc_hash::{FxHashMap, FxHashSet}; use std::fmt::Debug; use std::ops::{Deref, DerefMut, Index, IndexMut}; -use crate::node::{Node, NodeType, OwnedAttributeDiscription, OwnedAttributeValue}; +use crate::node::{FromAnyValue, Node, NodeType, OwnedAttributeDiscription, OwnedAttributeValue}; use crate::node_ref::{AttributeMask, NodeMask}; use crate::passes::DirtyNodeStates; use crate::state::State; @@ -25,9 +25,12 @@ fn mark_dirty( /// A Dom that can sync with the VirtualDom mutations intended for use in lazy renderers. /// The render state passes from parent to children and or accumulates state from children to parents. /// To get started implement [crate::state::ParentDepState], [crate::state::NodeDepState], or [crate::state::ChildDepState] and call [RealDom::apply_mutations] to update the dom and [RealDom::update_state] to update the state of the nodes. +/// +/// # Custom values +/// To allow custom values to be passed into attributes implement FromAnyValue on a type that can represent your custom value and specify the V generic to be that type. If you have many different custom values, it can be useful to use a enum type to represent the varients. #[derive(Debug)] -pub struct RealDom { - pub tree: Tree>, +pub struct RealDom, V: FromAnyValue + 'static = ()> { + pub tree: Tree>, /// a map from element id to real node id node_id_mapping: Vec>, nodes_listening: FxHashMap>, @@ -36,14 +39,14 @@ pub struct RealDom { root_initialized: bool, } -impl Default for RealDom { +impl, V: FromAnyValue> Default for RealDom { fn default() -> Self { Self::new() } } -impl RealDom { - pub fn new() -> RealDom { +impl, V: FromAnyValue> RealDom { + pub fn new() -> RealDom { let mut root = Node::new(NodeType::Element { tag: "Root".to_string(), namespace: Some("Root".to_string()), @@ -76,10 +79,6 @@ impl RealDom { if self.node_id_mapping.len() <= element_id.0 { self.node_id_mapping.resize(element_id.0 + 1, None); } - if let Some(Some(old_id)) = self.node_id_mapping.get(element_id.0) { - // free the memory associated with the old node - self.tree.remove(*old_id); - } self.node_id_mapping[element_id.0] = Some(node_id); } @@ -91,7 +90,7 @@ impl RealDom { current } - fn create_node(&mut self, node: Node) -> RealNodeId { + fn create_node(&mut self, node: Node) -> RealNodeId { let node_id = self.tree.create_node(node); let node = self.tree.get_mut(node_id).unwrap(); node.node_data.node_id = node_id; @@ -228,7 +227,7 @@ impl RealDom { self.tree.insert_before(old_node_id, new); mark_dirty(new, NodeMask::ALL, &mut nodes_updated); } - self.tree.remove(old_node_id); + self.remove(old_node_id, &mut nodes_updated); } ReplacePlaceholder { path, m } => { let new_nodes = self.stack.split_off(self.stack.len() - m); @@ -237,7 +236,7 @@ impl RealDom { self.tree.insert_before(old_node_id, new); mark_dirty(new, NodeMask::ALL, &mut nodes_updated); } - self.tree.remove(old_node_id); + self.remove(old_node_id, &mut nodes_updated); } InsertAfter { id, m } => { let new_nodes = self.stack.split_off(self.stack.len() - m); @@ -264,38 +263,32 @@ impl RealDom { let node_id = self.element_to_node_id(id); let node = self.tree.get_mut(node_id).unwrap(); if let NodeType::Element { attributes, .. } = &mut node.node_data.node_type { - attributes.insert( - OwnedAttributeDiscription { + if let BorrowedAttributeValue::None = &value { + attributes.remove(&OwnedAttributeDiscription { name: name.to_string(), namespace: ns.map(|s| s.to_string()), volatile: false, - }, - crate::node::OwnedAttributeValue::Text(value.to_string()), - ); - mark_dirty( - node_id, - NodeMask::new_with_attrs(AttributeMask::single(name)), - &mut nodes_updated, - ); - } - } - SetBoolAttribute { name, value, id } => { - let node_id = self.element_to_node_id(id); - let node = self.tree.get_mut(node_id).unwrap(); - if let NodeType::Element { attributes, .. } = &mut node.node_data.node_type { - attributes.insert( - OwnedAttributeDiscription { - name: name.to_string(), - namespace: None, - volatile: false, - }, - crate::node::OwnedAttributeValue::Bool(value), - ); - mark_dirty( - node_id, - NodeMask::new_with_attrs(AttributeMask::single(name)), - &mut nodes_updated, - ); + }); + mark_dirty( + node_id, + NodeMask::new_with_attrs(AttributeMask::single(name)), + &mut nodes_updated, + ); + } else { + attributes.insert( + OwnedAttributeDiscription { + name: name.to_string(), + namespace: ns.map(|s| s.to_string()), + volatile: false, + }, + OwnedAttributeValue::from(value), + ); + mark_dirty( + node_id, + NodeMask::new_with_attrs(AttributeMask::single(name)), + &mut nodes_updated, + ); + } } } SetText { value, id } => { @@ -333,7 +326,7 @@ impl RealDom { } Remove { id } => { let node_id = self.element_to_node_id(id); - self.tree.remove(node_id); + self.remove(node_id, &mut nodes_updated); } PushRoot { id } => { let node_id = self.element_to_node_id(id); @@ -342,13 +335,13 @@ impl RealDom { } } - let dirty_nodes = DirtyNodeStates::default(); + let mut dirty_nodes = DirtyNodeStates::default(); for (&n, mask) in &nodes_updated { // remove any nodes that were created and then removed in the same mutations from the dirty nodes list - if self.tree.contains(n) { + if let Some(height) = self.tree.height(n) { for (m, p) in S::MASKS.iter().zip(S::PASSES.iter()) { if mask.overlaps(m) { - dirty_nodes.insert(p.pass_id(), n); + dirty_nodes.insert(p.pass_id(), n, height); } } } @@ -358,17 +351,17 @@ impl RealDom { } /// Update the state of the dom, after appling some mutations. This will keep the nodes in the dom up to date with their VNode counterparts. - pub fn update_state( + pub fn update_state_single_threaded( &mut self, nodes_updated: DirtyNodeStates, ctx: SendAnyMap, ) -> FxDashSet { - S::update(nodes_updated, &mut self.tree, ctx) + S::update_single_threaded(nodes_updated, &mut self.tree, ctx) } /// Find all nodes that are listening for an event, sorted by there height in the dom progressing starting at the bottom and progressing up. /// This can be useful to avoid creating duplicate events. - pub fn get_listening_sorted(&self, event: &'static str) -> Vec<&Node> { + pub fn get_listening_sorted(&self, event: &'static str) -> Vec<&Node> { if let Some(nodes) = self.nodes_listening.get(event) { let mut listening: Vec<_> = nodes.iter().map(|id| &self[*id]).collect(); listening.sort_by(|n1, n2| { @@ -413,45 +406,77 @@ impl RealDom { } new_id } + + fn remove(&mut self, node_id: NodeId, nodes_updated: &mut FxHashMap) { + let node = self.tree.get(node_id).unwrap(); + if let NodeType::Element { listeners, .. } = &node.node_data.node_type { + for name in listeners.iter() { + self.nodes_listening.get_mut(name).unwrap().remove(&node_id); + } + } + if let Some(children) = self.tree.children_ids(node_id) { + let children = children.to_vec(); + for child in children { + self.remove(child, nodes_updated); + } + } + self.tree.remove(node_id); + mark_dirty(node_id, NodeMask::ALL, nodes_updated); + } } -impl Deref for RealDom { - type Target = Tree>; +impl + Sync, V: FromAnyValue> RealDom +where + Tree>: Sync + Send, +{ + /// Update the state of the dom, after appling some mutations. This will keep the nodes in the dom up to date with their VNode counterparts. + /// This will resolve the state in parallel + pub fn update_state( + &mut self, + nodes_updated: DirtyNodeStates, + ctx: SendAnyMap, + ) -> FxDashSet { + S::update(nodes_updated, &mut self.tree, ctx) + } +} + +impl, V: FromAnyValue> Deref for RealDom { + type Target = Tree>; fn deref(&self) -> &Self::Target { &self.tree } } -impl DerefMut for RealDom { +impl, V: FromAnyValue> DerefMut for RealDom { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.tree } } -impl Index for RealDom { - type Output = Node; +impl, V: FromAnyValue> Index for RealDom { + type Output = Node; fn index(&self, id: ElementId) -> &Self::Output { self.tree.get(self.element_to_node_id(id)).unwrap() } } -impl Index for RealDom { - type Output = Node; +impl, V: FromAnyValue> Index for RealDom { + type Output = Node; fn index(&self, idx: RealNodeId) -> &Self::Output { self.tree.get(idx).unwrap() } } -impl IndexMut for RealDom { +impl, V: FromAnyValue> IndexMut for RealDom { fn index_mut(&mut self, id: ElementId) -> &mut Self::Output { self.tree.get_mut(self.element_to_node_id(id)).unwrap() } } -impl IndexMut for RealDom { +impl, V: FromAnyValue> IndexMut for RealDom { fn index_mut(&mut self, idx: RealNodeId) -> &mut Self::Output { self.tree.get_mut(idx).unwrap() } diff --git a/packages/native-core/src/state.rs b/packages/native-core/src/state.rs index 7e6c5266a..44eb81e97 100644 --- a/packages/native-core/src/state.rs +++ b/packages/native-core/src/state.rs @@ -1,8 +1,8 @@ use std::cmp::Ordering; -use crate::node::Node; +use crate::node::{FromAnyValue, Node}; use crate::node_ref::{NodeMask, NodeView}; -use crate::passes::{resolve_passes, AnyPass, DirtyNodeStates}; +use crate::passes::{resolve_passes, resolve_passes_single_threaded, AnyPass, DirtyNodeStates}; use crate::tree::TreeView; use crate::{FxDashSet, RealNodeId, SendAnyMap}; @@ -76,7 +76,7 @@ pub(crate) fn union_ordered_iter<'a>( /// } /// } /// ``` -pub trait ChildDepState { +pub trait ChildDepState { /// The context is passed to the [ChildDepState::reduce] when it is resolved. type Ctx; /// A state from each child node that this node depends on. Typically this is Self, but it could be any state that is within the state tree. @@ -87,7 +87,7 @@ pub trait ChildDepState { /// Resolve the state current node's state from the state of the children, the state of the node, and some external context. fn reduce<'a>( &mut self, - node: NodeView, + node: NodeView<'a, V>, children: impl Iterator::ElementBorrowed<'a>>, ctx: &Self::Ctx, ) -> bool @@ -138,7 +138,7 @@ pub trait ChildDepState { /// } /// } /// ``` -pub trait ParentDepState { +pub trait ParentDepState { /// The context is passed to the [ParentDepState::reduce] when it is resolved. type Ctx; /// A state from from the parent node that this node depends on. Typically this is Self, but it could be any state that is within the state tree. @@ -149,7 +149,7 @@ pub trait ParentDepState { /// Resolve the state current node's state from the state of the parent node, the state of the node, and some external context. fn reduce<'a>( &mut self, - node: NodeView, + node: NodeView<'a, V>, parent: Option<::ElementBorrowed<'a>>, ctx: &Self::Ctx, ) -> bool; @@ -193,7 +193,7 @@ pub trait ParentDepState { /// } /// } /// ``` -pub trait NodeDepState { +pub trait NodeDepState { /// Depstate must be a tuple containing any number of borrowed elements that are either [ChildDepState], [ParentDepState] or [NodeDepState]. type DepState: ElementBorrowable; /// The state passed to [NodeDepState::reduce] when it is resolved. @@ -203,21 +203,21 @@ pub trait NodeDepState { /// Resolve the state current node's state from the state of the sibling states, the state of the node, and some external context. fn reduce<'a>( &mut self, - node: NodeView, + node: NodeView<'a, V>, node_state: ::ElementBorrowed<'a>, ctx: &Self::Ctx, ) -> bool; } /// Do not implement this trait. It is only meant to be derived and used through [crate::real_dom::RealDom]. -pub trait State: Default + Clone + 'static { +pub trait State: Default + Clone + 'static { #[doc(hidden)] - const PASSES: &'static [AnyPass>]; + const PASSES: &'static [AnyPass>]; #[doc(hidden)] const MASKS: &'static [NodeMask]; #[doc(hidden)] - fn update>>( + fn update> + Sync + Send>( dirty: DirtyNodeStates, tree: &mut T, ctx: SendAnyMap, @@ -225,12 +225,22 @@ pub trait State: Default + Clone + 'static { let passes = Self::PASSES.iter().collect(); resolve_passes(tree, dirty, passes, ctx) } + + #[doc(hidden)] + fn update_single_threaded>>( + dirty: DirtyNodeStates, + tree: &mut T, + ctx: SendAnyMap, + ) -> FxDashSet { + let passes = Self::PASSES.iter().collect(); + resolve_passes_single_threaded(tree, dirty, passes, ctx) + } } impl ChildDepState for () { type Ctx = (); type DepState = (); - fn reduce<'a>(&mut self, _: NodeView, _: impl Iterator, _: &Self::Ctx) -> bool + fn reduce<'a>(&mut self, _: NodeView<'a>, _: impl Iterator, _: &Self::Ctx) -> bool where Self::DepState: 'a, { @@ -241,7 +251,7 @@ impl ChildDepState for () { impl ParentDepState for () { type Ctx = (); type DepState = (); - fn reduce<'a>(&mut self, _: NodeView, _: Option<()>, _: &Self::Ctx) -> bool { + fn reduce<'a>(&mut self, _: NodeView<'a>, _: Option<()>, _: &Self::Ctx) -> bool { false } } @@ -249,7 +259,7 @@ impl ParentDepState for () { impl NodeDepState for () { type DepState = (); type Ctx = (); - fn reduce(&mut self, _: NodeView, _sibling: (), _: &Self::Ctx) -> bool { + fn reduce<'a>(&mut self, _: NodeView<'a>, _sibling: (), _: &Self::Ctx) -> bool { false } } diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index 0547fc3a9..760afe87b 100644 --- a/packages/native-core/src/tree.rs +++ b/packages/native-core/src/tree.rs @@ -1,11 +1,5 @@ -use core::panic; -use parking_lot::lock_api::RawMutex as _; -use parking_lot::{RawMutex, RwLock}; -use slab::Slab; -use std::cell::UnsafeCell; use std::collections::VecDeque; use std::marker::PhantomData; -use std::sync::Arc; #[derive(Hash, PartialEq, Eq, Clone, Copy, Debug, PartialOrd, Ord)] pub struct NodeId(pub usize); @@ -93,17 +87,7 @@ pub trait TreeView: Sized { fn children_mut(&mut self, id: NodeId) -> Option>; - fn parent_child_mut(&mut self, id: NodeId) -> Option<(&mut T, Self::IteratorMut<'_>)> { - let mut_ptr: *mut Self = self; - unsafe { - // Safety: No node has itself as a child. - (*mut_ptr).get_mut(id).and_then(|parent| { - (*mut_ptr) - .children_mut(id) - .map(|children| (parent, children)) - }) - } - } + fn parent_child_mut(&mut self, id: NodeId) -> Option<(&mut T, Self::IteratorMut<'_>)>; fn children_ids(&self, id: NodeId) -> Option<&[NodeId]>; @@ -111,28 +95,12 @@ pub trait TreeView: Sized { fn parent_mut(&mut self, id: NodeId) -> Option<&mut T>; - fn node_parent_mut(&mut self, id: NodeId) -> Option<(&mut T, Option<&mut T>)> { - let mut_ptr: *mut Self = self; - unsafe { - // Safety: No node has itself as a parent. - (*mut_ptr) - .get_mut(id) - .map(|node| (node, (*mut_ptr).parent_mut(id))) - } - } + fn node_parent_mut(&mut self, id: NodeId) -> Option<(&mut T, Option<&mut T>)>; fn parent_id(&self, id: NodeId) -> Option; fn height(&self, id: NodeId) -> Option; - fn map &T2, FMut: Fn(&mut T) -> &mut T2>( - &mut self, - map: F, - map_mut: FMut, - ) -> TreeMap { - TreeMap::new(self, map, map_mut) - } - fn size(&self) -> usize; fn traverse_depth_first(&self, mut f: impl FnMut(&T)) { @@ -208,58 +176,39 @@ pub trait TreeLike: TreeView { fn insert_after(&mut self, id: NodeId, new: NodeId); } -pub struct ChildNodeIterator<'a, T, Tr: TreeView> { - tree: &'a Tr, - children_ids: &'a [NodeId], +pub struct ChildNodeIterator<'a, T> { + nodes: &'a Slab>, + children_ids: Vec, index: usize, node_type: PhantomData, } -impl<'a, T: 'a, Tr: TreeView> Iterator for ChildNodeIterator<'a, T, Tr> { +impl<'a, T: 'a> Iterator for ChildNodeIterator<'a, T> { type Item = &'a T; fn next(&mut self) -> Option { self.children_ids.get(self.index).map(|id| { self.index += 1; - self.tree.get_unchecked(*id) + &self.nodes.get(id.0).unwrap().value }) } } -pub struct ChildNodeIteratorMut<'a, T, Tr: TreeView + 'a> { - tree: *mut Tr, - children_ids: &'a [NodeId], - index: usize, - node_type: PhantomData, +pub struct ChildNodeIteratorMut<'a, T> { + nodes: Vec<&'a mut Node>, } -unsafe impl<'a, T, Tr: TreeView + 'a> Sync for ChildNodeIteratorMut<'a, T, Tr> {} - -impl<'a, T, Tr: TreeView> ChildNodeIteratorMut<'a, T, Tr> { - fn tree_mut(&mut self) -> &'a mut Tr { - unsafe { &mut *self.tree } - } -} - -impl<'a, T: 'a, Tr: TreeView> Iterator for ChildNodeIteratorMut<'a, T, Tr> { +impl<'a, T: 'a> Iterator for ChildNodeIteratorMut<'a, T> { type Item = &'a mut T; fn next(&mut self) -> Option { - let owned = self.children_ids.get(self.index).copied(); - match owned { - Some(id) => { - self.index += 1; - - Some(self.tree_mut().get_mut_unchecked(id)) - } - None => None, - } + self.nodes.pop().map(|node| &mut node.value) } } impl TreeView for Tree { - type Iterator<'a> = ChildNodeIterator<'a, T, Tree> where T: 'a; - type IteratorMut<'a> = ChildNodeIteratorMut<'a, T, Tree> where T: 'a; + type Iterator<'a> = ChildNodeIterator<'a, T> where T: 'a; + type IteratorMut<'a> = ChildNodeIteratorMut<'a, T> where T: 'a; fn root(&self) -> NodeId { self.root @@ -275,24 +224,26 @@ impl TreeView for Tree { fn children(&self, id: NodeId) -> Option> { self.children_ids(id).map(|children_ids| ChildNodeIterator { - tree: self, - children_ids, + nodes: &self.nodes, + children_ids: children_ids.to_vec(), index: 0, node_type: PhantomData, }) } fn children_mut(&mut self, id: NodeId) -> Option> { - let raw_ptr = self as *mut Self; - unsafe { - // Safety: No node will appear as a child twice - self.children_ids(id) - .map(|children_ids| ChildNodeIteratorMut { - tree: &mut *raw_ptr, - children_ids, - index: 0, - node_type: PhantomData, - }) + // Safety: No node has itself as a parent. + if let Some(children_ids) = self.children_ids(id) { + let children_ids = children_ids.to_vec(); + Some(ChildNodeIteratorMut { + nodes: unsafe { + self.nodes + .get_many_mut_unchecked(children_ids.into_iter().rev().map(|id| id.0)) + .unwrap() + }, + }) + } else { + None } } @@ -340,6 +291,39 @@ impl TreeView for Tree { fn size(&self) -> usize { self.nodes.len() } + + fn node_parent_mut(&mut self, id: NodeId) -> Option<(&mut T, Option<&mut T>)> { + if let Some(parent_id) = self.parent_id(id) { + self.nodes + .get2_mut(id.0, parent_id.0) + .map(|(node, parent)| (&mut node.value, Some(&mut parent.value))) + } else { + self.nodes.get_mut(id.0).map(|node| (&mut node.value, None)) + } + } + + fn parent_child_mut(&mut self, id: NodeId) -> Option<(&mut T, Self::IteratorMut<'_>)> { + // Safety: No node will appear as a child twice + if let Some(children_ids) = self.children_ids(id) { + debug_assert!(!children_ids.iter().any(|child_id| *child_id == id)); + let mut borrowed = unsafe { + let as_vec = children_ids.to_vec(); + self.nodes + .get_many_mut_unchecked( + as_vec + .into_iter() + .rev() + .map(|id| id.0) + .chain(std::iter::once(id.0)), + ) + .unwrap() + }; + let node = &mut borrowed.pop().unwrap().value; + Some((node, ChildNodeIteratorMut { nodes: borrowed })) + } else { + None + } + } } impl TreeLike for Tree { @@ -435,248 +419,6 @@ impl TreeLike for Tree { self.set_height(new, height); } } - -pub struct TreeMap<'a, T1, T2, Tr, F, FMut> -where - Tr: TreeView, - F: Fn(&T1) -> &T2, - FMut: Fn(&mut T1) -> &mut T2, -{ - tree: &'a mut Tr, - map: F, - map_mut: FMut, - in_node_type: PhantomData, - out_node_type: PhantomData, -} - -impl<'a, T1, T2, Tr, F, FMut> TreeMap<'a, T1, T2, Tr, F, FMut> -where - Tr: TreeView, - F: Fn(&T1) -> &T2, - FMut: Fn(&mut T1) -> &mut T2, -{ - pub fn new(tree: &'a mut Tr, map: F, map_mut: FMut) -> Self { - TreeMap { - tree, - map, - map_mut, - in_node_type: PhantomData, - out_node_type: PhantomData, - } - } -} - -impl<'a, T1, T2, Tr, F, FMut> TreeView for TreeMap<'a, T1, T2, Tr, F, FMut> -where - Tr: TreeView, - F: Fn(&T1) -> &T2, - FMut: Fn(&mut T1) -> &mut T2, -{ - type Iterator<'b> = ChildNodeIterator<'b, T2, TreeMap<'a, T1, T2, Tr, F, FMut>> - where - T2: 'b, - Self:'b; - type IteratorMut<'b> = ChildNodeIteratorMut<'b, T2, TreeMap<'a, T1, T2, Tr, F, FMut>> - where - T2: 'b, - Self:'b; - - fn root(&self) -> NodeId { - self.tree.root() - } - - fn get(&self, id: NodeId) -> Option<&T2> { - self.tree.get(id).map(|node| (self.map)(node)) - } - - fn get_mut(&mut self, id: NodeId) -> Option<&mut T2> { - self.tree.get_mut(id).map(|node| (self.map_mut)(node)) - } - - fn children(&self, id: NodeId) -> Option> { - self.children_ids(id).map(|children_ids| ChildNodeIterator { - tree: self, - children_ids, - index: 0, - node_type: PhantomData, - }) - } - - fn children_mut(&mut self, id: NodeId) -> Option> { - let raw_ptr = self as *mut Self; - unsafe { - // Safety: No node can be a child twice. - self.children_ids(id) - .map(|children_ids| ChildNodeIteratorMut { - tree: &mut *raw_ptr, - children_ids, - index: 0, - node_type: PhantomData, - }) - } - } - - fn children_ids(&self, id: NodeId) -> Option<&[NodeId]> { - self.tree.children_ids(id) - } - - fn parent(&self, id: NodeId) -> Option<&T2> { - self.tree.parent(id).map(|node| (self.map)(node)) - } - - fn parent_mut(&mut self, id: NodeId) -> Option<&mut T2> { - self.tree.parent_mut(id).map(|node| (self.map_mut)(node)) - } - - fn parent_id(&self, id: NodeId) -> Option { - self.tree.parent_id(id) - } - - fn height(&self, id: NodeId) -> Option { - self.tree.height(id) - } - - fn get_unchecked(&self, id: NodeId) -> &T2 { - (self.map)(self.tree.get_unchecked(id)) - } - - fn get_mut_unchecked(&mut self, id: NodeId) -> &mut T2 { - (self.map_mut)(self.tree.get_mut_unchecked(id)) - } - - fn size(&self) -> usize { - self.tree.size() - } -} - -/// A view into a tree that can be shared between multiple threads. Nodes are locked invividually. -pub struct SharedView<'a, T, Tr: TreeView> { - tree: Arc>, - node_locks: Arc>>, - node_type: PhantomData, -} - -impl<'a, T, Tr: TreeView> SharedView<'a, T, Tr> { - /// Checks if a node is currently locked. Returns None if the node does not exist. - pub fn check_lock(&self, id: NodeId) -> Option { - let locks = self.node_locks.read(); - locks.get(id.0).map(|lock| lock.is_locked()) - } -} - -unsafe impl<'a, T, Tr: TreeView> Send for SharedView<'a, T, Tr> {} -unsafe impl<'a, T, Tr: TreeView> Sync for SharedView<'a, T, Tr> {} -impl<'a, T, Tr: TreeView> Clone for SharedView<'a, T, Tr> { - fn clone(&self) -> Self { - Self { - tree: self.tree.clone(), - node_locks: self.node_locks.clone(), - node_type: PhantomData, - } - } -} - -impl<'a, T, Tr: TreeView> SharedView<'a, T, Tr> { - pub fn new(tree: &'a mut Tr) -> Self { - let tree = Arc::new(UnsafeCell::new(tree)); - let mut node_locks = Vec::new(); - for _ in 0..unsafe { (*tree.get()).size() } { - node_locks.push(RawMutex::INIT); - } - Self { - tree, - node_locks: Arc::new(RwLock::new(node_locks)), - node_type: PhantomData, - } - } - - fn lock_node(&self, node: NodeId) { - let read = self.node_locks.read(); - let lock = read.get(node.0); - match lock { - Some(lock) => lock.lock(), - None => { - drop(read); - let mut write = self.node_locks.write(); - write.resize_with(node.0 + 1, || RawMutex::INIT); - unsafe { write.get_unchecked(node.0).lock() } - } - } - } - - fn unlock_node(&self, node: NodeId) { - let read = self.node_locks.read(); - let lock = read.get(node.0); - match lock { - Some(lock) => unsafe { lock.unlock() }, - None => { - panic!("unlocking node that was not locked") - } - } - } - - fn with_node(&self, node_id: NodeId, f: impl FnOnce(&'a mut Tr) -> R) -> R { - self.lock_node(node_id); - let tree = unsafe { &mut *self.tree.get() }; - let r = f(tree); - self.unlock_node(node_id); - r - } -} - -impl<'a, T, Tr: TreeView> TreeView for SharedView<'a, T, Tr> { - type Iterator<'b> = Tr::Iterator<'b> where T: 'b, Self: 'b; - - type IteratorMut<'b>=Tr::IteratorMut<'b> - where - T: 'b, - Self: 'b; - - fn root(&self) -> NodeId { - unsafe { (*self.tree.get()).root() } - } - - fn get(&self, id: NodeId) -> Option<&T> { - self.with_node(id, |t| t.get(id)) - } - - fn get_mut(&mut self, id: NodeId) -> Option<&mut T> { - self.with_node(id, |t| t.get_mut(id)) - } - - fn children(&self, id: NodeId) -> Option> { - self.with_node(id, |t| t.children(id)) - } - - fn children_mut(&mut self, id: NodeId) -> Option> { - self.with_node(id, |t| t.children_mut(id)) - } - - fn children_ids(&self, id: NodeId) -> Option<&[NodeId]> { - self.with_node(id, |t| t.children_ids(id)) - } - - fn parent(&self, id: NodeId) -> Option<&T> { - self.with_node(id, |t| t.get(id)) - } - - fn parent_mut(&mut self, id: NodeId) -> Option<&mut T> { - self.with_node(id, |t| t.parent_mut(id)) - } - - fn parent_id(&self, id: NodeId) -> Option { - self.with_node(id, |t| t.parent_id(id)) - } - - fn height(&self, id: NodeId) -> Option { - unsafe { (*self.tree.get()).height(id) } - } - - fn size(&self) -> usize { - unsafe { (*self.tree.get()).size() } - } -} - #[test] fn creation() { let mut tree = Tree::new(1); @@ -785,59 +527,6 @@ fn deletion() { assert_eq!(tree.children_ids(parent).unwrap(), &[]); } -#[test] -fn shared_view() { - use std::thread; - let mut tree = Tree::new(1); - let parent = tree.root(); - let child = tree.create_node(0); - tree.add_child(parent, child); - - let shared = SharedView::new(&mut tree); - - thread::scope(|s| { - let (mut shared1, mut shared2, mut shared3) = - (shared.clone(), shared.clone(), shared.clone()); - s.spawn(move || { - assert_eq!(*shared1.get_mut(parent).unwrap(), 1); - assert_eq!(*shared1.get_mut(child).unwrap(), 0); - }); - s.spawn(move || { - assert_eq!(*shared2.get_mut(child).unwrap(), 0); - assert_eq!(*shared2.get_mut(parent).unwrap(), 1); - }); - s.spawn(move || { - assert_eq!(*shared3.get_mut(parent).unwrap(), 1); - assert_eq!(*shared3.get_mut(child).unwrap(), 0); - }); - }); -} - -#[test] -fn map() { - #[derive(Debug, PartialEq)] - struct Value { - value: i32, - } - impl Value { - fn new(value: i32) -> Self { - Self { value } - } - } - let mut tree = Tree::new(Value::new(1)); - let parent = tree.root(); - let child = tree.create_node(Value::new(0)); - tree.add_child(parent, child); - - let mut mapped = tree.map(|x| &x.value, |x| &mut x.value); - - *mapped.get_mut(child).unwrap() = 1; - *mapped.get_mut(parent).unwrap() = 2; - - assert_eq!(*tree.get(parent).unwrap(), Value::new(2)); - assert_eq!(*tree.get(child).unwrap(), Value::new(1)); -} - #[test] fn traverse_depth_first() { let mut tree = Tree::new(0); @@ -857,3 +546,124 @@ fn traverse_depth_first() { node_count += 1; }); } + +#[test] +fn get_node_children_mut() { + let mut tree = Tree::new(0); + let parent = tree.root(); + let child1 = tree.create_node(1); + tree.add_child(parent, child1); + let child2 = tree.create_node(2); + tree.add_child(parent, child2); + let child3 = tree.create_node(3); + tree.add_child(parent, child3); + + let (parent, children) = tree.parent_child_mut(parent).unwrap(); + for (i, child) in children.enumerate() { + assert_eq!(*child, i + 1); + } + println!("Parent: {:#?}", parent); +} + +#[test] +fn get_many_mut_unchecked() { + let mut slab = Slab::new(); + let parent = slab.insert(0); + let child = slab.insert(1); + let grandchild = slab.insert(2); + + let all = + unsafe { slab.get_many_mut_unchecked([parent, child, grandchild].into_iter()) }.unwrap(); + println!("All: {:#?}", all); +} + +#[derive(Debug)] +struct Slab { + data: Vec>, + free: VecDeque, +} + +impl Default for Slab { + fn default() -> Self { + Self::new() + } +} + +impl Slab { + fn new() -> Self { + Self { + data: Vec::new(), + free: VecDeque::new(), + } + } + + fn get(&self, id: usize) -> Option<&T> { + self.data.get(id).and_then(|x| x.as_ref()) + } + + unsafe fn get_unchecked(&self, id: usize) -> &T { + self.data.get_unchecked(id).as_ref().unwrap() + } + + fn get_mut(&mut self, id: usize) -> Option<&mut T> { + self.data.get_mut(id).and_then(|x| x.as_mut()) + } + + unsafe fn get_unchecked_mut(&mut self, id: usize) -> &mut T { + self.data.get_unchecked_mut(id).as_mut().unwrap() + } + + fn get2_mut(&mut self, id1: usize, id2: usize) -> Option<(&mut T, &mut T)> { + assert!(id1 != id2); + let ptr = self.data.as_mut_ptr(); + let first = unsafe { &mut *ptr.add(id1) }; + let second = unsafe { &mut *ptr.add(id2) }; + if let (Some(first), Some(second)) = (first, second) { + Some((first, second)) + } else { + None + } + } + + unsafe fn get_many_mut_unchecked( + &mut self, + ids: impl Iterator, + ) -> Option> { + let ptr = self.data.as_mut_ptr(); + let mut result = Vec::new(); + for id in ids { + let item = unsafe { &mut *ptr.add(id) }; + if let Some(item) = item { + result.push(item); + } else { + return None; + } + } + Some(result) + } + + fn insert(&mut self, value: T) -> usize { + if let Some(id) = self.free.pop_front() { + self.data[id] = Some(value); + id + } else { + self.data.push(Some(value)); + self.data.len() - 1 + } + } + + fn try_remove(&mut self, id: usize) -> Option { + self.data.get_mut(id).and_then(|x| { + self.free.push_back(id); + x.take() + }) + } + + fn remove(&mut self, id: usize) -> T { + self.try_remove(id).unwrap() + } + + fn len(&self) -> usize { + self.data.len() - self.free.len() + } +} diff --git a/packages/native-core/src/utils/persistant_iterator.rs b/packages/native-core/src/utils/persistant_iterator.rs index 75bcf9064..521d449fc 100644 --- a/packages/native-core/src/utils/persistant_iterator.rs +++ b/packages/native-core/src/utils/persistant_iterator.rs @@ -1,4 +1,10 @@ -use crate::{node::NodeType, real_dom::RealDom, state::State, tree::TreeView, NodeId, RealNodeId}; +use crate::{ + node::{FromAnyValue, NodeType}, + real_dom::RealDom, + state::State, + tree::TreeView, + NodeId, RealNodeId, +}; use dioxus_core::{Mutation, Mutations}; use std::fmt::Debug; @@ -67,7 +73,11 @@ impl PersistantElementIter { /// remove stale element refreneces /// returns true if the focused element is removed - pub fn prune(&mut self, mutations: &Mutations, rdom: &RealDom) -> bool { + pub fn prune, V: FromAnyValue>( + &mut self, + mutations: &Mutations, + rdom: &RealDom, + ) -> bool { let mut changed = false; let ids_removed: Vec<_> = mutations .edits @@ -124,7 +134,7 @@ impl PersistantElementIter { } /// get the next element - pub fn next(&mut self, rdom: &RealDom) -> ElementProduced { + pub fn next, V: FromAnyValue>(&mut self, rdom: &RealDom) -> ElementProduced { if self.stack.is_empty() { let id = NodeId(0); let new = (id, NodePosition::AtNode); @@ -160,12 +170,12 @@ impl PersistantElementIter { } /// get the previous element - pub fn prev(&mut self, rdom: &RealDom) -> ElementProduced { + pub fn prev, V: FromAnyValue>(&mut self, rdom: &RealDom) -> ElementProduced { // recursively add the last child element to the stack - fn push_back( + fn push_back, V: FromAnyValue>( stack: &mut smallvec::SmallVec<[(RealNodeId, NodePosition); 5]>, new_node: RealNodeId, - rdom: &RealDom, + rdom: &RealDom, ) -> RealNodeId { match &rdom[new_node].node_data.node_type { NodeType::Element { .. } => { @@ -228,8 +238,8 @@ impl PersistantElementIter { #[derive(Default, Clone, Debug)] struct Empty {} -impl State for Empty { - const PASSES: &'static [crate::AnyPass>] = &[]; +impl State<()> for Empty { + const PASSES: &'static [crate::AnyPass>] = &[]; const MASKS: &'static [crate::NodeMask] = &[]; } diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 5b72e8117..d14d9ae19 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-router" -version = "0.2.3" +version = "0.3.0" edition = "2018" description = "Cross-platform router for Dioxus apps" license = "MIT/Apache-2.0" @@ -11,7 +11,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus = { path = "../dioxus" } +dioxus = { path = "../dioxus", version = "0.3.0"} futures-channel = "0.3.21" url = { version = "2.2.2", default-features = false } @@ -50,7 +50,7 @@ log = "0.4.14" wasm-logger = "0.2.0" wasm-bindgen-test = "0.3" gloo-utils = "0.1.2" -dioxus-web = { path = "../web" } +dioxus-web = { path = "../web", version = "0.3.0" } # dioxus-desktop = { path = "../desktop", optional = true } # not wasm diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml index 429a86de4..d0b906abd 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-rsx" -version = "0.0.1" +version = "0.0.2" edition = "2018" license = "MIT/Apache-2.0" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" @@ -13,8 +13,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] proc-macro2 = { version = "1.0", features = ["span-locations"] } -dioxus-core = { path = "../core" } +dioxus-core = { path = "../core", version = "0.3.0"} syn = { version = "1.0", features = ["full", "extra-traits"] } quote = { version = "1.0" } serde = { version = "1.0", features = ["derive"] } -internment = "0.7.0" \ No newline at end of file +internment = "0.7.0" diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index 0439108e2..5dc18e0e6 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -19,8 +19,7 @@ use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, spanned::Spanned, - token, AngleBracketedGenericArguments, Error, Expr, Ident, LitStr, PathArguments, Result, - Token, + AngleBracketedGenericArguments, Error, Expr, Ident, LitStr, PathArguments, Result, Token, }; #[derive(PartialEq, Eq, Clone, Debug, Hash)] @@ -30,6 +29,7 @@ pub struct Component { pub fields: Vec, pub children: Vec, pub manual_props: Option, + pub brace: syn::token::Brace, } impl Component { @@ -88,13 +88,9 @@ impl Parse for Component { // if we see a `{` then we have a block // else parse as a function-like call - if stream.peek(token::Brace) { - syn::braced!(content in stream); - } else { - syn::parenthesized!(content in stream); - } + let brace = syn::braced!(content in stream); - let mut body = Vec::new(); + let mut fields = Vec::new(); let mut children = Vec::new(); let mut manual_props = None; @@ -104,7 +100,7 @@ impl Parse for Component { content.parse::()?; manual_props = Some(content.parse::()?); } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - body.push(content.parse::()?); + fields.push(content.parse::()?); } else { children.push(content.parse::()?); } @@ -117,9 +113,10 @@ impl Parse for Component { Ok(Self { name, prop_gen_args, - fields: body, + fields, children, manual_props, + brace, }) } } diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index f654c5ae4..e47c2e02a 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -17,6 +17,7 @@ pub struct Element { pub attributes: Vec, pub children: Vec, pub _is_static: bool, + pub brace: syn::token::Brace, } impl Parse for Element { @@ -25,7 +26,7 @@ impl Parse for Element { // parse the guts let content: ParseBuffer; - syn::braced!(content in stream); + let brace = syn::braced!(content in stream); let mut attributes: Vec = vec![]; let mut children: Vec = vec![]; @@ -152,6 +153,7 @@ impl Parse for Element { name: el_name, attributes, children, + brace, _is_static: false, }) } @@ -246,8 +248,8 @@ impl ToTokens for ElementAttrNamed { __cx.attr( dioxus_elements::#el_name::#name.0, #value, - None, - false + dioxus_elements::#el_name::#name.1, + dioxus_elements::#el_name::#name.2 ) } } @@ -256,8 +258,8 @@ impl ToTokens for ElementAttrNamed { __cx.attr( dioxus_elements::#el_name::#name.0, #value, - None, - false + dioxus_elements::#el_name::#name.1, + dioxus_elements::#el_name::#name.2 ) } } diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml new file mode 100644 index 000000000..9a12c787d --- /dev/null +++ b/packages/signals/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "dioxus-signals" +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-core = { path = "../core" } +slab = "0.4.7" diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs new file mode 100644 index 000000000..e4b499ed8 --- /dev/null +++ b/packages/signals/src/lib.rs @@ -0,0 +1,131 @@ +use std::{ + cell::{Ref, RefMut}, + fmt::Display, + marker::PhantomData, + ops::{Add, Div, Mul, Sub}, +}; + +mod rt; + +use dioxus_core::ScopeState; +pub use rt::*; + +pub fn use_init_signal_rt(cx: &ScopeState) { + cx.use_hook(|| { + let rt = crate::rt::claim_rt(cx.schedule_update_any()); + cx.provide_context(rt); + }); +} + +pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { + cx.use_hook(|| { + let rt: &'static SignalRt = cx.consume_context().unwrap(); + let id = rt.init(f()); + rt.subscribe(id, cx.scope_id()); + + struct SignalHook { + signal: Signal, + } + + impl Drop for SignalHook { + fn drop(&mut self) { + self.signal.rt.remove(self.signal.id); + } + } + + SignalHook { + signal: Signal { + id, + rt, + t: PhantomData, + }, + } + }) + .signal +} + +pub struct Signal { + id: usize, + rt: &'static SignalRt, + t: PhantomData, +} + +impl Signal { + pub fn read(&self) -> Ref { + self.rt.read(self.id) + } + + pub fn write(&self) -> RefMut { + self.rt.write(self.id) + } + + pub fn set(&mut self, value: T) { + self.rt.set(self.id, value); + } + + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + let write = self.read(); + f(&*write) + } + + pub fn update(&self, _f: impl FnOnce(&mut T) -> O) -> O { + let mut write = self.write(); + _f(&mut *write) + } +} + +impl Signal { + pub fn get(&self) -> T { + self.rt.get(self.id) + } +} + +impl std::ops::Deref for Signal { + type Target = dyn Fn() -> T; + + fn deref(&self) -> &Self::Target { + self.rt.getter(self.id) + } +} + +impl std::clone::Clone for Signal { + fn clone(&self) -> Self { + Self { + t: PhantomData, + id: self.id, + rt: self.rt, + } + } +} + +impl Copy for Signal {} + +impl Display for Signal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.rt.with::(self.id, |v| T::fmt(v, f)) + } +} + +impl + Copy + 'static> std::ops::AddAssign for Signal { + fn add_assign(&mut self, rhs: T) { + self.set(self.get() + rhs); + } +} + +impl + Copy + 'static> std::ops::SubAssign for Signal { + fn sub_assign(&mut self, rhs: T) { + self.set(self.get() - rhs); + } +} + +impl + Copy + 'static> std::ops::MulAssign for Signal { + fn mul_assign(&mut self, rhs: T) { + self.set(self.get() * rhs); + } +} + +impl + Copy + 'static> std::ops::DivAssign for Signal { + fn div_assign(&mut self, rhs: T) { + self.set(self.get() / rhs); + } +} diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs new file mode 100644 index 000000000..e849f9008 --- /dev/null +++ b/packages/signals/src/rt.rs @@ -0,0 +1,121 @@ +use std::{any::Any, cell::RefCell, sync::Arc}; + +use dioxus_core::ScopeId; +use slab::Slab; + +thread_local! { + // we cannot drop these since any future might be using them + static RUNTIMES: RefCell> = RefCell::new(Vec::new()); +} + +/// Provide the runtime for signals +/// +/// This will reuse dead runtimes +pub fn claim_rt(update_any: Arc) -> &'static SignalRt { + RUNTIMES.with(|runtimes| { + if let Some(rt) = runtimes.borrow_mut().pop() { + return rt; + } + + Box::leak(Box::new(SignalRt { + signals: RefCell::new(Slab::new()), + update_any, + })) + }) +} + +/// Push this runtime into the global runtime list +pub fn reclam_rt(_rt: &'static SignalRt) { + RUNTIMES.with(|runtimes| { + runtimes.borrow_mut().push(_rt); + }); +} + +pub struct SignalRt { + pub(crate) signals: RefCell>, + pub(crate) update_any: Arc, +} + +impl SignalRt { + pub fn init(&'static self, val: T) -> usize { + self.signals.borrow_mut().insert(Inner { + value: Box::new(val), + subscribers: Vec::new(), + getter: None, + }) + } + + pub fn subscribe(&self, id: usize, subscriber: ScopeId) { + self.signals.borrow_mut()[id].subscribers.push(subscriber); + } + + pub fn get(&self, id: usize) -> T { + self.signals.borrow()[id] + .value + .downcast_ref::() + .cloned() + .unwrap() + } + + pub fn set(&self, id: usize, value: T) { + let mut signals = self.signals.borrow_mut(); + let inner = &mut signals[id]; + inner.value = Box::new(value); + + for subscriber in inner.subscribers.iter() { + (self.update_any)(*subscriber); + } + } + + pub fn remove(&self, id: usize) { + self.signals.borrow_mut().remove(id); + } + + pub fn with(&self, id: usize, f: impl FnOnce(&T) -> O) -> O { + let signals = self.signals.borrow(); + let inner = &signals[id]; + let inner = inner.value.downcast_ref::().unwrap(); + f(inner) + } + + pub(crate) fn read(&self, id: usize) -> std::cell::Ref { + let signals = self.signals.borrow(); + std::cell::Ref::map(signals, |signals| { + signals[id].value.downcast_ref::().unwrap() + }) + } + + pub(crate) fn write(&self, id: usize) -> std::cell::RefMut { + let signals = self.signals.borrow_mut(); + std::cell::RefMut::map(signals, |signals| { + signals[id].value.downcast_mut::().unwrap() + }) + } + + pub(crate) fn getter(&self, id: usize) -> &dyn Fn() -> T { + let mut signals = self.signals.borrow_mut(); + let inner = &mut signals[id]; + let r = inner.getter.as_mut(); + + if r.is_none() { + let rt = self; + let r = move || rt.get::(id); + let getter: Box T> = Box::new(r); + let getter: Box = unsafe { std::mem::transmute(getter) }; + + inner.getter = Some(getter); + } + + let r = inner.getter.as_ref().unwrap(); + + unsafe { std::mem::transmute::<&dyn Fn(), &dyn Fn() -> T>(r) } + } +} + +pub(crate) struct Inner { + pub value: Box, + pub subscribers: Vec, + + // todo: this has a soundness hole in it that you might not run into + pub getter: Option>, +} diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 2cdfa9ff6..7ea53f83a 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-ssr" -version = "0.2.1" +version = "0.3.0" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus render-to-string" @@ -13,10 +13,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] } +dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] } [dev-dependencies] -dioxus = { path = "../dioxus" } +dioxus = { path = "../dioxus", version = "0.3.0" } thiserror = "1.0.23" log = "0.4.13" fern = { version = "0.6.0", features = ["colored"] } diff --git a/packages/tui/Cargo.toml b/packages/tui/Cargo.toml index 94aaf3296..5341d40f4 100644 --- a/packages/tui/Cargo.toml +++ b/packages/tui/Cargo.toml @@ -13,9 +13,9 @@ license = "MIT/Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus = { path = "../dioxus", version = "^0.2.1" } -dioxus-core = { path = "../core", version = "^0.2.1" } -dioxus-html = { path = "../html", version = "^0.2.1" } +dioxus = { path = "../dioxus", version = "^0.3.0" } +dioxus-core = { path = "../core", version = "^0.3.0" } +dioxus-html = { path = "../html", version = "^0.3.0" } dioxus-native-core = { path = "../native-core", version = "^0.2.0" } dioxus-native-core-macro = { path = "../native-core-macro", version = "^0.2.0" } @@ -27,7 +27,7 @@ futures = "0.3.19" taffy = "0.2.1" smallvec = "1.6" rustc-hash = "1.1.0" -anymap = "0.12.1" +anymap = "1.0.0-beta.2" futures-channel = "0.3.25" [dev-dependencies] diff --git a/packages/tui/examples/tui_all_events.rs b/packages/tui/examples/all_events.rs similarity index 100% rename from packages/tui/examples/tui_all_events.rs rename to packages/tui/examples/all_events.rs diff --git a/packages/tui/examples/tui_border.rs b/packages/tui/examples/border.rs similarity index 100% rename from packages/tui/examples/tui_border.rs rename to packages/tui/examples/border.rs diff --git a/packages/tui/examples/tui_buttons.rs b/packages/tui/examples/buttons.rs similarity index 100% rename from packages/tui/examples/tui_buttons.rs rename to packages/tui/examples/buttons.rs diff --git a/packages/tui/examples/tui_color_test.rs b/packages/tui/examples/color_test.rs similarity index 100% rename from packages/tui/examples/tui_color_test.rs rename to packages/tui/examples/color_test.rs diff --git a/packages/tui/examples/tui_colorpicker.rs b/packages/tui/examples/colorpicker.rs similarity index 100% rename from packages/tui/examples/tui_colorpicker.rs rename to packages/tui/examples/colorpicker.rs diff --git a/packages/tui/examples/tui_components.rs b/packages/tui/examples/components.rs similarity index 100% rename from packages/tui/examples/tui_components.rs rename to packages/tui/examples/components.rs diff --git a/packages/tui/examples/tui_flex.rs b/packages/tui/examples/flex.rs similarity index 100% rename from packages/tui/examples/tui_flex.rs rename to packages/tui/examples/flex.rs diff --git a/packages/tui/examples/tui_hover.rs b/packages/tui/examples/hover.rs similarity index 100% rename from packages/tui/examples/tui_hover.rs rename to packages/tui/examples/hover.rs diff --git a/packages/tui/examples/tui_list.rs b/packages/tui/examples/list.rs similarity index 100% rename from packages/tui/examples/tui_list.rs rename to packages/tui/examples/list.rs diff --git a/packages/tui/examples/tui_margin.rs b/packages/tui/examples/margin.rs similarity index 100% rename from packages/tui/examples/tui_margin.rs rename to packages/tui/examples/margin.rs diff --git a/packages/tui/examples/tui_quadrants.rs b/packages/tui/examples/quadrants.rs similarity index 100% rename from packages/tui/examples/tui_quadrants.rs rename to packages/tui/examples/quadrants.rs diff --git a/packages/tui/examples/tui_readme.rs b/packages/tui/examples/readme.rs similarity index 100% rename from packages/tui/examples/tui_readme.rs rename to packages/tui/examples/readme.rs diff --git a/packages/tui/examples/tui_task.rs b/packages/tui/examples/task.rs similarity index 100% rename from packages/tui/examples/tui_task.rs rename to packages/tui/examples/task.rs diff --git a/packages/tui/examples/tui_text.rs b/packages/tui/examples/text.rs similarity index 98% rename from packages/tui/examples/tui_text.rs rename to packages/tui/examples/text.rs index a6662da8c..1136b9a92 100644 --- a/packages/tui/examples/tui_text.rs +++ b/packages/tui/examples/text.rs @@ -12,7 +12,7 @@ fn app(cx: Scope) -> Element { width: "100%", height: "100%", flex_direction: "column", - onwheel: move |evt| alpha.set((**alpha + evt.inner().delta().strip_units().y as i64).min(100).max(0)), + onwheel: move |evt| alpha.set((**alpha + evt.inner().delta().strip_units().y as i64).clamp(0, 100)), p { background_color: "black", diff --git a/packages/tui/examples/tui_widgets.rs b/packages/tui/examples/widgets.rs similarity index 100% rename from packages/tui/examples/tui_widgets.rs rename to packages/tui/examples/widgets.rs diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs index 90d9e9da0..57f1c9e1f 100644 --- a/packages/tui/src/focus.rs +++ b/packages/tui/src/focus.rs @@ -79,7 +79,7 @@ impl NodeDepState for Focus { if let Some(index) = a .value .as_int() - .or_else(|| a.value.as_text().and_then(|v| v.parse::().ok())) + .or_else(|| a.value.as_text().and_then(|v| v.parse::().ok())) { match index.cmp(&0) { Ordering::Less => FocusLevel::Unfocusable, diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 40056eaaa..5ebecb601 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-web" -version = "0.2.1" +version = "0.3.0" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for the web browser using websys" @@ -11,9 +11,9 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] } -dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] } -dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [ +dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] } +dioxus-html = { path = "../html", version = "^0.3.0", features = ["wasm-bind"] } +dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0", features = [ "sledgehammer" ] } @@ -84,8 +84,8 @@ panic_hook = ["console_error_panic_hook"] hydrate = [] [dev-dependencies] -dioxus = { path = "../dioxus" } +dioxus = { path = "../dioxus", version = "0.3.0" } wasm-bindgen-test = "0.3.29" -dioxus-ssr = { path = "../ssr" } +dioxus-ssr = { path = "../ssr", version = "0.3.0"} wasm-logger = "0.2.0" dioxus-web = { path = "." } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 33d2c1b81..252add068 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,7 +7,9 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> -use dioxus_core::{ElementId, Mutation, Template, TemplateAttribute, TemplateNode}; +use dioxus_core::{ + BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, +}; use dioxus_html::{event_bubbles, CompositionData, FormData}; use dioxus_interpreter_js::{save_template, Channel}; use futures_channel::mpsc; @@ -175,16 +177,33 @@ impl WebsysDom { value, id, ns, - } => i.set_attribute(id.0 as u32, name, value, ns.unwrap_or_default()), - SetBoolAttribute { name, value, id } => { - i.set_attribute(id.0 as u32, name, if *value { "true" } else { "false" }, "") - } + } => match value { + BorrowedAttributeValue::Text(txt) => { + i.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default()) + } + BorrowedAttributeValue::Float(f) => { + i.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Int(n) => { + i.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Bool(b) => i.set_attribute( + id.0 as u32, + name, + if *b { "true" } else { "false" }, + ns.unwrap_or_default(), + ), + BorrowedAttributeValue::None => { + i.remove_attribute(id.0 as u32, name, ns.unwrap_or_default()) + } + _ => unreachable!(), + }, SetText { value, id } => i.set_text(id.0 as u32, value), NewEventListener { name, id, .. } => { - i.new_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8); + i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8); } RemoveEventListener { name, id } => { - i.remove_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8) + i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) } Remove { id } => i.remove(id.0 as u32), PushRoot { id } => i.push_root(id.0 as u32), @@ -210,11 +229,13 @@ pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) - "change" | "input" | "invalid" | "reset" | "submit" => read_input_to_data(target), - "click" | "contextmenu" | "dblclick" | "doubleclick" | "drag" | "dragend" | "dragenter" - | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" - | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { + "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" + | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { Rc::new(MouseData::from(event)) } + "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" + | "drop" => Rc::new(DragData::from(event)), + "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { Rc::new(PointerData::from(event)) diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 877f72403..db2d1c7a9 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -189,10 +189,12 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // if should_hydrate { // } else { - let edits = dom.rebuild(); + { + let edits = dom.rebuild(); - websys_dom.load_templates(&edits.templates); - websys_dom.apply_edits(edits.edits); + websys_dom.load_templates(&edits.templates); + websys_dom.apply_edits(edits.edits); + } // the mutations come back with nothing - we need to actually mount them websys_dom.mount();