diff --git a/.github/workflows/docs stable.yml b/.github/workflows/docs stable.yml new file mode 100644 index 000000000..2790b1d51 --- /dev/null +++ b/.github/workflows/docs stable.yml @@ -0,0 +1,39 @@ +name: docs stable + +on: + workflow_dispatch: + +jobs: + build-deploy: + runs-on: ubuntu-latest + environment: docs + steps: + + # NOTE: Comment out when https://github.com/rust-lang/mdBook/pull/1306 is merged and released + # - name: Setup mdBook + # uses: peaceiris/actions-mdbook@v1 + # with: + # mdbook-version: "0.4.10" + + # NOTE: Delete when the previous one is enabled + - name: Setup mdBook + run: | + cargo install mdbook --git https://github.com/Demonthos/mdBook.git --branch master + - uses: actions/checkout@v3 + + - name: Build + run: cd docs && + cd guide && mdbook build -d ../nightly/guide && cd .. && + cd router && mdbook build -d ../nightly/router && cd .. + # cd reference && mdbook build -d ../nightly/reference && cd .. && + # cd fermi && mdbook build -d ../nightly/fermi && cd .. + + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@v4.4.1 + with: + branch: gh-pages # The branch the action should deploy to. + folder: docs/nightly # The folder the action should deploy. + target-folder: docs + repository-name: dioxuslabs/docsite + clean: false + token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 968d5f103..07a8b462b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,13 +23,15 @@ 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 run: cd docs && cd guide && mdbook build -d ../nightly/guide && cd .. && - cd router && mdbook build -d ../nightly/router && cd .. + cd router && mdbook build -d ../nightly/router && cd .. + # cd reference && mdbook build -d ../nightly/reference && cd .. && + # cd fermi && mdbook build -d ../nightly/fermi && cd .. - name: Deploy 🚀 uses: JamesIves/github-pages-deploy-action@v4.4.1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ee5a763c7..96649c903 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -104,7 +104,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: clippy - args: --workspace -- -D warnings + args: --workspace --examples --tests -- -D warnings # Coverage is disabled until we can fix it # coverage: diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml new file mode 100644 index 000000000..73b38dec9 --- /dev/null +++ b/.github/workflows/miri.yml @@ -0,0 +1,124 @@ +name: Miri Tests + +on: + push: + # Run in PRs and for bors, but not on master. + branches: + - 'auto' + - 'try' + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - 'master' + schedule: + - cron: '6 6 * * *' # At 6:06 UTC every day. + +env: + CARGO_UNSTABLE_SPARSE_REGISTRY: 'true' + RUSTFLAGS: -Dwarnings + RUST_BACKTRACE: 1 + # Change to specific Rust release to pin + rust_stable: stable + rust_nightly: nightly-2022-11-03 + rust_clippy: 1.65.0 + # When updating this, also update: + # - README.md + # - tokio/README.md + # - CONTRIBUTING.md + # - tokio/Cargo.toml + # - tokio-util/Cargo.toml + # - tokio-test/Cargo.toml + # - tokio-stream/Cargo.toml + # rust_min: 1.49.0 + + + +jobs: + test: + runs-on: ${{ matrix.os }} + env: + RUST_BACKTRACE: 1 + HOST_TARGET: ${{ matrix.host_target }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + host_target: x86_64-unknown-linux-gnu + # - os: macos-latest + # host_target: x86_64-apple-darwin + # - os: windows-latest + # host_target: i686-pc-windows-msvc + # - os: windows-latest + # host_target: i686-pc-windows-msvc + # - os: windows-latest + # host_target: i686-pc-windows-msvc + # - os: windows-latest + # host_target: i686-pc-windows-msvc + steps: + - name: Set the tag GC interval to 1 on linux + if: runner.os == 'Linux' + run: echo "MIRIFLAGS=-Zmiri-tag-gc=1" >> $GITHUB_ENV + + - uses: actions/checkout@v3 + - name: Install Rust ${{ env.rust_nightly }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.rust_nightly }} + components: miri + - uses: Swatinem/rust-cache@v2 + - name: miri + # Many of tests in tokio/tests and doctests use #[tokio::test] or + # #[tokio::main] that calls epoll_create1 that Miri does not support. + # run: cargo miri test --features full --lib --no-fail-fast + run: | + cargo miri test --package dioxus-core --test miri_stress -- --exact --nocapture + cargo miri test --package dioxus-native-core --test miri_native -- --exact --nocapture + + # working-directory: tokio + env: + # todo: disable memory leaks ignore + MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks + PROPTEST_CASES: 10 + + # Cache the global cargo directory, but NOT the local `target` directory which + # we cannot reuse anyway when the nightly changes (and it grows quite large + # over time). + # - name: Add cache for cargo + # id: cache + # uses: actions/cache@v3 + # with: + # path: | + # # Taken from . + # ~/.cargo/bin + # ~/.cargo/registry/index + # ~/.cargo/registry/cache + # ~/.cargo/git/db + # # contains package information of crates installed via `cargo install`. + # ~/.cargo/.crates.toml + # ~/.cargo/.crates2.json + # key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + # restore-keys: ${{ runner.os }}-cargo + + # - name: Install rustup-toolchain-install-master + # if: ${{ steps.cache.outputs.cache-hit != 'true' }} + # shell: bash + # run: | + # cargo install -f rustup-toolchain-install-master + # - name: Install "master" toolchain + # shell: bash + # run: | + # if [[ ${{ github.event_name }} == 'schedule' ]]; then + # echo "Building against latest rustc git version" + # git ls-remote https://github.com/rust-lang/rust/ HEAD | cut -f 1 > rust-version + # fi + # toolchain --host ${{ matrix.host_target }} + # - name: Show Rust version + # run: | + # rustup show + # rustc -Vv + # cargo -V + # - name: Test + # run: | + # MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-ignore-leaks" cargo +nightly miri test --package dioxus-core --test miri_stress -- --exact --nocapture + # MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-ignore-leaks" cargo +nightly miri test --package dioxus-native-core --test miri_native -- --exact --nocapture 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/.vscode/settings.json b/.vscode/settings.json index 819ffec5d..ebd52887e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "editor.formatOnSave": true, "[toml]": { "editor.formatOnSave": false - } + }, + "rust-analyzer.checkOnSave.allTargets": false, } diff --git a/Cargo.toml b/Cargo.toml index 539dcb911..79f6fdbd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,13 @@ members = [ "packages/liveview", "packages/autofmt", "packages/rsx", - "packages/tui", + "packages/dioxus-tui", + "packages/rink", "packages/native-core", "packages/native-core-macro", + "packages/rsx-rosetta", + "packages/signals", + "packages/hot-reload", "docs/guide", ] @@ -40,9 +44,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/README.md b/README.md index 4ef6acfee..40981401e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ | Examples | - Guide + Guide | 中文 | diff --git a/docs/guide/Cargo.toml b/docs/guide/Cargo.toml index 0f544e071..227bdfa35 100644 --- a/docs/guide/Cargo.toml +++ b/docs/guide/Cargo.toml @@ -4,18 +4,24 @@ version = "0.0.1" edition = "2021" description = "Dioxus guide, including testable examples" license = "MIT/Apache-2.0" +publish = false [dev-dependencies] dioxus = { path = "../../packages/dioxus" } dioxus-desktop = { path = "../../packages/desktop" } dioxus-web = { path = "../../packages/web" } dioxus-ssr = { path = "../../packages/ssr" } +dioxus-native-core = { path = "../../packages/native-core" } +dioxus-native-core-macro = { path = "../../packages/native-core-macro" } dioxus-router = { path = "../../packages/router" } -dioxus-tui = { path = "../../packages/tui" } +dioxus-liveview = { path = "../../packages/liveview", features = ["axum"] } +dioxus-tui = { path = "../../packages/dioxus-tui" } fermi = { path = "../../packages/fermi" } +shipyard = "0.6.2" # dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] } 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"] } 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/custom_renderer.rs b/docs/guide/examples/custom_renderer.rs new file mode 100644 index 000000000..13993fe7b --- /dev/null +++ b/docs/guide/examples/custom_renderer.rs @@ -0,0 +1,311 @@ +use dioxus::html::input_data::keyboard_types::{Code, Key, Modifiers}; +use dioxus::prelude::*; +use dioxus_native_core::exports::shipyard::Component; +use dioxus_native_core::node_ref::*; +use dioxus_native_core::prelude::*; +use dioxus_native_core::utils::cursor::{Cursor, Pos}; +use dioxus_native_core_macro::partial_derive_state; + +// ANCHOR: state_impl +struct FontSize(f64); + +// All states need to derive Component +#[derive(Default, Debug, Copy, Clone, Component)] +struct Size(f64, f64); + +/// Derive some of the boilerplate for the State implementation +#[partial_derive_state] +impl State for Size { + type ParentDependencies = (); + + // The size of the current node depends on the size of its children + type ChildDependencies = (Self,); + + type NodeDependencies = (); + + // Size only cares about the width, height, and text parts of the current node + const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new() + // Get access to the width and height attributes + .with_attrs(AttributeMaskBuilder::Some(&["width", "height"])) + // Get access to the text of the node + .with_text(); + + fn update<'a>( + &mut self, + node_view: NodeView<()>, + _node: ::ElementBorrowed<'a>, + _parent: Option<::ElementBorrowed<'a>>, + children: Vec<::ElementBorrowed<'a>>, + context: &SendAnyMap, + ) -> bool { + let font_size = context.get::().unwrap().0; + let mut width; + let mut height; + if let Some(text) = node_view.text() { + // if the node has text, use the text to size our object + width = text.len() as f64 * font_size; + height = font_size; + } else { + // otherwise, the size is the maximum size of the children + width = children + .iter() + .map(|(item,)| item.0) + .reduce(|accum, item| if accum >= item { accum } else { item }) + .unwrap_or(0.0); + + height = children + .iter() + .map(|(item,)| item.1) + .reduce(|accum, item| if accum >= item { accum } else { item }) + .unwrap_or(0.0); + } + // if the node contains a width or height attribute it overrides the other size + for a in node_view.attributes().into_iter().flatten() { + match &*a.attribute.name { + "width" => width = a.value.as_float().unwrap(), + "height" => height = a.value.as_float().unwrap(), + // because Size only depends on the width and height, no other attributes will be passed to the member + _ => panic!(), + } + } + // to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed + let changed = (width != self.0) || (height != self.1); + *self = Self(width, height); + changed + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default, Component)] +struct TextColor { + r: u8, + g: u8, + b: u8, +} + +#[partial_derive_state] +impl State for TextColor { + // TextColor depends on the TextColor part of the parent + type ParentDependencies = (Self,); + + type ChildDependencies = (); + + type NodeDependencies = (); + + // TextColor only cares about the color attribute of the current node + const NODE_MASK: NodeMaskBuilder<'static> = + // Get access to the color attribute + NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"])); + + fn update<'a>( + &mut self, + node_view: NodeView<()>, + _node: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + _children: Vec<::ElementBorrowed<'a>>, + _context: &SendAnyMap, + ) -> bool { + // TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags + let new = match node_view + .attributes() + .and_then(|mut attrs| attrs.next()) + .and_then(|attr| attr.value.as_text()) + { + // if there is a color tag, translate it + Some("red") => TextColor { r: 255, g: 0, b: 0 }, + Some("green") => TextColor { r: 0, g: 255, b: 0 }, + Some("blue") => TextColor { r: 0, g: 0, b: 255 }, + Some(color) => panic!("unknown color {color}"), + // otherwise check if the node has a parent and inherit that color + None => match parent { + Some((parent,)) => *parent, + None => Self::default(), + }, + }; + // check if the member has changed + let changed = new != *self; + *self = new; + changed + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default, Component)] +struct Border(bool); + +#[partial_derive_state] +impl State for Border { + // TextColor depends on the TextColor part of the parent + type ParentDependencies = (Self,); + + type ChildDependencies = (); + + type NodeDependencies = (); + + // Border does not depended on any other member in the current node + const NODE_MASK: NodeMaskBuilder<'static> = + // Get access to the border attribute + NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"])); + + fn update<'a>( + &mut self, + node_view: NodeView<()>, + _node: ::ElementBorrowed<'a>, + _parent: Option<::ElementBorrowed<'a>>, + _children: Vec<::ElementBorrowed<'a>>, + _context: &SendAnyMap, + ) -> bool { + // check if the node contians a border attribute + let new = Self( + node_view + .attributes() + .and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border")) + .is_some(), + ); + // check if the member has changed + let changed = new != *self; + *self = new; + changed + } +} +// ANCHOR_END: state_impl + +// ANCHOR: rendering +fn main() -> Result<(), Box> { + fn app(cx: Scope) -> Element { + let count = use_state(cx, || 0); + + use_future(cx, (count,), |(count,)| async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + count.set(*count + 1); + } + }); + + cx.render(rsx! { + div{ + color: "red", + "{count}" + } + }) + } + + // create the vdom, the real_dom, and the binding layer between them + let mut vdom = VirtualDom::new(app); + let mut rdom: RealDom = RealDom::new([ + Border::to_type_erased(), + TextColor::to_type_erased(), + Size::to_type_erased(), + ]); + let mut dioxus_intigration_state = DioxusState::create(&mut rdom); + + let mutations = vdom.rebuild(); + // update the structure of the real_dom tree + dioxus_intigration_state.apply_mutations(&mut rdom, mutations); + let mut ctx = SendAnyMap::new(); + // set the font size to 3.3 + ctx.insert(FontSize(3.3)); + // update the State for nodes in the real_dom tree + let _to_rerender = rdom.update_state(ctx); + + // we need to run the vdom in a async runtime + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()? + .block_on(async { + loop { + // wait for the vdom to update + vdom.wait_for_work().await; + + // get the mutations from the vdom + let mutations = vdom.render_immediate(); + + // update the structure of the real_dom tree + dioxus_intigration_state.apply_mutations(&mut rdom, mutations); + + // update the state of the real_dom tree + let mut ctx = SendAnyMap::new(); + // set the font size to 3.3 + ctx.insert(FontSize(3.3)); + let _to_rerender = rdom.update_state(ctx); + + // render... + rdom.traverse_depth_first(|node| { + let indent = " ".repeat(node.height() as usize); + let color = *node.get::().unwrap(); + let size = *node.get::().unwrap(); + let border = *node.get::().unwrap(); + let id = node.id(); + let node = node.node_type(); + let node_type = &*node; + println!("{indent}{id:?} {color:?} {size:?} {border:?} {node_type:?}"); + }); + } + }) +} +// ANCHOR_END: rendering + +// ANCHOR: derive_state +// All states must derive Component (https://docs.rs/shipyard/latest/shipyard/derive.Component.html) +// They also must implement Default or provide a custom implementation of create in the State trait +#[derive(Default, Component)] +struct MyState; + +/// Derive some of the boilerplate for the State implementation +#[partial_derive_state] +impl State for MyState { + // The states of the parent nodes this state depends on + type ParentDependencies = (); + + // The states of the child nodes this state depends on + type ChildDependencies = (Self,); + + // The states of the current node this state depends on + type NodeDependencies = (); + + // The parts of the current text, element, or placeholder node in the tree that this state depends on + const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new(); + + // How to update the state of the current node based on the state of the parent nodes, child nodes, and the current node + // Returns true if the node was updated and false if the node was not updated + fn update<'a>( + &mut self, + // The view of the current node limited to the parts this state depends on + _node_view: NodeView<()>, + // The state of the current node that this state depends on + _node: ::ElementBorrowed<'a>, + // The state of the parent nodes that this state depends on + _parent: Option<::ElementBorrowed<'a>>, + // The state of the child nodes that this state depends on + _children: Vec<::ElementBorrowed<'a>>, + // The context of the current node used to pass global state into the tree + _context: &SendAnyMap, + ) -> bool { + todo!() + } + + // partial_derive_state will generate a default implementation of all the other methods +} +// ANCHOR_END: derive_state + +#[allow(unused)] +// ANCHOR: cursor +fn text_editing() { + let mut cursor = Cursor::default(); + let mut text = String::new(); + + // handle keyboard input with a max text length of 10 + cursor.handle_input( + &Code::ArrowRight, + &Key::ArrowRight, + &Modifiers::empty(), + &mut text, + 10, + ); + + // mannually select text between characters 0-5 on the first line (this could be from dragging with a mouse) + cursor.start = Pos::new(0, 0); + cursor.end = Some(Pos::new(5, 0)); + + // delete the selected text and move the cursor to the start of the selection + cursor.delete_selection(&mut text); +} +// ANCHOR_END: cursor 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..c88dc2f53 --- /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_composed.rs b/docs/guide/examples/hooks_composed.rs index 5b1f5ebfe..801474aa3 100644 --- a/docs/guide/examples/hooks_composed.rs +++ b/docs/guide/examples/hooks_composed.rs @@ -7,7 +7,7 @@ fn main() {} struct AppSettings {} // ANCHOR: wrap_context -fn use_settings(cx: &ScopeState) -> UseSharedState { +fn use_settings(cx: &ScopeState) -> &UseSharedState { use_shared_state::(cx).expect("App settings not provided") } // ANCHOR_END: wrap_context 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..f9f31ed52 100644 --- a/docs/guide/src/en/SUMMARY.md +++ b/docs/guide/src/en/SUMMARY.md @@ -5,10 +5,11 @@ - [Getting Started](getting_started/index.md) - [Desktop](getting_started/desktop.md) - [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) + - [Hot Reloading](getting_started/hot_reload.md) - [Describing the UI](describing_ui/index.md) - [Special Attributes](describing_ui/special_attributes.md) - [Components](describing_ui/components.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..537e27ea4 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,14 +50,50 @@ 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 +use futures_util::stream::StreamExt; + enum ProfileUpdate { SetUsername(String), SetAge(i32) @@ -83,6 +119,10 @@ cx.render(rsx!{ }) ``` + +> Note: In order to use/run the `rx.next().await` statement you will need to extend the [`Stream`] trait (used by [`UnboundedReceiver`]) by adding 'futures_util' as a dependency to your project and adding the `use futures_util::stream::StreamExt;`. + + For sufficiently complex apps, we could build a bunch of different useful "services" that loop on channels to update the app. ```rust @@ -103,7 +143,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(); @@ -130,6 +170,8 @@ fn Banner(cx: Scope) -> Element { Now, in our sync service, we can structure our state however we want. We only need to update the view values when ready. ```rust +use futures_util::stream::StreamExt; + enum SyncAction { SetUsername(String), } @@ -152,27 +194,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..2d4ddb759 100644 --- a/docs/guide/src/en/custom_renderer/index.md +++ b/docs/guide/src/en/custom_renderer/index.md @@ -2,7 +2,7 @@ Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer. -Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but only really require processing `DomEdits` and sending `UserEvents`. +Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but only really require processing `Mutations` and sending `UserEvents`. ## The specifics: @@ -11,153 +11,225 @@ 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. +The Dioxus diffing mechanism operates as a [stack machine](https://en.wikipedia.org/wiki/Stack_machine) where the [LoadTemplate](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.LoadTemplate), [CreatePlaceholder](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.CreatePlaceholder), and [CreateTextNode](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.CreateTextNode) mutations pushes a new "real" DOM node onto the stack and [AppendChildren](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.AppendChildren), [InsertAfter](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.InsertAfter), [InsertBefore](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.InsertBefore), [ReplacePlaceholder](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.ReplacePlaceholder), and [ReplaceWith](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.ReplaceWith) all remove nodes from the stack. +## Node storage -### An example +Dioxus saves and loads elements with IDs. Inside the VirtualDOM, this is just tracked as as a u64. -For the sake of understanding, lets consider this example – a very simple UI declaration: +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 id is used in a mutation. 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. + +### An Example + +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. +#### Building Templates -When the renderer receives this instruction, it pushes the actual Node onto its own stack. The real renderer's stack will look like this: +The above rsx will create a template that contains one static h1 tag and a placeholder for a dynamic text node. The template contains the static parts of the UI, and ids for the dynamic parts along with the paths to access them. + +The template will look something like this: + +```rust +Template { + // Some id that is unique for the entire project + name: "main.rs:1:1:0", + // The root nodes of the template + roots: &[ + TemplateNode::Element { + tag: "h1", + namespace: None, + attrs: &[], + children: &[ + TemplateNode::DynamicText { + id: 0 + }, + ], + } + ], + // the path to each of the dynamic nodes + node_paths: &[ + // the path to dynamic node with a id of 0 + &[ + // on the first root node + 0, + // the first child of the root node + 0, + ] + ], + // the path to each of the dynamic attributes + attr_paths: &'a [&'a [u8]], +} +``` + +> For more detailed docs about the struture of templates see the [Template api docs](https://docs.rs/dioxus-core/latest/dioxus_core/prelude/struct.Template.html) + +This template will be sent to the renderer in the [list of templates](https://docs.rs/dioxus-core/latest/dioxus_core/struct.Mutations.html#structfield.templates) supplied with the mutations the first time it is used. Any time the renderer encounters a [LoadTemplate](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.LoadTemplate) mutation after this, it should clone the template and store it in the given id. + +For dynamic nodes and dynamic text nodes, a placeholder node should be created and inserted into the UI so that the node can be modified later. + +In HTML renderers, this template could look like this: + +```html +

""

+``` + +#### Applying Mutations + +After the renderer has created all of the new templates, it can begin to process the mutations. + +When the renderer starts, it should contain the Root node on the stack and store the Root node with an id of 0. The Root node is the top-level node of the UI. In HTML, this is the `
` element. + +```rust +instructions: [] +stack: [ + RootNode, +] +nodes: [ + RootNode, +] +``` + +The first mutation is a `LoadTemplate` mutation. This tells the renderer to load a root from the template with the given id. The renderer will then push the root node of the template onto the stack and store it with an id for later. In this case, the root node is an h1 element. ```rust instructions: [ - PushRoot(Container) + LoadTemplate { + // the id of the template + name: "main.rs:1:1:0", + // the index of the root node in the template + index: 0, + // the id to store + id: ElementId(1), + } ] stack: [ - ContainerNode, + RootNode, +

""

, +] +nodes: [ + RootNode, +

""

, ] ``` -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 create the dynamic text node. The diff algorithm decides that this node needs to be created, so Dioxus will generate the Mutation `HydrateText`. When the renderer receives this instruction, it will navigate to the placeholder text node in the template and replace it with the new text. ```rust instructions: [ - PushRoot(Container), - CreateElement(h1), + LoadTemplate { + name: "main.rs:1:1:0", + index: 0, + id: ElementId(1), + }, + HydrateText { + // the id to store the text node + id: ElementId(2), + // the text to set + text: "count: 0", + } ] stack: [ - ContainerNode, - h1, + RootNode, +

"count: 0"

, +] +nodes: [ + RootNode, +

"count: 0"

, + "count: 0", ] ``` -Next, Dioxus sees the text node, and generates the `CreateTextNode` DomEdit: -```rust -instructions: [ - PushRoot(Container), - CreateElement(h1), - CreateTextNode("hello world") -] -stack: [ - ContainerNode, - h1, - "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 h1 node is not attached to anything (it is unmounted) so Dioxus needs to generate an Edit that connects the h1 node to the Root. It depends on the situation, but in this case, we use `AppendChildren`. This pops the text node off the stack, leaving the Root element as the next element on the stack. ```rust instructions: [ - PushRoot(Container), - CreateElement(h1), - CreateTextNode("hello world"), - AppendChildren(1) + LoadTemplate { + name: "main.rs:1:1:0", + index: 0, + id: ElementId(1), + }, + HydrateText { + id: ElementId(2), + text: "count: 0", + }, + AppendChildren { + // the id of the parent node + id: ElementId(0), + // the number of nodes to pop off the stack and append + m: 1 + } ] stack: [ - ContainerNode, - h1 + RootNode, +] +nodes: [ + RootNode, +

"count: 0"

, + "count: 0", ] ``` -We call `AppendChildren` again, popping off the h1 node and attaching it to the parent: -```rust -instructions: [ - PushRoot(Container), - CreateElement(h1), - CreateTextNode("hello world"), - AppendChildren(1), - AppendChildren(1) -] -stack: [ - ContainerNode, -] -``` -Finally, the container is popped since we don't need it anymore. -```rust -instructions: [ - PushRoot(Container), - CreateElement(h1), - CreateTextNode("hello world"), - AppendChildren(1), - AppendChildren(1), - PopRoot -] -stack: [] -``` + Over time, our stack looked like this: + ```rust -[] -[Container] -[Container, h1] -[Container, h1, "hello world"] -[Container, h1] -[Container] -[] +[Root] +[Root,

""

] +[Root,

"count: 0"

] +[Root] ``` -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. +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. - -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. +This little demo serves to show exactly how a Renderer would need to process a mutation stream to build UIs. ## Event loop -Like most GUIs, Dioxus relies on an event loop to progress the VirtualDOM. The VirtualDOM itself can produce events as well, so it's important that your custom renderer can handle those too. +Like most GUIs, Dioxus relies on an event loop to progress the VirtualDOM. The VirtualDOM itself can produce events as well, so it's important for your custom renderer can handle those too. The code for the WebSys implementation is straightforward, so we'll add it here to demonstrate how simple an event loop is: -```rust +```rust, ignore pub async fn run(&mut self) -> dioxus_core::error::Result<()> { // Push the body element onto the WebsysDom's stack machine let mut websys_dom = crate::new::WebsysDom::new(prepare_websys_dom()); @@ -185,9 +257,9 @@ pub async fn run(&mut self) -> dioxus_core::error::Result<()> { } ``` -It's important that you decode the real events from your event system into Dioxus' synthetic event system (synthetic meaning abstracted). This simply means matching your event type and creating a Dioxus `UserEvent` type. Right now, the VirtualEvent system is modeled almost entirely around the HTML spec, but we are interested in slimming it down. +It's important to decode what the real events are for your event system into Dioxus' synthetic event system (synthetic meaning abstracted). This simply means matching your event type and creating a Dioxus `UserEvent` type. Right now, the virtual event system is modeled almost entirely around the HTML spec, but we are interested in slimming it down. -```rust +```rust, ignore fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { match event.type_().as_str() { "keydown" => { @@ -219,40 +291,23 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { ## Custom raw elements -If you need to go as far as relying on custom elements for your renderer – you totally can. This still enables you to use Dioxus' reactive nature, component system, shared state, and other features, but will ultimately generate different nodes. All attributes and listeners for the HTML and SVG namespace are shuttled through helper structs that essentially compile away (pose no runtime overhead). You can drop in your own elements any time you want, with little hassle. However, you must be absolutely sure your renderer can handle the new type, or it will crash and burn. +If you need to go as far as relying on custom elements/attributes for your renderer – you totally can. This still enables you to use Dioxus' reactive nature, component system, shared state, and other features, but will ultimately generate different nodes. All attributes and listeners for the HTML and SVG namespace are shuttled through helper structs that essentially compile away. You can drop in your elements any time you want, with little hassle. However, you must be sure your renderer can handle the new namespace. -These custom elements are defined as unit structs with trait implementations. - -For example, the `div` element is (approximately!) defined as such: - -```rust -struct div; -impl div { - /// Some glorious documentation about the class property. - const TAG_NAME: &'static str = "div"; - const NAME_SPACE: Option<&'static str> = None; - // define the class attribute - pub fn class<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { - cx.attr("class", val, None, false) - } - // more attributes -} -``` - -You've probably noticed that many elements in the `rsx!` macros support on-hover documentation. The approach we take to custom elements means that the unit struct is created immediately where the element is used in the macro. When the macro is expanded, the doc comments still apply to the unit struct, giving tons of in-editor feedback, even inside a proc macro. +For more examples and information on how to create custom namespaces, see the [`dioxus_html` crate](https://github.com/DioxusLabs/dioxus/blob/master/packages/html/README.md#how-to-extend-it). # 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, the [native-core](https://github.com/DioxusLabs/dioxus/tree/master/packages/native-core) crate provides some utilities to implement a renderer. It provides an abstraction over Mutations and Templates and contains helpers that can handle the layout and text editing for you. -## RealDom +## The 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 uses an entity component system to manage the state of nodes. This system allows you to modify insert and modify arbitrary components on nodes. On top of this, the RealDom provides a way to manage a tree of nodes, and the State trait provides a way to automatically add and update these components when the tree is modified. It also provides a way to apply `Mutations` to the RealDom. ### 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,11 +320,11 @@ 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 layout depends on the children's layout, the current text, and the text size. The border depends on only the current node. In the following diagram arrows represent dataflow: -[![](https://mermaid.ink/img/pako:eNqdVNFqgzAU_RXJXizUUZPJmIM-jO0LukdhpCbO0JhIGteW0n9fNK1Oa0brfUnu9VxyzzkXjyCVhIIYZFzu0hwr7X2-JcIzsa3W3wqXuZdKoele22oddfa1Y0Tnfn31muvMfqeCDNq3GmvaNROmaKqZFO1DPTRhP8MOd1fTWYNDvzlmQbBMJZcq9JtjNgY1mLVUhBqQPQeojl3wGCw5PsjqnIe-zXqEL8GZ2Kz0gVMPmoeU3ND4IcuiaLGY2zRouuKncv_qGKv3VodpJe0JVU6QCQ5kgqMyWQVr8hbk4hm1PBcmsuwmnrCVH94rP7xN_ucp8sOB_EPSfz9drYVrkpc_AmH8_yTjJueUc-ntpOJkgt2os9tKjcYlt-DLUiD3UsB2KZCLcwjv3Aq33-g2v0M0xXA0MBy5DUdXi-gcJZriuLmAOSioKjAj5ld8rMsJ0DktaAJicyVYbRKQiJPBVSUx438QpqUCcYb5ls4BrrRcHUTaFizqnWGzR8W5evoFI-bJdw)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqdVNFqgzAU_RXJXizUUZPJmIM-jO0LukdhpCbO0JhIGteW0n9fNK1Oa0brfUnu9VxyzzkXjyCVhIIYZFzu0hwr7X2-JcIzsa3W3wqXuZdKoele22oddfa1Y0Tnfn31muvMfqeCDNq3GmvaNROmaKqZFO1DPTRhP8MOd1fTWYNDvzlmQbBMJZcq9JtjNgY1mLVUhBqQPQeojl3wGCw5PsjqnIe-zXqEL8GZ2Kz0gVMPmoeU3ND4IcuiaLGY2zRouuKncv_qGKv3VodpJe0JVU6QCQ5kgqMyWQVr8hbk4hm1PBcmsuwmnrCVH94rP7xN_ucp8sOB_EPSfz9drYVrkpc_AmH8_yTjJueUc-ntpOJkgt2os9tKjcYlt-DLUiD3UsB2KZCLcwjv3Aq33-g2v0M0xXA0MBy5DUdXi-gcJZriuLmAOSioKjAj5ld8rMsJ0DktaAJicyVYbRKQiJPBVSUx438QpqUCcYb5ls4BrrRcHUTaFizqnWGzR8W5evoFI-bJdw) +[![](https://mermaid.ink/img/pako:eNqllV1vgjAUhv8K6W4wkQVa2QdLdrHsdlfukmSptEhjoaSWqTH-9xVwONAKst70g5739JzzlO5BJAgFAYi52EQJlsr6fAszS7d1sVhKnCdWJDJFt6peLVs5-9owohK7HFrVcFJ_pxnpmK8VVvRkTJikkWIiaxy1dhP23bUwW1WW5WbPrrqJ4ziR4EJ6dtVN2ls5y1ZztePUcrWZFCvqVEcPPDffvlyS1XoLIQnVgnVvVPR6FU9Zc-6dV453ojjOPbuetRJ57gIeXQR3cez7rjtteZyZQ2j5MqmjqwE0ZW0VKx9RKtgpFewp1aw3sXXFy6TWgiYlv8mfq1scD8ofbBCAfQg8_AMBOAyBxzEIwA4CxgQ99QbQkjnD2KT7_CfxGF8_9WXQEsq5sDZCcjICOXRCri4h6r3NA38Q6Jdi1EOx5w3DGDYYI6MUvJFjM3VoGHUeGoMd6mBnDmh2E3fo7O4Yhf0x4OkBmIKUyhQzol_GfbkcApXQlIYg0EOC5SoEYXbQ-3ChxHyXRSBQsqBTUOREx_7OsAY3BUGM-VqvUsKUkB_1U6vf05gtweEHTk4_HQ?type=png)](https://mermaid.live/edit#pako:eNqllV1vgjAUhv8K6W4wkQVa2QdLdrHsdlfukmSptEhjoaSWqTH-9xVwONAKst70g5739JzzlO5BJAgFAYi52EQJlsr6fAszS7d1sVhKnCdWJDJFt6peLVs5-9owohK7HFrVcFJ_pxnpmK8VVvRkTJikkWIiaxy1dhP23bUwW1WW5WbPrrqJ4ziR4EJ6dtVN2ls5y1ZztePUcrWZFCvqVEcPPDffvlyS1XoLIQnVgnVvVPR6FU9Zc-6dV453ojjOPbuetRJ57gIeXQR3cez7rjtteZyZQ2j5MqmjqwE0ZW0VKx9RKtgpFewp1aw3sXXFy6TWgiYlv8mfq1scD8ofbBCAfQg8_AMBOAyBxzEIwA4CxgQ99QbQkjnD2KT7_CfxGF8_9WXQEsq5sDZCcjICOXRCri4h6r3NA38Q6Jdi1EOx5w3DGDYYI6MUvJFjM3VoGHUeGoMd6mBnDmh2E3fo7O4Yhf0x4OkBmIKUyhQzol_GfbkcApXQlIYg0EOC5SoEYXbQ-3ChxHyXRSBQsqBTUOREx_7OsAY3BUGM-VqvUsKUkB_1U6vf05gtweEHTk4_HQ) [//]: # "%% mermaid flow chart" [//]: # "flowchart TB" @@ -280,217 +335,79 @@ In the following diagram arrows represent dataflow: [//]: # " direction TB" [//]: # " subgraph div state" [//]: # " direction TB" -[//]: # " state1(state)-->color1(color)" -[//]: # " state1-->border1(border)" +[//]: # " state1(state)---color1(color)" +[//]: # " linkStyle 0 stroke-width:10px;" +[//]: # " state1---border1(border)" +[//]: # " linkStyle 1 stroke-width:10px;" [//]: # " text_width-.->layout_width1(layout width)" [//]: # " linkStyle 2 stroke:#ff5500,stroke-width:4px;" -[//]: # " state1-->layout_width1" +[//]: # " state1---layout_width1" +[//]: # " linkStyle 3 stroke-width:10px;" [//]: # " end" [//]: # " subgraph p state" [//]: # " direction TB" -[//]: # " state2(state)-->color2(color)" +[//]: # " state2(state)---color2(color)" +[//]: # " linkStyle 4 stroke-width:10px;" [//]: # " color1-.->color2" [//]: # " linkStyle 5 stroke:#0000ff,stroke-width:4px;" -[//]: # " state2-->border2(border)" +[//]: # " state2---border2(border)" +[//]: # " linkStyle 6 stroke-width:10px;" [//]: # " text_width-.->layout_width2(layout width)" [//]: # " linkStyle 7 stroke:#ff5500,stroke-width:4px;" -[//]: # " state2-->layout_width2" +[//]: # " state2---layout_width2" +[//]: # " linkStyle 8 stroke-width:10px;" [//]: # " layout_width2-.->layout_width1" [//]: # " linkStyle 9 stroke:#00aa00,stroke-width:4px;" [//]: # " end" [//]: # " subgraph hello world state" [//]: # " direction TB" -[//]: # " state3(state)-->border3(border)" -[//]: # " state3-->color3(color)" +[//]: # " state3(state)---border3(border)" +[//]: # " linkStyle 10 stroke-width:10px;" +[//]: # " state3---color3(color)" +[//]: # " linkStyle 11 stroke-width:10px;" [//]: # " color2-.->color3" [//]: # " linkStyle 12 stroke:#0000ff,stroke-width:4px;" [//]: # " text_width-.->layout_width3(layout width)" [//]: # " linkStyle 13 stroke:#ff5500,stroke-width:4px;" -[//]: # " state3-->layout_width3" +[//]: # " state3---layout_width3" +[//]: # " linkStyle 14 stroke-width:10px;" [//]: # " layout_width3-.->layout_width2" [//]: # " linkStyle 15 stroke:#00aa00,stroke-width:4px;" [//]: # " 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 the State trait and a RealDom struct. The State trait provides a way to describe how states in a node depend on other states in its relatives. By describing how to update a single node from its relations, native-core will derive a way to update the states of all nodes for you. 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::*; -use dioxus_native_core::state::{ChildDepState, NodeDepState, ParentDepState, State}; -use dioxus_native_core_macro::{sorted_str_slice, State}; +Native Core cannot create all of the required methods for the State trait, but it can derive some of them. To implement the State trait, you must implement the following methods and let the `#[partial_derive_state]` macro handle the rest: -#[derive(Default, Copy, Clone)] -struct Size(f32, f32); -// Size only depends on the current node and its children, so it implements ChildDepState -impl ChildDepState for Size { - // Size accepts a font size context - type Ctx = f32; - // Size depends on the Size part of each child - type DepState = Self; - // Size only cares about the width, height, and text parts of the current node - const NODE_MASK: NodeMask = - NodeMask::new_with_attrs(AttributeMask::Static(&sorted_str_slice!(["width", "height"]))).with_text(); - fn reduce<'a>( - &mut self, - node: NodeView, - children: impl Iterator, - ctx: &Self::Ctx, - ) -> bool - where - Self::DepState: 'a, - { - let mut width; - let mut height; - if let Some(text) = node.text() { - // if the node has text, use the text to size our object - width = text.len() as f32 * ctx; - height = *ctx; - } else { - // otherwise, the size is the maximum size of the children - width = children - .by_ref() - .map(|item| item.0) - .reduce(|accum, item| if accum >= item { accum } else { item }) - .unwrap_or(0.0); - - height = children - .map(|item| item.1) - .reduce(|accum, item| if accum >= item { accum } else { item }) - .unwrap_or(0.0); - } - // if the node contains a width or height attribute it overrides the other size - for a in node.attributes(){ - match a.name{ - "width" => width = a.value.as_float32().unwrap(), - "height" => height = a.value.as_float32().unwrap(), - // because Size only depends on the width and height, no other attributes will be passed to the member - _ => panic!() - } - } - // to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed - let changed = (width != self.0) || (height != self.1); - *self = Self(width, height); - changed - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Default)] -struct TextColor { - r: u8, - g: u8, - b: u8, -} -// TextColor only depends on the current node and its parent, so it implements ParentDepState -impl ParentDepState for TextColor { - type Ctx = (); - // TextColor depends on the TextColor part of the parent - type DepState = Self; - // TextColor only cares about the color attribute of the current node - const NODE_MASK: NodeMask = NodeMask::new_with_attrs(AttributeMask::Static(&["color"])); - fn reduce( - &mut self, - node: NodeView, - parent: Option<&Self::DepState>, - _ctx: &Self::Ctx, - ) -> bool { - // TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags - let new = match node.attributes().next().map(|attr| attr.name) { - // if there is a color tag, translate it - Some("red") => TextColor { r: 255, g: 0, b: 0 }, - Some("green") => TextColor { r: 0, g: 255, b: 0 }, - Some("blue") => TextColor { r: 0, g: 0, b: 255 }, - Some(_) => panic!("unknown color"), - // otherwise check if the node has a parent and inherit that color - None => match parent { - Some(parent) => *parent, - None => Self::default(), - }, - }; - // check if the member has changed - let changed = new != *self; - *self = new; - changed - } -} - -#[derive(Debug, Clone, PartialEq, Default)] -struct Border(bool); -// TextColor only depends on the current node, so it implements NodeDepState -impl NodeDepState<()> for Border { - type Ctx = (); - - // Border does not depended on any other member in the current node - const NODE_MASK: NodeMask = - NodeMask::new_with_attrs(AttributeMask::Static(&["border"])); - fn reduce(&mut self, node: NodeView, _sibling: (), _ctx: &Self::Ctx) -> bool { - // check if the node contians a border attribute - let new = Self(node.attributes().next().map(|a| a.name == "border").is_some()); - // check if the member has changed - let changed = new != *self; - *self = new; - changed - } -} - -// State provides a derive macro, but anotations on the members are needed in the form #[dep_type(dep_member, CtxType)] -#[derive(State, Default, Clone)] -struct ToyState { - // the color member of it's parent and no context - #[parent_dep_state(color)] - color: TextColor, - // depends on the node, and no context - #[node_dep_state()] - border: Border, - // depends on the layout_width member of children and f32 context (for text size) - #[child_dep_state(size, f32)] - size: Size, -} +```rust, ignore +{{#include ../../../examples/custom_renderer.rs:derive_state}} ``` -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. +Lets take a look at how to implement the State trait for a simple renderer. + ```rust -fn main(){ - fn app(cx: Scope) -> Element { - cx.render(rsx!{ - div{ - color: "red", - "hello world" - } - }) - } - let vdom = VirtualDom::new(app); - let rdom: RealDom = RealDom::new(); +{{#include ../../../examples/custom_renderer.rs:state_impl}} +``` - let mutations = dom.rebuild(); - // update the structure of the real_dom tree - let to_update = rdom.apply_mutations(vec![mutations]); - let mut ctx = AnyMap::new(); - // set the font size to 3.3 - ctx.insert(3.3f32); - // update the ToyState for nodes in the real_dom tree - let _to_rerender = rdom.update_state(&dom, to_update, ctx).unwrap(); +Now that we have our state, we can put it to use in our RealDom. We can update the RealDom with apply_mutations to update the structure of the dom (adding, removing, and changing properties of nodes) and then update_state to update the States for each of the nodes that changed. - // we need to run the vdom in a async runtime - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()? - .block_on(async { - loop{ - let wait = vdom.wait_for_work(); - let mutations = vdom.work_with_deadline(|| false); - let to_update = rdom.apply_mutations(mutations); - let mut ctx = AnyMap::new(); - ctx.insert(3.3); - let _to_rerender = rdom.update_state(vdom, to_update, ctx).unwrap(); - - // render... - } - }) -} +```rust +{{#include ../../../examples/custom_renderer.rs:rendering}} ``` ## 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](https://docs.rs/dioxus-native-core/latest/dioxus_native_core/layout_attributes/index.html) module provides a way to apply HTML attributes a [Taffy](https://docs.rs/taffy/latest/taffy/index.html) layout style. + +## Text Editing + +To make it easier to implement text editing in rust renderers, `native-core` also contains a renderer-agnostic cursor system. The cursor can handle text editing, selection, and movement with common keyboard shortcuts integrated. + +```rust +{{#include ../../../examples/custom_renderer.rs:cursor}} +``` ## 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 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..311ad2c9f 100644 --- a/docs/guide/src/en/getting_started/desktop.md +++ b/docs/guide/src/en/getting_started/desktop.md @@ -1,20 +1,57 @@ -# 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. +Dioxus Desktop is built off [Tauri](https://tauri.app/). Right now there aren't any Dioxus abstractions over the 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 @@ -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..599ce57da 100644 --- a/docs/guide/src/en/getting_started/hot_reload.md +++ b/docs/guide/src/en/getting_started/hot_reload.md @@ -1,25 +1,49 @@ # 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. +3. Currently the cli only implements hot reloading for the web renderer. For TUI, desktop, and LiveView you can use the hot reload macro instead. -# Setup +# Web +For the web renderer, you can use the dioxus cli to serve your application with hot reloading enabled. + +## 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: -``` +## Usage +1. Run: +```bash dioxus serve --hot-reload ``` -2. change some code within a rsx macro -3. open your localhost in a browser -4. save and watch the style change without recompiling +2. Change some code within a rsx or render macro +3. Open your localhost in a browser +4. Save and watch the style change without recompiling + +# Desktop/Liveview/TUI +For desktop, LiveView, and tui, you can place the hot reload macro at the top of your main function to enable hot reloading. +Hot reloading is automatically enabled on debug builds. + +For more information about hot reloading on native platforms and configuration options see the [dioxus-hot-reload](https://crates.io/crates/dioxus-hot-reload) crate. + +## Setup +Add the following to your main function: + +```rust +fn main() { + hot_reload_init!(); + // launch your application +} +``` + +## Usage +1. Run: +```bash +cargo run +``` +2. Change some code within a rsx or render macro +3. 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 require a full recompile to capture the expression. +2. Components, Iterators, and some attributes can contain arbitrary rust code and will trigger a full recompile when changed. 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..e49cb84a8 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/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/custom_hooks.md b/docs/guide/src/en/interactivity/custom_hooks.md index 2f0155cb5..07472bb95 100644 --- a/docs/guide/src/en/interactivity/custom_hooks.md +++ b/docs/guide/src/en/interactivity/custom_hooks.md @@ -14,7 +14,7 @@ For example, if many components need to access an `AppSettings` struct, you can ## Custom Hook Logic -You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.use_hook) to build your own hooks. In fact, this is what all the standard hooks are built on! +You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.use_hook) to build your own hooks. In fact, this is what all the standard hooks are built on! `use_hook` accepts a single closure for initializing the hook. It will be only run the first time the component is rendered. The return value of that closure will be used as the value of the hook – Dioxus will take it, and store it for as long as the component is alive. On every render (not just the first one!), you will get a reference to this value. @@ -22,5 +22,5 @@ You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct. Inside the initialization closure, you will typically make calls to other `cx` methods. For example: -- The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.schedule_update) to make Dioxus re-render the component whenever it changes. -- The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope \ No newline at end of file +- The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.schedule_update) to make Dioxus re-render the component whenever it changes. +- The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope 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/docs/posts/release.md b/docs/posts/release.md index b40b451cc..171664dd3 100644 --- a/docs/posts/release.md +++ b/docs/posts/release.md @@ -63,7 +63,7 @@ This very site is built with Dioxus, and the source code is available [here](htt To get started with Dioxus, check out any of the "Getting Started" guides for your platform of choice, or check out the GitHub Repository for more details. -- [Getting Started with Dioxus](https://dioxuslabs.com/guide) +- [Getting Started with Dioxus](https://dioxuslabs.com/guide/en) - [Getting Started with Web](https://dioxuslabs.com/reference/web) - [Getting Started with Desktop](https://dioxuslabs.com/reference/desktop) - [Getting Started with Mobile](https://dioxuslabs.com/reference/mobile) @@ -163,7 +163,7 @@ Today, to publish a Dioxus app, you don't need NPM/WebPack/Parcel/etc. Dioxus si ## Show me more -Here, we'll dive into some features of Dioxus and why it's so fun to use. The [guide](https://dioxuslabs.com/guide/) serves as a deeper and more comprehensive look at what Dioxus can do. +Here, we'll dive into some features of Dioxus and why it's so fun to use. The [guide](https://dioxuslabs.com/guide/en/) serves as a deeper and more comprehensive look at what Dioxus can do. ## Building a new project is simple diff --git a/docs/reference/src/README_pt-br.md b/docs/reference/src/README_pt-br.md index c345da6bd..e76f43516 100644 --- a/docs/reference/src/README_pt-br.md +++ b/docs/reference/src/README_pt-br.md @@ -4,7 +4,7 @@ **Dioxus** é um framework e ecossistema para desenvolver interfaces rápidas, escaláveis e robustas com a linguagem de Programação Rust. Este guia irá ajudar você a começar com o Dioxus para Web, Desktop, Móvel e mais. -> Este livro é a Referência e Guias Avançados para o framework Dioxus. Para um tutorial em como de fato _usar_ o Dioxus, procure o [guia oficial](https://dioxuslabs.com/guide/). +> Este livro é a Referência e Guias Avançados para o framework Dioxus. Para um tutorial em como de fato _usar_ o Dioxus, procure o [guia oficial](https://dioxuslabs.com/guide/en/). ## Guias e Referência diff --git a/docs/reference/src/platforms/desktop.md b/docs/reference/src/platforms/desktop.md index d444b90e4..dcdcca778 100644 --- a/docs/reference/src/platforms/desktop.md +++ b/docs/reference/src/platforms/desktop.md @@ -2,7 +2,7 @@ One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. 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. -Dioxus Desktop is built off Tauri. 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. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. +Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over the 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. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. ## Getting Set up @@ -42,4 +42,4 @@ To configure the webview, menubar, and other important desktop-specific features ## Future Steps -Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! +Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide/en) if you already haven't! diff --git a/examples/README.md b/examples/README.md index 2533722ed..3963d186d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,8 +18,6 @@ cargo run --example hello_world [custom_assets](./custom_assets.rs) - Include images -[custom_element](./custom_element.rs) - Render webcomponents - [custom_html](./custom_html.rs) - Customize wrapper HTML [eval](./eval.rs) - Evaluate JS expressions @@ -132,36 +130,6 @@ cargo run --example hello_world [todomvc](./todomvc.rs) - Todo task list example -## Terminal UI - -[tui_all_events](../packages/tui/examples/tui_all_events.rs) - All of the terminal events - -[tui_border](../packages/tui/examples/tui_border.rs) - Different styles of borders and corners - -[tui_buttons](../packages/tui/examples/tui_buttons.rs) - A grid of buttons that work demonstrate the focus system - -[tui_color_test](../packages/tui/examples/tui_color_test.rs) - Grid of colors to demonstrate compatablility with different terminal color rendering modes - -[tui_colorpicker](../packages/tui/examples/tui_colorpicker.rs) - A colorpicker - -[tui_components](../packages/tui/examples/tui_components.rs) - Simple component example - -[tui_flex](../packages/tui/examples/tui_flex.rs) - Flexbox support - -[tui_hover](../packages/tui/examples/tui_hover.rs) - Detect hover and mouse events - -[tui_list](../packages/tui/examples/tui_list.rs) - Renders a list of items - -[tui_margin](../packages/tui/examples/tui_margin.rs) - Margins around flexboxes - -[tui_quadrants](../packages/tui/examples/tui_quadrants.rs) - Four quadrants - -[tui_readme](../packages/tui/examples/tui_readme.rs) - The readme example - -[tui_task](../packages/tui/examples/tui_task.rs) - Background tasks - -[tui_text](../packages/tui/examples/tui_text.rs) - Simple text example - # TODO Missing Features - Fine-grained reactivity @@ -175,4 +143,4 @@ Missing examples - Custom elements - Component Children: Pass children into child components - Render To string: Render a mounted virtualdom to a string -- Testing and Debugging \ No newline at end of file +- Testing and Debugging diff --git a/examples/button.rs b/examples/button.rs new file mode 100644 index 000000000..f001ed531 --- /dev/null +++ b/examples/button.rs @@ -0,0 +1,18 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + button { + onclick: |_| async move { + println!("hello, desktop!"); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + println!("goodbye, desktop!"); + }, + "hello, desktop!" + } + }) +} diff --git a/examples/calculator.rs b/examples/calculator.rs index bf183ba67..bc7f13459 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -82,7 +82,7 @@ fn app(cx: Scope) -> Element { onclick: move |_| { let temp = calc_val(val.as_str()); if temp > 0.0 { - val.set(format!("-{}", temp)); + val.set(format!("-{temp}")); } else { val.set(format!("{}", temp.abs())); } diff --git a/examples/callback.rs b/examples/callback.rs index bf2e3e3a0..cb300b701 100644 --- a/examples/callback.rs +++ b/examples/callback.rs @@ -13,7 +13,7 @@ fn app(cx: Scope) -> Element { .await .unwrap(); - println!("{:#?}, ", res); + println!("{res:#?}, "); }); cx.render(rsx! { diff --git a/examples/clock.rs b/examples/clock.rs new file mode 100644 index 000000000..505a5e87c --- /dev/null +++ b/examples/clock.rs @@ -0,0 +1,28 @@ +//! Example: README.md showcase +//! +//! 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 { + use_init_signal_rt(cx); + + let mut count = use_signal(cx, || 0); + + use_future!(cx, || async move { + loop { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + count += 1; + println!("current: {count}"); + } + }); + + cx.render(rsx! { + div { "High-Five counter: {count}" } + }) +} diff --git a/examples/compose.rs b/examples/compose.rs new file mode 100644 index 000000000..3ee6d36ae --- /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::new); + + 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/counter.rs b/examples/counter.rs new file mode 100644 index 000000000..f51083a95 --- /dev/null +++ b/examples/counter.rs @@ -0,0 +1,36 @@ +//! Comparison example with leptos' counter example +//! https://github.com/leptos-rs/leptos/blob/main/examples/counters/src/lib.rs + +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let counters = use_state(cx, || vec![0, 0, 0]); + let sum: usize = counters.iter().copied().sum(); + + render! { + div { + button { onclick: move |_| counters.make_mut().push(0), "Add counter" } + button { onclick: move |_| { counters.make_mut().pop(); }, "Remove counter" } + p { "Total: {sum}" } + for (i, counter) in counters.iter().enumerate() { + li { + button { onclick: move |_| counters.make_mut()[i] -= 1, "-1" } + input { + value: "{counter}", + oninput: move |e| { + if let Ok(value) = e.value.parse::() { + counters.make_mut()[i] = value; + } + } + } + button { onclick: move |_| counters.make_mut()[i] += 1, "+1" } + button { onclick: move |_| { counters.make_mut().remove(i); }, "x" } + } + } + } + } +} 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/custom_element.rs b/examples/custom_element.rs deleted file mode 100644 index 6ed2e64cd..000000000 --- a/examples/custom_element.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! This example shows to wrap a webcomponent / custom element with a component. -//! -//! Oftentimes, a third party library will provide a webcomponent that you want -//! to use in your application. This example shows how to create that custom element -//! directly with the raw_element method on NodeFactory. - -use dioxus::prelude::*; - -fn main() { - let mut dom = VirtualDom::new(app); - let _ = dom.rebuild(); - - let output = dioxus_ssr::render(&dom); - - println!("{}", output); -} - -fn app(cx: Scope) -> Element { - let g = cx.component(component, (), "component"); - let c = cx.make_node(g); - cx.render(rsx! { - div { c } - }) - - // let nf = NodeFactory::new(cx); - - // let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump()); - - // attrs.push(nf.attr("client-id", format_args!("abc123"), None, false)); - - // attrs.push(nf.attr("name", format_args!("bob"), None, false)); - - // attrs.push(nf.attr("age", format_args!("47"), None, false)); - - // Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None)) -} - -fn component(cx: Scope) -> Element { - todo!() -} diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 363d85f4b..107251cd7 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -52,7 +52,7 @@ struct DogApi { #[inline_props] async fn breed_pic(cx: Scope, breed: String) -> Element { let fut = use_future!(cx, |breed| async move { - reqwest::get(format!("https://dog.ceo/api/breed/{}/images/random", breed)) + reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) .await .unwrap() .json::() diff --git a/examples/drops.rs b/examples/drops.rs new file mode 100644 index 000000000..71a791990 --- /dev/null +++ b/examples/drops.rs @@ -0,0 +1,36 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let count = if cx.generation() % 2 == 0 { 10 } else { 0 }; + + println!("Generation: {}", cx.generation()); + + if cx.generation() < 10 { + cx.needs_update(); + } + + render! { + (0..count).map(|_| rsx!{ + drop_child {} + }) + } +} + +fn drop_child(cx: Scope) -> Element { + cx.use_hook(|| Drops); + render! { + div{} + } +} + +struct Drops; + +impl Drop for Drops { + fn drop(&mut self) { + println!("Dropped!"); + } +} diff --git a/examples/eval.rs b/examples/eval.rs index 22e036b9c..3a16b774e 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -1,25 +1,17 @@ use dioxus::prelude::*; -use dioxus_desktop::EvalResult; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - let script = use_state(cx, String::new); let eval = dioxus_desktop::use_eval(cx); - let future: &UseRef> = use_ref(cx, || None); - if future.read().is_some() { - let future_clone = future.clone(); - cx.spawn(async move { - if let Some(fut) = future_clone.with_mut(|o| o.take()) { - println!("{:?}", fut.await) - } - }); - } + let script = use_state(cx, String::new); + let output = use_state(cx, String::new); cx.render(rsx! { div { + p { "Output: {output}" } input { placeholder: "Enter an expression", value: "{script}", @@ -27,8 +19,12 @@ fn app(cx: Scope) -> Element { } button { onclick: move |_| { - let fut = eval(script); - future.set(Some(fut)); + to_owned![script, eval, output]; + cx.spawn(async move { + if let Ok(res) = eval(script.to_string()).await { + output.set(res.to_string()); + } + }); }, "Execute" } diff --git a/examples/fermi.rs b/examples/fermi.rs index babe3e024..001890892 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/file_explorer.rs b/examples/file_explorer.rs index 64173ad11..88b37789d 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -89,7 +89,7 @@ impl Files { let paths = match std::fs::read_dir(cur_path) { Ok(e) => e, Err(err) => { - let err = format!("An error occured: {:?}", err); + let err = format!("An error occured: {err:?}"); self.err = Some(err); self.path_stack.pop(); return; diff --git a/examples/filedragdrop.rs b/examples/filedragdrop.rs index 79d784f1b..f5b760493 100644 --- a/examples/filedragdrop.rs +++ b/examples/filedragdrop.rs @@ -3,7 +3,7 @@ use dioxus_desktop::Config; fn main() { let cfg = Config::new().with_file_drop_handler(|_w, e| { - println!("{:?}", e); + println!("{e:?}"); true }); diff --git a/examples/inputs.rs b/examples/inputs.rs index 223e3a7e7..cf848d99e 100644 --- a/examples/inputs.rs +++ b/examples/inputs.rs @@ -42,7 +42,7 @@ fn app(cx: Scope) -> Element { // so the value of our input event will be either huey, dewey, louie, or true/false (because of the checkboxe) // be mindful in grouping inputs together, as they will all be handled by the same event handler oninput: move |evt| { - println!("{:?}", evt); + println!("{evt:?}"); }, div { input { @@ -104,7 +104,7 @@ fn app(cx: Scope) -> Element { name: "pdf", r#type: "checkbox", oninput: move |evt| { - println!("{:?}", evt); + println!("{evt:?}"); }, } label { @@ -121,7 +121,7 @@ fn app(cx: Scope) -> Element { r#type: "{field}", value: "{value}", oninput: move |evt: FormEvent| { - println!("{:?}", evt); + println!("{evt:?}"); }, } label { diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs new file mode 100644 index 000000000..e2caeba80 --- /dev/null +++ b/examples/multiwindow.rs @@ -0,0 +1,27 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let window = dioxus_desktop::use_window(cx); + + cx.render(rsx! { + div { + button { + onclick: move |_| { + let dom = VirtualDom::new(popup); + window.new_window(dom, Default::default()); + }, + "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..0afd830be --- /dev/null +++ b/examples/overlay.rs @@ -0,0 +1,63 @@ +use dioxus::prelude::*; +use dioxus_desktop::{tao::dpi::PhysicalPosition, use_window, 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/rsx_usage.rs b/examples/rsx_usage.rs index 23ab19cf9..d5e2fcdb3 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -83,7 +83,7 @@ fn app(cx: Scope) -> Element { div { class: { const WORD: &str = "expressions"; - format_args!("Arguments can be passed in through curly braces for complex {}", WORD) + format_args!("Arguments can be passed in through curly braces for complex {WORD}") } } } @@ -214,7 +214,7 @@ fn app(cx: Scope) -> Element { } fn format_dollars(dollars: u32, cents: u32) -> String { - format!("${}.{:02}", dollars, cents) + format!("${dollars}.{cents:02}") } fn helper<'a>(cx: &'a ScopeState, text: &str) -> Element<'a> { diff --git a/examples/shortcut.rs b/examples/shortcut.rs new file mode 100644 index 000000000..d98a69ad6 --- /dev/null +++ b/examples/shortcut.rs @@ -0,0 +1,17 @@ +use dioxus::prelude::*; +use dioxus_desktop::tao::keyboard::ModifiersState; +use dioxus_desktop::use_global_shortcut; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let toggled = use_state(cx, || false); + use_global_shortcut(cx, KeyCode::S, ModifiersState::CONTROL, { + to_owned![toggled]; + move || toggled.modify(|t| !*t) + }); + + cx.render(rsx!("toggle: {toggled.get()}")) +} 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/ssr.rs b/examples/ssr.rs index bac3759dd..2bc1657cb 100644 --- a/examples/ssr.rs +++ b/examples/ssr.rs @@ -27,7 +27,7 @@ fn main() { let mut file = String::new(); let mut renderer = dioxus_ssr::Renderer::default(); renderer.render_to(&mut file, &vdom).unwrap(); - println!("{}", file); + println!("{file}"); } fn app(cx: Scope) -> Element { diff --git a/examples/textarea.rs b/examples/textarea.rs index b27a9c99a..df684aa85 100644 --- a/examples/textarea.rs +++ b/examples/textarea.rs @@ -9,7 +9,7 @@ fn main() { fn app(cx: Scope) -> Element { let model = use_state(cx, || String::from("asd")); - println!("{}", model); + println!("{model}"); cx.render(rsx! { textarea { diff --git a/examples/todomvc.rs b/examples/todomvc.rs index b5277133e..4b25507a5 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -7,7 +7,7 @@ fn main() { dioxus_desktop::launch(app); } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] pub enum FilterState { All, Active, @@ -39,56 +39,99 @@ pub fn app(cx: Scope<()>) -> Element { .collect::>(); filtered_todos.sort_unstable(); - let show_clear_completed = todos.values().any(|todo| todo.checked); - let items_left = filtered_todos.len(); - let item_text = match items_left { + let active_todo_count = todos.values().filter(|item| !item.checked).count(); + let active_todo_text = match active_todo_count { 1 => "item", _ => "items", }; - cx.render(rsx!{ + let show_clear_completed = todos.values().any(|todo| todo.checked); + + let selected = |state| { + if *filter == state { + "selected" + } else { + "false" + } + }; + + cx.render(rsx! { section { class: "todoapp", style { include_str!("./assets/todomvc.css") } - div { - header { class: "header", - h1 {"todos"} - input { - class: "new-todo", - placeholder: "What needs to be done?", - value: "{draft}", - autofocus: "true", - oninput: move |evt| { - draft.set(evt.value.clone()); - }, - onkeydown: move |evt| { - if evt.key() == Key::Enter && !draft.is_empty() { - todos.make_mut().insert( - **todo_id, - TodoItem { - id: **todo_id, - checked: false, - contents: draft.to_string(), - }, - ); - *todo_id.make_mut() += 1; - draft.set("".to_string()); - } + header { class: "header", + h1 {"todos"} + input { + class: "new-todo", + placeholder: "What needs to be done?", + value: "{draft}", + autofocus: "true", + oninput: move |evt| { + draft.set(evt.value.clone()); + }, + onkeydown: move |evt| { + if evt.key() == Key::Enter && !draft.is_empty() { + todos.make_mut().insert( + **todo_id, + TodoItem { + id: **todo_id, + checked: false, + contents: draft.to_string(), + }, + ); + *todo_id.make_mut() += 1; + draft.set("".to_string()); } } } + } + section { + class: "main", + if !todos.is_empty() { + rsx! { + input { + id: "toggle-all", + class: "toggle-all", + r#type: "checkbox", + onchange: move |_| { + let check = active_todo_count != 0; + for (_, item) in todos.make_mut().iter_mut() { + item.checked = check; + } + }, + checked: if active_todo_count == 0 { "true" } else { "false" }, + } + label { r#for: "toggle-all" } + } + } ul { class: "todo-list", - filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id, todos: todos })) + filtered_todos.iter().map(|id| rsx!(TodoEntry { + key: "{id}", + id: *id, + todos: todos, + })) } (!todos.is_empty()).then(|| rsx!( footer { class: "footer", span { class: "todo-count", - strong {"{items_left} "} - span {"{item_text} left"} + strong {"{active_todo_count} "} + span {"{active_todo_text} left"} } ul { class: "filters", - li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }} - li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }} - li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }} + for (state, state_text, url) in [ + (FilterState::All, "All", "#/"), + (FilterState::Active, "Active", "#/active"), + (FilterState::Completed, "Completed", "#/completed"), + ] { + li { + a { + href: url, + class: selected(state), + onclick: move |_| filter.set(state), + prevent_default: "onclick", + state_text + } + } + } } show_clear_completed.then(|| rsx!( button { @@ -102,8 +145,8 @@ pub fn app(cx: Scope<()>) -> Element { } } footer { class: "info", - p {"Double-click to edit a todo"} - p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }} + p { "Double-click to edit a todo" } + p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }} p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }} } }) @@ -136,13 +179,17 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap(); } } - label { r#for: "cbg-{todo.id}", - onclick: move |_| is_editing.set(true), + ondblclick: move |_| is_editing.set(true), prevent_default: "onclick", "{todo.contents}" } + button { + class: "destroy", + onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); }, + prevent_default: "onclick", + } } is_editing.then(|| rsx!{ input { 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/examples/window_focus.rs b/examples/window_focus.rs new file mode 100644 index 000000000..59b4d5166 --- /dev/null +++ b/examples/window_focus.rs @@ -0,0 +1,42 @@ +use dioxus::prelude::*; +use dioxus_desktop::tao::event::WindowEvent; +use dioxus_desktop::use_wry_event_handler; +use dioxus_desktop::wry::application::event::Event as WryEvent; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let focused = use_state(cx, || false); + + use_wry_event_handler(cx, { + to_owned![focused]; + move |event, _| { + if let WryEvent::WindowEvent { + event: WindowEvent::Focused(new_focused), + .. + } = event + { + focused.set(*new_focused); + } + } + }); + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + display: "flex", + flex_direction: "column", + align_items: "center", + { + if *focused.get() { + "This window is focused!" + } else { + "This window is not focused!" + } + } + } + }) +} diff --git a/examples/window_zoom.rs b/examples/window_zoom.rs index b7c190896..e45d71be2 100644 --- a/examples/window_zoom.rs +++ b/examples/window_zoom.rs @@ -7,16 +7,17 @@ fn main() { fn app(cx: Scope) -> Element { let window = use_window(cx); - let level = use_state(cx, || 1.0); + cx.render(rsx! { input { r#type: "number", value: "{level}", oninput: |e| { - let num = e.value.parse::().unwrap_or(1.0); - level.set(num); - window.set_zoom_level(num); + if let Ok(new_zoom) = e.value.parse::() { + level.set(new_zoom); + window.webview.zoom(new_zoom); + } } } }) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 8b1e6185b..ca37f7be1 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -40,7 +40,7 @@ | 代码示例 | - 开发指南 + 开发指南 | English | diff --git a/notes/SOLVEDPROBLEMS.md b/notes/SOLVEDPROBLEMS.md deleted file mode 100644 index b978c923d..000000000 --- a/notes/SOLVEDPROBLEMS.md +++ /dev/null @@ -1,494 +0,0 @@ -# Solved problems while building Dioxus - -focuses: - -- ergonomics -- render agnostic -- remote coupling -- memory efficient -- concurrent -- global context -- scheduled updates -- - -## FC Macro for more elegant components - -Originally the syntax of the FC macro was meant to look like: - -```rust -#[fc] -fn example(cx: &Context<{ name: String }>) -> DomTree { - html! {
"Hello, {name}!"
} -} -``` - -`Context` was originally meant to be more obviously parameterized around a struct definition. However, while this works with rustc, this does not work well with Rust Analyzer. Instead, the new form was chosen which works with Rust Analyzer and happens to be more ergonomic. - -```rust -#[fc] -fn example(cx: &Context, name: String) -> DomTree { - html! {
"Hello, {name}!"
} -} -``` - -## Anonymous Components - -In Yew, the function_component macro turns a struct into a Trait `impl` with associated type `props`. Like so: - -```rust -#[derive(Properties)] -struct Props { - // some props -} - -struct SomeComponent; -impl FunctionProvider for SomeComponent { - type TProps = Props; - - fn run(&mut self, props: &Props) -> Html { - // user's functional component goes here - } -} - -pub type SomeComponent = FunctionComponent; -``` - -By default, the underlying component is defined as a "functional" implementation of the `Component` trait with all the lifecycle methods. In Dioxus, we don't allow components as structs, and instead take a "hooks-only" approach. However, we still need cx. To get these without dealing with traits, we just assume functional components are modules. This lets the macros assume an FC is a module, and `FC::Props` is its props and `FC::component` is the component. Yew's method does a similar thing, but with associated types on traits. - -Perhaps one day we might use traits instead. - -The FC macro needs to work like this to generate a final module signature: - -```rust -// "Example" can be used directly -// The "associated types" are just children of the module -// That way, files can just be components (yay, no naming craziness) -mod Example { - // Associated metadata important for liveview - static NAME: &'static str = "Example"; - - struct Props { - name: String - } - - fn component(cx: &Context) -> DomTree { - html! {
"Hello, {name}!"
} - } -} - -// or, Example.rs - -static NAME: &'static str = "Example"; - -struct Props { - name: String -} - -fn component(cx: &Context) -> DomTree { - html! {
"Hello, {name}!"
} -} -``` - -These definitions might be ugly, but the fc macro cleans it all up. The fc macro also allows some configuration - -```rust -#[fc] -fn example(cx: &Context, name: String) -> DomTree { - html! {
"Hello, {name}!"
} -} - -// .. expands to - -mod Example { - use super::*; - static NAME: &'static str = "Example"; - struct Props { - name: String - } - fn component(cx: &Context) -> DomTree { - html! {
"Hello, {name}!"
} - } -} -``` - -## Live Components - -Live components are a very important part of the Dioxus ecosystem. However, the goal with live components was to constrain their implementation purely to APIs available through Context (concurrency, context, subscription). - -From a certain perspective, live components are simply server-side-rendered components that update when their props change. Here's more-or-less how live components work: - -```rust -#[fc] -static LiveFc: FC = |cx, refresh_handler: impl FnOnce| { - // Grab the "live context" - let live_context = cx.use_context::(); - - // Ensure this component is registered as "live" - live_context.register_scope(); - - // send our props to the live context and get back a future - let vnodes = live_context.request_update(cx); - - // Suspend the rendering of this component until the vnodes are finished arriving - // Render them once available - cx.suspend(async move { - let output = vnodes.await; - - // inject any listener handles (ie button clicks, views, etc) to the parsed nodes - output[1].add_listener("onclick", refresh_handler); - - // Return these nodes - // Nodes skip diffing and go straight to rendering - output - }) -} -``` - -Notice that LiveComponent receivers (the client-side interpretation of a LiveComponent) are simply suspended components waiting for updates from the LiveContext (the context that wraps the app to make it "live"). - -## Allocation Strategy (ie incorporating Dodrio research) - ---- - -The `VNodeTree` type is a very special type that allows VNodes to be created using a pluggable allocator. The html! macro creates something that looks like: - -```rust -pub static Example: Component = |cx| { - html! {
"blah"
} -}; - -// expands to... - -pub static Example: Component = |cx| { - // This function converts a Fn(allocator) -> DomTree closure to a VNode struct that will later be evaluated. - html_macro_to_vnodetree(move |allocator| { - let mut node0 = allocator.alloc(VElement::div); - let node1 = allocator.alloc_text("blah"); - node0.children = [node1]; - node0 - }) -}; -``` - -At runtime, the new closure is created that captures references to `cx`. Therefore, this closure can only be evaluated while `cx` is borrowed and in scope. However, this closure can only be evaluated with an `allocator`. Currently, the global and Bumpalo allocators are available, though in the future we will add support for creating a VDom with any allocator or arena system (IE Jemalloc, wee-alloc, etc). The intention here is to allow arena allocation of VNodes (no need to box nested VNodes). Between diffing phases, the arena will be overwritten as old nodes are replaced with new nodes. This saves allocation time and enables bump allocators. - -## Context and lifetimes - -We want components to be able to fearlessly "use_context" for use in state management solutions. - -However, we cannot provide these guarantees without compromising the references. If a context mutates, it cannot lend out references. - -Functionally, this can be solved with UnsafeCell and runtime dynamics. Essentially, if a context mutates, then any affected components would need to be updated, even if they themselves aren't updated. Otherwise, a reference would be pointing at data that could have potentially been moved. - -To do this safely is a pretty big challenge. We need to provide a method of sharing data that is safe, ergonomic, and that fits the abstraction model. - -Enter, the "ContextGuard". - -The "ContextGuard" is very similar to a Ref/RefMut from the RefCell implementation, but one that derefs into actual underlying value. - -However, derefs of the ContextGuard are a bit more sophisticated than the Ref model. - -For RefCell, when a Ref is taken, the RefCell is now "locked." This means you cannot take another `borrow_mut` instance while the Ref is still active. For our purposes, our modification phase is very limited, so we can make more assumptions about what is safe. - -1. We can pass out ContextGuards from any use of use_context. These don't actually lock the value until used. -2. The ContextGuards only lock the data while the component is executing and when a callback is running. -3. Modifications of the underlying context occur after a component is rendered and after the event has been run. - -With the knowledge that usage of ContextGuard can only be achieved in a component context and the above assumptions, we can design a guard that prevents any poor usage but also is ergonomic. - -As such, the design of the ContextGuard must: - -- be /copy/ for easy moves into closures -- never point to invalid data (no dereferencing of raw pointers after movable data has been changed (IE a vec has been resized)) -- not allow references of underlying data to leak into closures - -To solve this, we can be clever with lifetimes to ensure that any data is protected, but context is still ergonomic. - -1. As such, deref context guard returns an element with a lifetime bound to the borrow of the guard. -2. Because we cannot return locally borrowed data AND we consume context, this borrow cannot be moved into a closure. -3. ContextGuard is _copy_ so the guard itself can be moved into closures -4. ContextGuard derefs with its unique lifetime _inside_ closures -5. Derefing a ContextGuard evaluates the underlying selector to ensure safe temporary access to underlying data - -```rust -struct ExampleContext { - // unpinnable objects with dynamic sizing - items: Vec -} - -fn Example<'src>(cx: Scope<'src, ()>) -> DomTree<'src> { - let val: &'b ContextGuard = (&'b cx).use_context(|context: &'other ExampleContext| { - // always select the last element - context.items.last() - }); - - let handler1 = move |_| println!("Value is {}", val); // deref coercion performed here for printing - let handler2 = move |_| println!("Value is {}", val); // deref coercion performed here for printing - - cx.render(html! { -
- - -
-

"Value is: {val}"

-
-
- }) -} -``` - -A few notes: - -- this does _not_ protect you from data races!!! -- this does _not_ force rendering of components -- this _does_ protect you from invalid + UB use of data -- this approach leaves lots of room for fancy state management libraries -- this approach is fairly quick, especially if borrows can be cached during usage phases - -## Concurrency - -For Dioxus, concurrency is built directly into the VirtualDOM lifecycle and event system. Suspended components prove "no changes" while diffing, and will cause a lifecycle update later. This is considered a "trigger" and will cause targeted diffs and re-renders. Renderers will need to await the Dioxus suspense queue if they want to process these updates. This will typically involve joining the suspense queue and event listeners together like: - -```rust -// wait for an even either from the suspense queue or our own custom listener system -let (left, right) = join!(vdom.suspense_queue, self.custom_event_listener); -``` - -LiveView is built on this model, and updates from the WebSocket connection to the host server are treated as external updates. This means any renderer can feed targeted EditLists (the underlying message of this event) directly into the VirtualDOM. - -## Execution Model - - - -## Diffing - -Diffing is an interesting story. Since we don't re-render the entire DOM, we need a way to patch up the DOM without visiting every component. To get this working, we need to think in cycles, queues, and stacks. Most of the original logic is pulled from Dodrio as Dioxus and Dodrio share much of the same DNA. - -When an event is triggered, we find the callback that installed the listener and run it. We then record all components affected by the running of the "subscription" primitive. In practice, many hooks will initiate a subscription, so it is likely that many components throughout the entire tree will need to be re-rendered. For each component, we attach its index and the type of update it needs. - -In practice, removals trump prop updates which trump subscription updates. Therefore, we only process updates where props are directly changed first, as this will likely flow into child components. - -Roughly, the flow looks like: - -- Process the initiating event -- Mark components affected by the subscription API (the only way of causing forward updates) -- Descend from the root into children, ignoring those not affected by the subscription API. (walking the tree until we hit the first affected component, or choosing the highest component) -- Run this component and then immediately diff its output, marking any children that also need to be updated and putting them into the immediate queue -- Mark this component as already-ran and remove it from the need_to_diff list, instead moving it into the "already diffed list" -- Run the marked children until the immediate queue is empty - -```rust -struct DiffMachine { - immediate_queue: Vec, - diffed: HashSet, - need_to_diff: HashSet - marked_for_removal: Vec -} -``` - -On the actual diffing level, we're using the diffing algorithm pulled from Dodrio, but plan to move to a dedicated crate that implements Meyers/Patience for us. During the diffing phase, we track our current position using a "Traversal" which implements the "MoveTo". When "MoveTo" is combined with "Edit", it is possible for renderers to fully interpret a series of Moves and Edits together to update their internal node structures for rendering. - -## Patch Stream - -One of the most important parts of Dioxus is the ability to stream patches from server to client. However, this inherently has challenges where raw VNodes attach listeners to themselves, and are therefore not serializable. - -### How do properties work? - -How should properties passing work? Should we directly call the child? Should we box the props? Should we replace the pops inside the box? - -Here's (generally) my head is at: - -Components need to store their props on them if they want to be updated remotely. These props _can_ be updated after the fact. - -Perf concerns: -unnecessary function runs - list-y components - hook calls? - making vnodes? - -Does any of this matter? -Should we just run any component we see, immediately and imperatively? That will cause checks throughout the whole tree, no matter where the update occurred - -https://calendar.perfplanet.com/2013/diff/ - -Here's how react does it: - -Any "dirty" node causes an entire subtree render. Calling "setState" at the very top will cascade all the way down. This is particularly bad for this component design: - -```rust -static APP: Component = |cx| { - let title = use_context(Title); - cx.render(html!{ -
-

"{title}"

- // VComponent::new(|| (FC, PropsForFc)) -> needs a context to immediately update the component's props imperatively? store the props in a box on bump? store the props on the child? - // if props didnt change, then let the refernece stay invalid?.... no, cant do that, bump gets reset - // immediately update props on the child component if it can be found? -> interesting, feels wrong, but faster, at the very least. - // can box on bump for the time being (fast enough), and then move it over? during the comparison phase? props only need to matter - // cant downcast (can with transmute, but yikes) - // how does chain borrowing work? a -> b -> c -> d - // if b gets marked as dirty, then c and d are invalidated (semantically, UB, but not *bad* UB, just data races) - // make props static? -> easy to move, gross to use - // - // treat like a context selector? - // use_props::

(2) - // child_props: Map> - // vs children: BTreeSet -> to get nth -

- }) -}; -static HEAVY_LIST: Component = |cx| { - cx.render({ - {0.100.map(i => )} - }) -}; -``` - -An update to the use_context subscription will mark the node as dirty. The node then is forced to re-analyze HeavyList, even though HeavyList did not change. We should automatically implement this suppression knowing that props are immutable and can be partialeq. - -## FC Layout - -The FC layout was altered to make life easier for us inside the VirtualDom. The "view" function returns an unbounded VNode object. Calling the "view" function is unsafe under the hood, but prevents lifetimes from leaking out of the function call. Plus, it's easier to write. Because there are no lifetimes on the output (occur purely under the hood), we can escape needing to annotate them. - -```rust -fn component(cx: Scope) -> DomTree { - -} -``` - -The VNode object purely represents a viewable "key". It also forces components to use the "view" function as there is no other way to generate the VNode object. Because the VNode is a required type of FC, we can guarantee the same usage and flow patterns for all components. - -## Events - -Events are finally in! To do events properly, we are abstracting over the event source with synthetic events. This forces 3rd party renderers to create the appropriate cross-platform event - -## Optional Props on Components - -A major goal here is ergonomics. Any field that is Option should default to none. - -```rust - -rsx! { - Example { /* args go here */ a: 10, b: 20 } -} - - -``` - -```rust -#[derive(Properties)] -struct Props { - -} - -static Component: Component = |cx| { - -} -``` - -or - -```rust -#[fc] -static Component: FC = |cx, name: &str| { - -} -``` - -## Noderefs - -How do we resolve noderefs in a world of patches? Patches _must_ be serializable, so if we do something like `Option<&RefCell>`, then that must serialize as _something_ to indicate to a remote host that access to the node itself is desired. Our `Slot` type will need to be somewhat abstract. - -If we add a new patch type called "BindRef" we could do something like: - -```rust -enum Patch { - //... - BindAsRef { raw_node: &RefCell> } -} -``` - -```rust -let node_ref = use_node_ref(cx); -use_effect(cx, || { - -}, []); -div { ref: node_ref, - "hello me" - h3 {"yo dom"} -} -``` - -refs only work when you're native to the platform. it doesn't make sense to gain a ref when you're not native. - -## In-sync or separate? - -React makes refs - and protection against dom manipulation - work by modifying the real dom while diffing the virtual dom. This lets it bind real dom elements to the virtual dom elements. Dioxus currently does not do this, instead creating a list of changes for an interpreter to apply once diffing has completed. - -This behavior fit dodrio well as all dom manipulations would occur batched. The original intention for this approach was to make it faster to read out of Wasm and into JS. Dodrio is essentially performing the Wasm job that Wasm<->JS for strings does. In theory, this particular optimization is not necessary. - -https://github.com/fitzgen/dodrio/issues/77 - -This issue/pr on the dodrio repository points to a future where elements are held on to by the virtualdom. - -Can we solve events, refs, and protection against 3rd party dom mutation all in one shot? - -I think we can.... - -every node gets a globally unique ID - -abstract the real dom - -```rust - -struct VirtualDom - -trait RealDom { - type Node: RealNode; - fn get_node(&self, id: u32) -> &Self::Node; - fn get_node_mut(&mut self, id: u32) -> &mut Self::Node; - fn replace_node(); - fn create_text_node(); - fn create_element(); - fn create_element_ns(); -} - -trait RealNode { - fn add_listener(&mut self, event: &str); - fn set_inner_text(&mut self, text: &str); - fn set_attr(&mut self, name, value); - fn set_class(&mut self); - fn remove_attr(&mut self); - // We can't have a generic type in trait objects, so instead we provide the inner as Any - fn raw_node_as_any_mut(&mut self) -> &mut dyn Any; -} - -impl VirtualDom { - fn diff() { - - } -} -enum VNode<'bump, 'realdom, RealDom> { - VElement { - real: &RealDom::Node - } - VText { - real: &RealDom::Node - } -} - - -fn main() { - let mut real_dom = websys::Document(); - let virtual_dom = Dioxus::VirtualDom::new(); - - virtual_dom.rebuild(&mut real_dom); - - loop { - let event = switch! { - real_dom.events.await => event, - virtual_dom.inner_events.await => event - }; - - virtual_dom.apply_event(&mut real_dom, event); - } -} - -``` diff --git a/packages/autofmt/Cargo.toml b/packages/autofmt/Cargo.toml index fae35afd0..ccb6fed24 100644 --- a/packages/autofmt/Cargo.toml +++ b/packages/autofmt/Cargo.toml @@ -1,18 +1,23 @@ [package] name = "dioxus-autofmt" -version = "0.0.0" +version = "0.3.0" edition = "2021" - +authors = ["Jonathan Kelley"] +description = "Autofomatter for Dioxus RSX" +license = "MIT/Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react"] # 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.3" } proc-macro2 = { version = "1.0.6", features = ["span-locations"] } quote = "1.0" syn = { version = "1.0.11", features = ["full", "extra-traits"] } -triple_accel = "0.4.0" serde = { version = "1.0.136", features = ["derive"] } -prettyplease = { git = "https://github.com/DioxusLabs/prettyplease-macro-fmt.git", features = [ +prettyplease = { package = "prettier-please", version = "0.1.16", features = [ "verbatim", ] } diff --git a/packages/autofmt/README.md b/packages/autofmt/README.md index eec3b590e..bd934f2c7 100644 --- a/packages/autofmt/README.md +++ b/packages/autofmt/README.md @@ -1,88 +1,49 @@ -# This crate autofmts blocks of rsx! - -This crate formats rsx! by parsing call bodies and pretty-printing them back out. +# dioxus-autofmt +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] -# Todo: -Sorted roughly in order of what's possible +[crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg +[crates-url]: https://crates.io/crates/dioxus-autofmt -- [x] Oneline rsx! calls - blocker because this wrecks formatting -- [ ] Nested RSX calls (important) - unnecessary but desirable -- [ ] RSX edits overstepping each other -- [ ] Collapse components and elements under syntax - -- [ ] Don't eat comments in exprs -- [ ] Format regular exprs -- [ ] Fix prettyplease around chaining -- [ ] Don't eat comments in prettyplease +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) | +[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus_autofmt) | +[Chat](https://discord.gg/XgGxMSkvUM) -# Technique +## Overview + +`dioxus-autofmt` provides a pretty printer for the `rsx` syntax tree. -div { - div {} - div {} -} +This is done manually with a via set of formatting rules. The output is not guaranteed to be stable between minor versions of the crate as we might tweak the output. + +`dioxus-autofmt` provides an API to perform precision edits as well as just spit out a block of formatted RSX from any RSX syntax tree. This is used by the `rsx-rosetta` crate which can accept various input languages and output valid RSX. -div +## Contributing -possible line break -div -div +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! +## License +This project is licensed under the [MIT license]. +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT -string of possible items within a nesting -div { - attr_pair - expr - text - comment -} -a nesting is either a component or an element - -idea: -collect all items into a queue -q -```rust -section { - div { - h1 { p { "asdasd" } } - h1 { p { "asdasd" } } - } -} - -section {} -``` - - -// space -// space -// space - - -3 - section -3 - section div -3 - section div h1 -3 - section div h1 p -3 - section div h1 p text -3 - section -3 - section div -3 - section div h1 -3 - section div h1 p -3 - section div h1 p text - -block - -- when we hit the end of a trail, we can make a decision what needs to be hard breaked -- most nestings cannot be merged into a single one, so at some point we need to write the line break -- this is the scan section. we scan forward until it's obvious where to place a hard break -- when a line is finished, we can print it out by unloading our queued items -- never double nested - - -Terms -- break is a whitespace than can flex, dependent on the situation -- ‹⁠› +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT without any additional +terms or conditions. diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs index 81c35a702..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.flart()) { - 'line: for line in self.src[..attr.attr.flart().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/collect_macros.rs b/packages/autofmt/src/collect_macros.rs new file mode 100644 index 000000000..ebb6314e2 --- /dev/null +++ b/packages/autofmt/src/collect_macros.rs @@ -0,0 +1,198 @@ +//! Collect macros from a file +//! +//! Returns all macros that match a pattern. You can use this information to autoformat them later + +use proc_macro2::LineColumn; +use syn::{Block, Expr, File, Item, Macro, Stmt}; + +type CollectedMacro<'a> = &'a Macro; + +pub fn collect_from_file<'a>(file: &'a File, macros: &mut Vec>) { + for item in file.items.iter() { + collect_from_item(item, macros); + } +} + +pub fn collect_from_item<'a>(item: &'a Item, macros: &mut Vec>) { + match item { + Item::Fn(f) => collect_from_block(&f.block, macros), + + // Ignore macros if they're not rsx or render + Item::Macro(macro_) => { + if macro_.mac.path.segments[0].ident == "rsx" + || macro_.mac.path.segments[0].ident == "render" + { + macros.push(¯o_.mac); + } + } + + // Currently disabled since we're not focused on autoformatting these + Item::Impl(_imp) => {} + Item::Trait(_) => {} + + // Global-ish things + Item::Static(f) => collect_from_expr(&f.expr, macros), + Item::Const(f) => collect_from_expr(&f.expr, macros), + Item::Mod(s) => { + if let Some((_, block)) = &s.content { + for item in block { + collect_from_item(item, macros); + } + } + } + + // None of these we can really do anything with at the item level + Item::Macro2(_) + | Item::Enum(_) + | Item::ExternCrate(_) + | Item::ForeignMod(_) + | Item::TraitAlias(_) + | Item::Type(_) + | Item::Struct(_) + | Item::Union(_) + | Item::Use(_) + | Item::Verbatim(_) => {} + _ => {} + } +} + +pub fn collect_from_block<'a>(block: &'a Block, macros: &mut Vec>) { + for stmt in &block.stmts { + match stmt { + Stmt::Item(item) => collect_from_item(item, macros), + Stmt::Local(local) => { + if let Some((_eq, init)) = &local.init { + collect_from_expr(init, macros); + } + } + Stmt::Expr(exp) | Stmt::Semi(exp, _) => collect_from_expr(exp, macros), + } + } +} + +pub fn collect_from_expr<'a>(expr: &'a Expr, macros: &mut Vec>) { + // collect an expr from the exprs, descending into blocks + match expr { + Expr::Macro(macro_) => { + if macro_.mac.path.segments[0].ident == "rsx" + || macro_.mac.path.segments[0].ident == "render" + { + macros.push(¯o_.mac); + } + } + + Expr::MethodCall(e) => { + collect_from_expr(&e.receiver, macros); + for expr in e.args.iter() { + collect_from_expr(expr, macros); + } + } + Expr::Assign(exp) => { + collect_from_expr(&exp.left, macros); + collect_from_expr(&exp.right, macros); + } + + Expr::Async(b) => collect_from_block(&b.block, macros), + Expr::Block(b) => collect_from_block(&b.block, macros), + Expr::Closure(c) => collect_from_expr(&c.body, macros), + Expr::Let(l) => collect_from_expr(&l.expr, macros), + Expr::Unsafe(u) => collect_from_block(&u.block, macros), + Expr::Loop(l) => collect_from_block(&l.body, macros), + + Expr::Call(c) => { + collect_from_expr(&c.func, macros); + for expr in c.args.iter() { + collect_from_expr(expr, macros); + } + } + + Expr::ForLoop(b) => { + collect_from_expr(&b.expr, macros); + collect_from_block(&b.body, macros); + } + Expr::If(f) => { + collect_from_expr(&f.cond, macros); + collect_from_block(&f.then_branch, macros); + if let Some((_, else_branch)) = &f.else_branch { + collect_from_expr(else_branch, macros); + } + } + Expr::Yield(y) => { + if let Some(expr) = &y.expr { + collect_from_expr(expr, macros); + } + } + + Expr::Return(r) => { + if let Some(expr) = &r.expr { + collect_from_expr(expr, macros); + } + } + + Expr::Match(l) => { + collect_from_expr(&l.expr, macros); + for arm in l.arms.iter() { + if let Some((_, expr)) = &arm.guard { + collect_from_expr(expr, macros); + } + + collect_from_expr(&arm.body, macros); + } + } + + Expr::While(w) => { + collect_from_expr(&w.cond, macros); + collect_from_block(&w.body, macros); + } + + // don't both formatting these for now + Expr::Array(_) + | Expr::AssignOp(_) + | Expr::Await(_) + | Expr::Binary(_) + | Expr::Box(_) + | Expr::Break(_) + | Expr::Cast(_) + | Expr::Continue(_) + | Expr::Field(_) + | Expr::Group(_) + | Expr::Index(_) + | Expr::Lit(_) + | Expr::Paren(_) + | Expr::Path(_) + | Expr::Range(_) + | Expr::Reference(_) + | Expr::Repeat(_) + | Expr::Struct(_) + | Expr::Try(_) + | Expr::TryBlock(_) + | Expr::Tuple(_) + | Expr::Type(_) + | Expr::Unary(_) + | Expr::Verbatim(_) => {} + + _ => {} + }; +} + +pub fn byte_offset(input: &str, location: LineColumn) -> usize { + let mut offset = 0; + for _ in 1..location.line { + offset += input[offset..].find('\n').unwrap() + 1; + } + offset + + input[offset..] + .chars() + .take(location.column) + .map(char::len_utf8) + .sum::() +} + +#[test] +fn parses_file_and_collects_rsx_macros() { + let contents = include_str!("../tests/samples/long.rsx"); + let parsed = syn::parse_file(contents).unwrap(); + let mut macros = vec![]; + collect_from_file(&parsed, &mut macros); + assert_eq!(macros.len(), 3); +} diff --git a/packages/autofmt/src/component.rs b/packages/autofmt/src/component.rs index 0da8a85d7..89ebe2229 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 f17aac462..6d2cdc830 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 @@ -45,8 +66,9 @@ impl Buffer { // check if we have a lot of attributes let attr_len = self.is_short_attrs(attributes); - let is_short_attr_list = attr_len < 80; - let is_small_children = self.is_short_children(children).is_some(); + let is_short_attr_list = (attr_len + self.out.indent * 4) < 80; + let children_len = self.is_short_children(children); + let is_small_children = children_len.is_some(); // if we have few attributes and a lot of children, place the attrs on top if is_short_attr_list && !is_small_children { @@ -64,12 +86,19 @@ impl Buffer { // if we have few children and few attributes, make it a one-liner if is_short_attr_list && is_small_children { - opt_level = ShortOptimization::Oneliner; + if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 { + opt_level = ShortOptimization::Oneliner; + } else { + opt_level = ShortOptimization::PropsOnTop; + } } // 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 +109,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 +173,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.flart())?; + 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 +217,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 +238,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 +250,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)?; } } } @@ -246,7 +276,7 @@ impl Buffer { let start = location.start(); let line_start = start.line - 1; - let this_line = self.src[line_start].as_str(); + let this_line = self.src[line_start]; let beginning = if this_line.len() > start.column { this_line[..start.column].trim() @@ -254,8 +284,6 @@ impl Buffer { "" }; - // dbg!(beginning); - beginning.is_empty() } @@ -268,6 +296,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); } @@ -296,10 +328,8 @@ impl Buffer { None } } - [BodyNode::RawExpr(ref expr)] => { - // TODO: let rawexprs to be inlined - get_expr_length(expr) - } + // TODO: let rawexprs to be inlined + [BodyNode::RawExpr(ref expr)] => get_expr_length(expr), [BodyNode::Element(ref el)] => { let attr_len = self.is_short_attrs(&el.attributes); @@ -333,8 +363,8 @@ impl Buffer { Some(len) => total_count += len, None => return None, }, - BodyNode::ForLoop(_) => todo!(), - BodyNode::IfChain(_) => todo!(), + BodyNode::ForLoop(_forloop) => return None, + BodyNode::IfChain(_chain) => return None, } } @@ -342,6 +372,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..ddc0892f8 100644 --- a/packages/autofmt/src/expr.rs +++ b/packages/autofmt/src/expr.rs @@ -1,48 +1,26 @@ //! pretty printer for rsx! use std::fmt::{Result, Write}; -use crate::Buffer; +use proc_macro2::Span; -impl Buffer { - pub fn write_raw_expr(&mut self, exp: &syn::Expr) -> Result { +use crate::{collect_macros::byte_offset, Writer}; + +impl Writer<'_> { + pub fn write_raw_expr(&mut self, placement: Span) -> 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.src[start.line - 1][start.column - 1..end.column].trim() - )?; + // split counting utf8 chars + let start = byte_offset(self.raw_src, start); + let end = byte_offset(self.raw_src, end); + let row = self.raw_src[start..end].trim(); + write!(self.out, "{row}")?; return Ok(()); } @@ -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 aca9f64b0..12bf00585 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -1,11 +1,15 @@ -use crate::buffer::*; -use crate::util::*; +use crate::writer::*; +use collect_macros::byte_offset; +use dioxus_rsx::{BodyNode, CallBody}; +use proc_macro2::LineColumn; +use syn::{ExprMacro, MacroDelimiter}; mod buffer; +mod collect_macros; mod component; mod element; mod expr; -mod util; +mod writer; /// A modification to the original file to be applied by an IDE /// @@ -31,87 +35,141 @@ pub struct FormattedBlock { /// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting. /// /// This function expects a complete file, not just a block of code. To format individual rsx! blocks, use fmt_block instead. +/// +/// The point here is to provide precise modifications of a source file so an accompanying IDE tool can map these changes +/// back to the file precisely. +/// +/// Nested blocks of RSX will be handled automatically pub fn fmt_file(contents: &str) -> Vec { let mut formatted_blocks = Vec::new(); - let mut last_bracket_end = 0; - use triple_accel::{levenshtein_search, Match}; + let parsed = syn::parse_file(contents).unwrap(); - for Match { end, start, k } in levenshtein_search(b"rsx! {", contents.as_bytes()) { - if k > 1 { + let mut macros = vec![]; + collect_macros::collect_from_file(&parsed, &mut macros); + + // No macros, no work to do + if macros.is_empty() { + return formatted_blocks; + } + + let mut writer = Writer::new(contents); + + // Dont parse nested macros + let mut end_span = LineColumn { column: 0, line: 0 }; + for item in macros { + let macro_path = &item.path.segments[0].ident; + + // this macro is inside the last macro we parsed, skip it + if macro_path.span().start() < end_span { continue; } - // ensure the marker is not nested - if start < last_bracket_end { - continue; + // item.parse_body::(); + let body = item.parse_body::().unwrap(); + + let rsx_start = macro_path.span().start(); + + writer.out.indent = &writer.src[rsx_start.line - 1] + .chars() + .take_while(|c| *c == ' ') + .count() + / 4; + + write_body(&mut writer, &body); + + // writing idents leaves the final line ended at the end of the last ident + if writer.out.buf.contains('\n') { + writer.out.new_line().unwrap(); + writer.out.tab().unwrap(); } - let mut indent_level = { - // walk backwards from start until we find a new line - let mut lines = contents[..start].lines().rev(); - match lines.next() { - Some(line) => { - if line.starts_with("//") || line.starts_with("///") { - continue; - } - - line.chars().take_while(|c| *c == ' ').count() / 4 - } - None => 0, - } + let span = match item.delimiter { + MacroDelimiter::Paren(b) => b.span, + MacroDelimiter::Brace(b) => b.span, + MacroDelimiter::Bracket(b) => b.span, }; - let remaining = &contents[end - 1..]; - let bracket_end = find_bracket_end(remaining).unwrap(); - let sub_string = &contents[end..bracket_end + end - 1]; - last_bracket_end = bracket_end + end - 1; + let mut formatted = String::new(); - let mut new = fmt_block(sub_string, indent_level).unwrap(); + std::mem::swap(&mut formatted, &mut writer.out.buf); - if new.len() <= 80 && !new.contains('\n') { - new = format!(" {new} "); + let start = byte_offset(contents, span.start()) + 1; + let end = byte_offset(contents, span.end()) - 1; - // if the new string is not multiline, don't try to adjust the marker ending - // We want to trim off any indentation that there might be - indent_level = 0; + // Rustfmt will remove the space between the macro and the opening paren if the macro is a single expression + let body_is_solo_expr = body.roots.len() == 1 + && matches!(body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_)); + + if formatted.len() <= 80 && !formatted.contains('\n') && !body_is_solo_expr { + formatted = format!(" {formatted} "); } - let end_marker = end + bracket_end - indent_level * 4 - 1; + end_span = span.end(); - if new == contents[end..end_marker] { + if contents[start..end] == formatted { continue; } formatted_blocks.push(FormattedBlock { - formatted: new, - start: end, - end: end_marker, + formatted, + start, + end, }); } formatted_blocks } -pub fn fmt_block(block: &str, indent_level: usize) -> Option { - let mut buf = Buffer { - src: block.lines().map(|f| f.to_string()).collect(), - indent: indent_level, - ..Buffer::default() - }; +pub fn write_block_out(body: CallBody) -> Option { + let mut buf = Writer::new(""); - let body = syn::parse_str::(block).unwrap(); + write_body(&mut buf, &body); + + buf.consume() +} + +fn write_body(buf: &mut Writer, body: &CallBody) { + use std::fmt::Write; - // Oneliner optimization if buf.is_short_children(&body.roots).is_some() { - buf.write_ident(&body.roots[0]).unwrap(); + // write all the indents with spaces and commas between + for idx in 0..body.roots.len() - 1 { + let ident = &body.roots[idx]; + buf.write_ident(ident).unwrap(); + write!(&mut buf.out.buf, ", ").unwrap(); + } + + // write the last ident without a comma + let ident = &body.roots[body.roots.len() - 1]; + buf.write_ident(ident).unwrap(); } else { buf.write_body_indented(&body.roots).unwrap(); } +} + +pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option { + let body = syn::parse2::(expr.mac.tokens).unwrap(); + + let mut buf = Writer::new(raw); + + write_body(&mut buf, &body); + + buf.consume() +} + +pub fn fmt_block(block: &str, indent_level: usize) -> Option { + let body = syn::parse_str::(block).unwrap(); + + let mut buf = Writer::new(block); + + buf.out.indent = indent_level; + + write_body(&mut buf, &body); // 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() @@ -124,8 +182,6 @@ pub fn apply_format(input: &str, block: FormattedBlock) -> String { let (left, _) = input.split_at(start); let (_, right) = input.split_at(end); - // dbg!(&block.formatted); - format!("{}{}{}", left, block.formatted, right) } diff --git a/packages/autofmt/src/util.rs b/packages/autofmt/src/util.rs deleted file mode 100644 index 7fd4c325c..000000000 --- a/packages/autofmt/src/util.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub fn find_bracket_end(contents: &str) -> Option { - let mut depth = 0; - - for (i, c) in contents.chars().enumerate() { - if c == '{' { - depth += 1; - } else if c == '}' { - depth -= 1; - - if depth == 0 { - return Some(i); - } - } - } - - None -} diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs new file mode 100644 index 000000000..6a0e38d82 --- /dev/null +++ b/packages/autofmt/src/writer.rs @@ -0,0 +1,227 @@ +use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop}; +use proc_macro2::{LineColumn, Span}; +use quote::ToTokens; +use std::{ + collections::{HashMap, VecDeque}, + fmt::{Result, Write}, +}; +use syn::{spanned::Spanned, Expr, ExprIf}; + +use crate::buffer::Buffer; + +#[derive(Debug)] +pub struct Writer<'a> { + pub raw_src: &'a str, + pub src: Vec<&'a str>, + 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<'a> Writer<'a> { + pub fn new(raw_src: &'a str) -> Self { + let src = raw_src.lines().collect(); + Self { + raw_src, + src, + cached_formats: HashMap::new(), + comments: VecDeque::new(), + out: Buffer::default(), + } + } + + // 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.span()), + BodyNode::ForLoop(forloop) => self.write_for_loop(forloop), + BodyNode::IfChain(ifchain) => self.write_if_chain(ifchain), + } + } + + 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() + 6 + } + ElementAttr::AttrExpression { name, value } => { + value.span().line_length() + name.span().line_length() + 6 + } + ElementAttr::CustomAttrText { value, name } => { + value.source.as_ref().unwrap().value().len() + name.value().len() + 6 + } + ElementAttr::CustomAttrExpression { name, value } => { + name.value().len() + value.span().line_length() + 6 + } + ElementAttr::EventTokens { tokens, name } => { + let location = Location::new(tokens.span().start()); + + let len = if let std::collections::hash_map::Entry::Vacant(e) = + self.cached_formats.entry(location) + { + let formatted = prettyplease::unparse_expr(tokens); + let len = if formatted.contains('\n') { + 10000 + } else { + formatted.len() + }; + e.insert(formatted); + len + } else { + self.cached_formats[&location].len() + }; + + len + name.span().line_length() + 6 + } + }; + } + + total + } + + 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() + } + + fn write_for_loop(&mut self, forloop: &ForLoop) -> std::fmt::Result { + write!( + self.out, + "for {} in {} {{", + forloop.pat.clone().into_token_stream(), + prettyplease::unparse_expr(&forloop.expr) + )?; + + if forloop.body.is_empty() { + write!(self.out, "}}")?; + return Ok(()); + } + + self.write_body_indented(&forloop.body)?; + + self.out.tabbed_line()?; + write!(self.out, "}}")?; + + Ok(()) + } + + fn write_if_chain(&mut self, ifchain: &ExprIf) -> std::fmt::Result { + self.write_raw_expr(ifchain.span()) + } +} + +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..19e636926 100644 --- a/packages/autofmt/tests/samples.rs +++ b/packages/autofmt/tests/samples.rs @@ -1,32 +1,44 @@ macro_rules! twoway { - ($val:literal => $name:ident) => { - #[test] - fn $name() { - let src = include_str!(concat!("./samples/", $val, ".rsx")); - let formatted = dioxus_autofmt::fmt_file(src); - let out = dioxus_autofmt::apply_formats(src, formatted); - pretty_assertions::assert_eq!(&src, &out); - } + ( + $( + + // doc attrs + $( #[doc = $doc:expr] )* + $name:ident + ),* + ) => { + $( + $( #[doc = $doc] )* + #[test] + fn $name() { + let src = include_str!(concat!("./samples/", stringify!($name), ".rsx")); + let formatted = dioxus_autofmt::fmt_file(src); + let out = dioxus_autofmt::apply_formats(src, formatted); + pretty_assertions::assert_eq!(&src, &out); + } + )* }; } -twoway! ("simple" => simple); - -twoway! ("comments" => comments); - -twoway! ("attributes" => attributes); - -twoway! ("manual_props" => manual_props); - -twoway! ("complex" => complex); - -twoway! ("tiny" => tiny); - -twoway! ("tinynoopt" => tinynoopt); - -twoway! ("long" => long); - -twoway! ("key" => key); - -// Disabled because we can't handle comments on exprs yet -twoway! ("multirsx" => multirsx); +twoway![ + simple, + comments, + attributes, + manual_props, + complex, + tiny, + tinynoopt, + long, + key, + multirsx, + commentshard, + emoji, + messy_indent, + long_exprs, + ifchain_forloop, + t2, + reallylong, + immediate_expr, + collapse_expr, + trailing_expr +]; diff --git a/packages/autofmt/tests/samples/attributes.rsx b/packages/autofmt/tests/samples/attributes.rsx index 68bfa33c9..fa08b1cf4 100644 --- a/packages/autofmt/tests/samples/attributes.rsx +++ b/packages/autofmt/tests/samples/attributes.rsx @@ -45,6 +45,15 @@ rsx! { a: "123" } - // Short attributes - div { a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123" } + div { + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123" + } } diff --git a/packages/autofmt/tests/samples/collapse_expr.rsx b/packages/autofmt/tests/samples/collapse_expr.rsx new file mode 100644 index 000000000..189717a0a --- /dev/null +++ b/packages/autofmt/tests/samples/collapse_expr.rsx @@ -0,0 +1,4 @@ +fn itworks() { + rsx!( "{name}", "{name}", "{name}" ) +} + 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/autofmt/tests/samples/emoji.rsx b/packages/autofmt/tests/samples/emoji.rsx new file mode 100644 index 000000000..066491583 --- /dev/null +++ b/packages/autofmt/tests/samples/emoji.rsx @@ -0,0 +1,5 @@ +rsx! { + div { class: "asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd", + section { "🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀" } + } +} diff --git a/packages/autofmt/tests/samples/ifchain_forloop.rsx b/packages/autofmt/tests/samples/ifchain_forloop.rsx new file mode 100644 index 000000000..9ec7326af --- /dev/null +++ b/packages/autofmt/tests/samples/ifchain_forloop.rsx @@ -0,0 +1,13 @@ +rsx! { + // Does this work? + for i in b { + // Hey it works? + div {} + } + + // Some ifchain + if a > 10 { + // + rsx! { div {} } + } +} diff --git a/packages/autofmt/tests/samples/immediate_expr.rsx b/packages/autofmt/tests/samples/immediate_expr.rsx new file mode 100644 index 000000000..9e298499d --- /dev/null +++ b/packages/autofmt/tests/samples/immediate_expr.rsx @@ -0,0 +1,4 @@ +fn it_works() { + cx.render(rsx!(())) +} + diff --git a/packages/autofmt/tests/samples/long.rsx b/packages/autofmt/tests/samples/long.rsx index 2464a1905..c5ace9cc3 100644 --- a/packages/autofmt/tests/samples/long.rsx +++ b/packages/autofmt/tests/samples/long.rsx @@ -33,6 +33,9 @@ pub fn Explainer<'a>( } cx.render(rsx! { - div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", + left, + right + } }) } diff --git a/packages/autofmt/tests/samples/long_exprs.rsx b/packages/autofmt/tests/samples/long_exprs.rsx new file mode 100644 index 000000000..fb4dab93f --- /dev/null +++ b/packages/autofmt/tests/samples/long_exprs.rsx @@ -0,0 +1,17 @@ +rsx! { + div { + div { + div { + div { + section { class: "body-font overflow-hidden dark:bg-ideblack", + div { class: "container px-6 mx-auto", + div { class: "-my-8 divide-y-2 divide-gray-100", + POSTS.iter().enumerate().map(|(id, post)| rsx! { BlogPostItem { post: post, id: id } }) + } + } + } + } + } + } + } +} diff --git a/packages/autofmt/tests/samples/messy_indent.rsx b/packages/autofmt/tests/samples/messy_indent.rsx new file mode 100644 index 000000000..0fba72031 --- /dev/null +++ b/packages/autofmt/tests/samples/messy_indent.rsx @@ -0,0 +1,13 @@ +fn SaveClipboard(cx: Scope) -> Element { + rsx! { + div { class: "relative w-1/2 {align} max-w-md leading-8", + h2 { class: "mb-6 text-3xl leading-tight md:text-4xl md:leading-tight lg:text-3xl lg:leading-tight font-heading font-mono font-bold", + "{title}" + } + } + }; + + cx.render(rsx! { + div { "hello world", "hello world", "hello world" } + }) +} diff --git a/packages/autofmt/tests/samples/reallylong.rsx b/packages/autofmt/tests/samples/reallylong.rsx new file mode 100644 index 000000000..98e9765ae --- /dev/null +++ b/packages/autofmt/tests/samples/reallylong.rsx @@ -0,0 +1,15 @@ +pub static Icon3: Component<()> = |cx| { + cx.render(rsx! { + svg { + class: "w-6 h-6", + stroke_linecap: "round", + fill: "none", + stroke_linejoin: "round", + stroke_width: "2", + stroke: "currentColor", + view_box: "0 0 24 24", + path { d: "M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" } + circle { cx: "12", cy: "7", r: "4" } + } + }) +}; diff --git a/packages/autofmt/tests/samples/simple.rsx b/packages/autofmt/tests/samples/simple.rsx index 54ff5f867..a0e573974 100644 --- a/packages/autofmt/tests/samples/simple.rsx +++ b/packages/autofmt/tests/samples/simple.rsx @@ -37,7 +37,11 @@ rsx! { // One level compression div { - a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", "Send invitation" } + a { + class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", + href: "#", + "Send invitation" + } } // Components diff --git a/packages/autofmt/tests/samples/t2.rsx b/packages/autofmt/tests/samples/t2.rsx new file mode 100644 index 000000000..3dab991b4 --- /dev/null +++ b/packages/autofmt/tests/samples/t2.rsx @@ -0,0 +1,7 @@ +rsx! { + div {} + div { + // div { + } +} + diff --git a/packages/autofmt/tests/samples/tiny.rsx b/packages/autofmt/tests/samples/tiny.rsx index 6bd50b9dd..edbfb762b 100644 --- a/packages/autofmt/tests/samples/tiny.rsx +++ b/packages/autofmt/tests/samples/tiny.rsx @@ -1 +1,10 @@ -rsx! { div {} } +fn ItWorks() { + rsx! { + div { + div { + div {} + div {} + } + } + } +} diff --git a/packages/autofmt/tests/samples/trailing_expr.rsx b/packages/autofmt/tests/samples/trailing_expr.rsx new file mode 100644 index 000000000..667b8bb32 --- /dev/null +++ b/packages/autofmt/tests/samples/trailing_expr.rsx @@ -0,0 +1,7 @@ +fn it_works() { + cx.render(rsx! { + div { + span { "Description: ", package.description.as_deref().unwrap_or("❌❌❌❌ missing") } + } + }) +} diff --git a/packages/autofmt/tests/wrong/multiexpr.rsx b/packages/autofmt/tests/wrong/multiexpr.rsx index 1c3f38434..56c57e37e 100644 --- a/packages/autofmt/tests/wrong/multiexpr.rsx +++ b/packages/autofmt/tests/wrong/multiexpr.rsx @@ -1,3 +1,8 @@ -cx.render(rsx! { - div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } -}) +fn ItWroks() { + cx.render(rsx! { + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", + left, + right + } + }) +} diff --git a/packages/autofmt/tests/wrong/multiexpr.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr.wrong.rsx index 1c3f38434..5732f2cdd 100644 --- a/packages/autofmt/tests/wrong/multiexpr.wrong.rsx +++ b/packages/autofmt/tests/wrong/multiexpr.wrong.rsx @@ -1,3 +1,5 @@ -cx.render(rsx! { - div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } -}) +fn ItWroks() { + cx.render(rsx! { + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } + }) +} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index e8dd1e1ef..843470646 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" @@ -8,7 +8,7 @@ license = "MIT/Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" -keywords = ["dom", "ui", "gui", "react", "wasm"] +keywords = ["dom", "ui", "gui", "react"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] @@ -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.3" } # testing [dev-dependencies] diff --git a/packages/core-macro/README.md b/packages/core-macro/README.md index d9527a411..bcf1572cf 100644 --- a/packages/core-macro/README.md +++ b/packages/core-macro/README.md @@ -1,7 +1,48 @@ -# core-macro +# dioxus-core-macro -This crate implements these macros: -- `format_args_f`: for f-string interpolation inside of text blocks -- `Props`: derive macro for typed-builder with props configurations -- -- + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-core-macro.svg +[crates-url]: https://crates.io/crates/dioxus-core-macro + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) | +[API Docs](https://docs.rs/dioxus-core-macro/latest/dioxus_core_macro) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +`dioxus-core-macro` provides a handful of helpful macros used by the `dioxus` crate. These include: + +- The `rsx!` macro that underpins templates and node creation +- The `inline_props` macro transforms function arguments into an auto-derived struct +- The `format_args_f` macro which allows f-string formatting with support for expressions + + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT without any additional +terms or conditions. diff --git a/packages/core-macro/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs index dc9c4711b..814724b56 100644 --- a/packages/core-macro/src/inlineprops.rs +++ b/packages/core-macro/src/inlineprops.rs @@ -95,7 +95,7 @@ impl ToTokens for InlinePropsBody { quote! { #vis #f } }); - let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site()); + let struct_name = Ident::new(&format!("{ident}Props"), Span::call_site()); let field_names = inputs.iter().filter_map(|f| match f { FnArg::Receiver(_) => todo!(), @@ -145,6 +145,25 @@ impl ToTokens for InlinePropsBody { (quote! { #lifetime, }, fn_generics, quote! { #generics }) }; + let generics_no_bounds = { + let mut generics = generics.clone(); + generics.params = generics + .params + .iter() + .map(|it| match it { + GenericParam::Type(tp) => { + let mut tp = tp.clone(); + tp.bounds.clear(); + + GenericParam::Type(tp) + } + _ => it.clone(), + }) + .collect(); + + generics + }; + out_tokens.append_all(quote! { #modifiers #[allow(non_camel_case_types)] @@ -155,10 +174,10 @@ impl ToTokens for InlinePropsBody { } #(#attrs)* - #maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output + #maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics_no_bounds>) #output #where_clause { - let #struct_name { #(#field_names),* } = &cx.props; + let #struct_name { #(#field_names),* } = &#cx_token.props; #block } }); diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 4e4f5600a..501c38e7b 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -1,5 +1,6 @@ use proc_macro::TokenStream; use quote::ToTokens; +use rsx::RenderCallBody; use syn::parse_macro_input; mod inlineprops; @@ -41,10 +42,7 @@ pub fn rsx(s: TokenStream) -> TokenStream { pub fn render(s: TokenStream) -> TokenStream { match syn::parse::(s) { Err(err) => err.to_compile_error().into(), - Ok(mut body) => { - body.inline_cx = true; - body.into_token_stream().into() - } + Ok(body) => RenderCallBody(body).into_token_stream().into(), } } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 56a3407f7..5f3de9dc9 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -323,7 +323,7 @@ mod field_info { let tokenized_code = TokenStream::from_str(&code.value())?; self.default = Some( syn::parse(tokenized_code.into()) - .map_err(|e| Error::new_spanned(code, format!("{}", e)))?, + .map_err(|e| Error::new_spanned(code, format!("{e}")))?, ); } else { return Err(Error::new_spanned(assign.right, "Expected string")); @@ -332,7 +332,7 @@ mod field_info { } _ => Err(Error::new_spanned( &assign, - format!("Unknown parameter {:?}", name), + format!("Unknown parameter {name:?}"), )), } } @@ -503,11 +503,11 @@ mod struct_info { builder_attr, builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()), conversion_helper_trait_name: syn::Ident::new( - &format!("{}_Optional", builder_name), + &format!("{builder_name}_Optional"), proc_macro2::Span::call_site(), ), core: syn::Ident::new( - &format!("{}_core", builder_name), + &format!("{builder_name}_core"), proc_macro2::Span::call_site(), ), }) @@ -594,7 +594,6 @@ Finally, call `.build()` to create the instance of `{name}`. None => { let doc = format!( "Builder for [`{name}`] instances.\n\nSee [`{name}::builder()`] for more info.", - name = name ); quote!(#[doc = #doc]) } @@ -709,9 +708,9 @@ Finally, call `.build()` to create the instance of `{name}`. }); let reconstructing = self.included_fields().map(|f| f.name); - let &FieldInfo { - name: ref field_name, - ty: ref field_type, + let FieldInfo { + name: field_name, + ty: field_type, .. } = field; let mut ty_generics: Vec = self @@ -810,7 +809,7 @@ Finally, call `.build()` to create the instance of `{name}`. ), proc_macro2::Span::call_site(), ); - let repeated_fields_error_message = format!("Repeated field {}", field_name); + let repeated_fields_error_message = format!("Repeated field {field_name}"); Ok(quote! { #[allow(dead_code, non_camel_case_types, missing_docs)] @@ -929,7 +928,7 @@ Finally, call `.build()` to create the instance of `{name}`. ), proc_macro2::Span::call_site(), ); - let early_build_error_message = format!("Missing required field {}", field_name); + let early_build_error_message = format!("Missing required field {field_name}"); Ok(quote! { #[doc(hidden)] @@ -1037,7 +1036,7 @@ Finally, call `.build()` to create the instance of `{name}`. // I’d prefer “a” or “an” to “its”, but determining which is grammatically // correct is roughly impossible. let doc = - format!("Finalise the builder and create its [`{}`] instance", name); + format!("Finalise the builder and create its [`{name}`] instance"); quote!(#[doc = #doc]) } } @@ -1132,7 +1131,7 @@ Finally, call `.build()` to create the instance of `{name}`. } _ => Err(Error::new_spanned( &assign, - format!("Unknown parameter {:?}", name), + format!("Unknown parameter {name:?}"), )), } } @@ -1146,7 +1145,7 @@ Finally, call `.build()` to create the instance of `{name}`. } _ => Err(Error::new_spanned( &path, - format!("Unknown parameter {:?}", name), + format!("Unknown parameter {name:?}"), )), } } @@ -1161,7 +1160,7 @@ Finally, call `.build()` to create the instance of `{name}`. let call_func = quote!(#call_func); Error::new_spanned( &call.func, - format!("Illegal builder setting group {}", call_func), + format!("Illegal builder setting group {call_func}"), ) })?; match subsetting_name.as_str() { @@ -1173,7 +1172,7 @@ Finally, call `.build()` to create the instance of `{name}`. } _ => Err(Error::new_spanned( &call.func, - format!("Illegal builder setting group name {}", subsetting_name), + format!("Illegal builder setting group name {subsetting_name}"), )), } } diff --git a/packages/core-macro/tests/ifmt.rs b/packages/core-macro/tests/ifmt.rs index e4fd1754f..3e03d37a9 100644 --- a/packages/core-macro/tests/ifmt.rs +++ b/packages/core-macro/tests/ifmt.rs @@ -8,11 +8,11 @@ fn formatting_compiles() { // escape sequences work assert_eq!( format_args_f!("{x:?} {{}}}}").to_string(), - format!("{:?} {{}}}}", x) + format!("{x:?} {{}}}}") ); assert_eq!( format_args_f!("{{{{}} {x:?}").to_string(), - format!("{{{{}} {:?}", x) + format!("{{{{}} {x:?}") ); // paths in formating works @@ -27,6 +27,6 @@ fn formatting_compiles() { // allows duplicate format args assert_eq!( format_args_f!("{x:?} {x:?}").to_string(), - format!("{:?} {:?}", x, x) + format!("{x:?} {x:?}") ); } diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json new file mode 100644 index 000000000..eb230f7ba --- /dev/null +++ b/packages/core/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.checkOnSave.allTargets": false +} diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index bca8bd031..b86466692 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.2" authors = ["Jonathan Kelley"] edition = "2018" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" @@ -8,7 +8,7 @@ license = "MIT/Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" -keywords = ["dom", "ui", "gui", "react", "wasm"] +keywords = ["dom", "ui", "gui", "react"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -23,7 +23,9 @@ rustc-hash = "1.1.0" # Used in diffing longest-increasing-subsequence = "0.1.0" -futures-util = { version = "0.3", default-features = false } +futures-util = { version = "0.3", default-features = false, features = [ + "alloc", +] } slab = "0.4" @@ -37,10 +39,13 @@ log = "0.4.17" # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } +bumpslab = { version = "0.2.0" } + [dev-dependencies] -tokio = { version = "*", features = ["full"] } +tokio = { version = "1", features = ["full"] } dioxus = { path = "../dioxus" } pretty_assertions = "1.3.0" +rand = "0.8.5" [features] default = [] diff --git a/packages/core/README.md b/packages/core/README.md index d3ad1946f..7659f6713 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -13,7 +13,7 @@ dioxus-core is a fast and featureful VirtualDom implementation written in and fo - Error boundaries through the `anyhow` crate - Customizable memoization -If just starting out, check out the Guides first. +If you are just starting, check out the Guides first. # General Theory @@ -21,7 +21,7 @@ The dioxus-core `VirtualDom` object is built around the concept of a `Template`. Each component in the VirtualDom works as a dedicated render loop where re-renders are triggered by events external to the VirtualDom, or from the components themselves. -When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result`. Between two renders, Dioxus compares the inner `VNode` object, and calculates the differences of the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and new layout, Dioxus will write modifications to the `Mutations` object. +When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result`. Between two renders, Dioxus compares the inner `VNode` object and calculates the differences between the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and the new layout, Dioxus will write modifications to the `Mutations` object. Dioxus expects the target renderer to save its nodes in a list. Each element is given a numerical ID which can be used to directly index into that list for O(1) lookups. @@ -33,37 +33,48 @@ The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler s First, start with your app: -```rust, ignore +```rust +# use dioxus::core::Mutations; +use dioxus::prelude::*; + +// First, declare a root component fn app(cx: Scope) -> Element { - cx.render(rsx!( div { "hello world" } )) + cx.render(rsx!{ + div { "hello world" } + }) } + +fn main() { + // Next, create a new VirtualDom using this app as the root component. + let mut dom = VirtualDom::new(app); + + // The initial render of the dom will generate a stream of edits for the real dom to apply + let mutations = dom.rebuild(); + + // Somehow, you can apply these edits to the real dom + apply_edits_to_real_dom(mutations); +} + +# fn apply_edits_to_real_dom(mutations: Mutations) {} ``` -Then, we'll want to create a new VirtualDom using this app as the root component. - -```rust, ignore -let mut dom = VirtualDom::new(app); -``` - -To build the app into a stream of mutations, we'll use [`VirtualDom::rebuild`]: - -```rust, ignore -let mutations = dom.rebuild(); - -apply_edits_to_real_dom(mutations); -``` We can then wait for any asynchronous components or pending futures using the `wait_for_work()` method. If we have a deadline, then we can use render_with_deadline instead: +```rust +# #![allow(unused)] +# use dioxus::prelude::*; -```rust, ignore +# use std::time::Duration; +# async fn wait(mut dom: VirtualDom) { // Wait for the dom to be marked dirty internally dom.wait_for_work().await; // Or wait for a deadline and then collect edits -dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16))); +let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16))); +# } ``` -If an event occurs from outside the virtualdom while waiting for work, then we can cancel the wait using a `select!` block and inject the event. +If an event occurs from outside the VirtualDom while waiting for work, then we can cancel the wait using a `select!` block and inject the event. ```rust, ignore loop { @@ -88,18 +99,18 @@ Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus - React: hooks, concurrency, suspense - Dodrio: bump allocation, double buffering, and some diffing architecture -Dioxus-core leverages some really cool techniques and hits a very high level of parity with mature frameworks. However, Dioxus also brings some new unique features: +Dioxus-core hits a very high level of parity with mature frameworks. However, Dioxus also brings some new unique features: - managed lifetimes for borrowed data - placeholder approach for suspended vnodes - fiber/interruptible diffing algorithm -- custom memory allocator for vnodes and all text content +- custom memory allocator for VNodes and all text content - support for fragments w/ lazy normalization - slab allocator for scopes -- mirrored-slab approach for remote vdoms +- mirrored-slab approach for remote VirtualDoms - dedicated subtrees for rendering into separate contexts from the same app -There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, it is possible that zero allocations will need to be performed once the app has been loaded. Only when new components are added to the dom will allocations occur. For a given component, the space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components. +There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, no allocations may be needed once the app has been loaded. Only when new components are added to the dom will allocations occur. For a given component, the space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components. All in all, Dioxus treats memory as a valuable resource. Combined with the memory-efficient footprint of Wasm compilation, Dioxus apps can scale to thousands of components and still stay snappy. @@ -112,5 +123,5 @@ The final implementation of Dioxus must: - Be concurrent. Components should be able to pause rendering to let the screen paint the next frame. - Be disconnected from a specific renderer (no WebSys dependency in the core crate). - Support server-side-rendering (SSR). VNodes should render to a string that can be served via a web server. -- Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs. -- Be modular. Components and hooks should be work anywhere without worrying about target platform. +- Be "live". Components should be able to be both server-rendered and client rendered without needing frontend APIs. +- Be modular. Components and hooks should work anywhere without worrying about the target platform. diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index c3ea4dc2c..7014a2771 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{marker::PhantomData, panic::AssertUnwindSafe}; use crate::{ innerlude::Scoped, @@ -62,12 +62,19 @@ where } fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> { - let scope: &mut Scoped

= cx.bump().alloc(Scoped { - props: &self.props, - scope: cx, - }); + let res = std::panic::catch_unwind(AssertUnwindSafe(move || { + // Call the render function directly + let scope: &mut Scoped

= cx.bump().alloc(Scoped { + props: &self.props, + scope: cx, + }); - // Call the render function directly - (self.render_fn)(scope).into_return(cx) + (self.render_fn)(scope).into_return(cx) + })); + + match res { + Ok(e) => e, + Err(_) => RenderReturn::default(), + } } } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 8e5858a4c..caa863adf 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,6 +1,8 @@ +use std::ptr::NonNull; + use crate::{ - nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode, - ScopeId, + innerlude::DirtyScope, nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, + AttributeValue, DynamicNode, ScopeId, }; use bumpalo::boxed::Box as BumpBox; @@ -17,7 +19,7 @@ pub(crate) struct ElementRef { pub path: ElementPath, // The actual template - pub template: *const VNode<'static>, + pub template: Option>>, } #[derive(Clone, Copy)] @@ -27,9 +29,9 @@ pub enum ElementPath { } impl ElementRef { - pub(crate) fn null() -> Self { + pub(crate) fn none() -> Self { Self { - template: std::ptr::null_mut(), + template: None, path: ElementPath::Root(0), } } @@ -44,12 +46,21 @@ impl VirtualDom { self.next_reference(template, ElementPath::Root(path)) } + pub(crate) fn next_null(&mut self) -> ElementId { + let entry = self.elements.vacant_entry(); + let id = entry.key(); + + entry.insert(ElementRef::none()); + ElementId(id) + } + fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); entry.insert(ElementRef { - template: template as *const _ as *mut _, + // We know this is non-null because it comes from a reference + template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }), path, }); ElementId(id) @@ -77,67 +88,59 @@ impl VirtualDom { } // Drop a scope and all its children - pub(crate) fn drop_scope(&mut self, id: ScopeId) { + // + // Note: This will not remove any ids from the arena + pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) { + self.dirty_scopes.remove(&DirtyScope { + height: self.scopes[id].height, + id, + }); + self.ensure_drop_safety(id); - if let Some(root) = self.scopes[id.0].as_ref().try_root_node() { - if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } { - self.drop_scope_inner(node) - } - } - if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } { - if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } { - self.drop_scope_inner(node) + if recursive { + if let Some(root) = self.scopes[id].try_root_node() { + if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } { + self.drop_scope_inner(node) + } } } - self.scopes[id.0].props.take(); - - let scope = &mut self.scopes[id.0]; + let scope = &mut self.scopes[id]; // Drop all the hooks once the children are dropped // this means we'll drop hooks bottom-up for hook in scope.hook_list.get_mut().drain(..) { drop(unsafe { BumpBox::from_raw(hook) }); } + + // Drop all the futures once the hooks are dropped + for task_id in scope.spawned_tasks.borrow_mut().drain() { + scope.tasks.remove(task_id); + } + + self.scopes.remove(id); } fn drop_scope_inner(&mut self, node: &VNode) { - node.clear_listeners(); node.dynamic_nodes.iter().for_each(|node| match node { DynamicNode::Component(c) => { if let Some(f) = c.scope.get() { - self.drop_scope(f); + self.drop_scope(f, true); } c.props.take(); } DynamicNode::Fragment(nodes) => { nodes.iter().for_each(|node| self.drop_scope_inner(node)) } - DynamicNode::Placeholder(t) => { - if let Some(id) = t.id.get() { - self.try_reclaim(id); - } - } - DynamicNode::Text(t) => { - if let Some(id) = t.id.get() { - self.try_reclaim(id); - } - } + DynamicNode::Placeholder(_) => {} + DynamicNode::Text(_) => {} }); - - for root in node.root_ids { - if let Some(id) = root.get() { - if id.0 != 0 { - self.try_reclaim(id); - } - } - } } /// Descend through the tree, removing any borrowed props and listeners pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { - let scope = &self.scopes[scope_id.0]; + let scope = &self.scopes[scope_id]; // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we // run the hooks (which hold an &mut Reference) @@ -155,21 +158,27 @@ 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(); + } + _ => (), } }); } } impl ElementPath { - pub(crate) fn is_ascendant(&self, big: &&[u8]) -> bool { + pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool { match *self { - ElementPath::Deep(small) => small.len() <= big.len() && small == &big[..small.len()], - ElementPath::Root(r) => big.len() == 1 && big[0] == r as u8, + ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()], + ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8, } } } 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 239bd85fa..80561e937 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,17 +1,67 @@ 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; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; -use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext}; +use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext, Template}; use std::cell::Cell; -use std::iter::{Enumerate, Peekable}; +use std::iter::Peekable; use std::rc::Rc; -use std::slice; use TemplateNode::*; +#[cfg(debug_assertions)] +fn sort_bfs(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> { + let mut with_indecies = paths.iter().copied().enumerate().collect::>(); + with_indecies.sort_unstable_by(|(_, a), (_, b)| { + let mut a = a.iter(); + let mut b = b.iter(); + loop { + match (a.next(), b.next()) { + (Some(a), Some(b)) => { + if a != b { + return a.cmp(b); + } + } + // The shorter path goes first + (None, Some(_)) => return std::cmp::Ordering::Less, + (Some(_), None) => return std::cmp::Ordering::Greater, + (None, None) => return std::cmp::Ordering::Equal, + } + } + }); + with_indecies +} + +#[test] +#[cfg(debug_assertions)] +fn sorting() { + let r: [(usize, &[u8]); 5] = [ + (0, &[0, 1]), + (1, &[0, 2]), + (2, &[1, 0]), + (3, &[1, 0, 1]), + (4, &[1, 2]), + ]; + assert_eq!( + sort_bfs(&[&[0, 1,], &[0, 2,], &[1, 0,], &[1, 0, 1,], &[1, 2,],]), + r + ); + let r: [(usize, &[u8]); 6] = [ + (0, &[0]), + (1, &[0, 1]), + (2, &[0, 1, 2]), + (3, &[1]), + (4, &[1, 2]), + (5, &[2]), + ]; + assert_eq!( + sort_bfs(&[&[0], &[0, 1], &[0, 1, 2], &[1], &[1, 2], &[2],]), + r + ); +} + impl<'b> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. /// @@ -25,21 +75,77 @@ impl<'b> VirtualDom { /// Create this template and write its mutations pub(crate) fn create(&mut self, node: &'b VNode<'b>) -> usize { - // 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.name) { - self.register_template(node); + // check for a overriden template + #[cfg(debug_assertions)] + { + let (path, byte_index) = node.template.get().name.rsplit_once(':').unwrap(); + if let Some(template) = self + .templates + .get(path) + .and_then(|map| map.get(&byte_index.parse().unwrap())) + { + node.template.set(*template); + } } + // Intialize the root nodes slice + node.root_ids + .intialize(vec![ElementId(0); node.template.get().roots.len()].into_boxed_slice()); + + // The best renderers will have templates prehydrated and registered + // Just in case, let's create the template using instructions anyways + self.register_template(node.template.get()); + // we know that this will generate at least one mutation per node - self.mutations.edits.reserve(node.template.roots.len()); + self.mutations + .edits + .reserve(node.template.get().roots.len()); // Walk the roots, creating nodes and assigning IDs + // nodes in an iterator of ((dynamic_node_index, sorted_index), path) // todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS) - let mut attrs = node.template.attr_paths.iter().enumerate().peekable(); - let mut nodes = node.template.node_paths.iter().enumerate().peekable(); + #[cfg(not(debug_assertions))] + let (mut attrs, mut nodes) = ( + node.template + .get() + .attr_paths + .iter() + .copied() + .enumerate() + .peekable(), + node.template + .get() + .node_paths + .iter() + .copied() + .enumerate() + .map(|(i, path)| ((i, i), path)) + .peekable(), + ); + // If this is a debug build, we need to check that the paths are in the correct order because hot reloading can cause scrambled states + + #[cfg(debug_assertions)] + let (attrs_sorted, nodes_sorted) = { + ( + sort_bfs(node.template.get().attr_paths), + sort_bfs(node.template.get().node_paths), + ) + }; + #[cfg(debug_assertions)] + let (mut attrs, mut nodes) = { + ( + attrs_sorted.into_iter().peekable(), + nodes_sorted + .iter() + .copied() + .enumerate() + .map(|(i, (id, path))| ((id, i), path)) + .peekable(), + ) + }; node.template + .get() .roots .iter() .enumerate() @@ -48,7 +154,14 @@ impl<'b> VirtualDom { nodes.next().unwrap(); self.write_dynamic_root(node, *id) } - Element { .. } => self.write_element_root(node, idx, &mut attrs, &mut nodes), + Element { .. } => { + #[cfg(not(debug_assertions))] + let id = self.write_element_root(node, idx, &mut attrs, &mut nodes, &[]); + #[cfg(debug_assertions)] + let id = + self.write_element_root(node, idx, &mut attrs, &mut nodes, &nodes_sorted); + id + } Text { .. } => self.write_static_text_root(node, idx), }) .sum() @@ -65,8 +178,9 @@ impl<'b> VirtualDom { fn write_dynamic_root(&mut self, template: &'b VNode<'b>, idx: usize) -> usize { use DynamicNode::*; match &template.dynamic_nodes[idx] { - node @ Fragment(_) => self.create_dynamic_node(template, node, idx), - node @ Component { .. } => self.create_dynamic_node(template, node, idx), + node @ Component { .. } | node @ Fragment(_) => { + self.create_dynamic_node(template, node, idx) + } Placeholder(VPlaceholder { id }) => { let id = self.set_slot(template, id, idx); self.mutations.push(CreatePlaceholder { id }); @@ -98,17 +212,18 @@ impl<'b> VirtualDom { &mut self, template: &'b VNode<'b>, root_idx: usize, - dynamic_attrs: &mut Peekable>>, - dynamic_nodes: &mut Peekable>>, + dynamic_attrs: &mut Peekable>, + dynamic_nodes_iter: &mut Peekable>, + dynamic_nodes: &[(usize, &'static [u8])], ) -> usize { // Load the template root and get the ID for the node on the stack let root_on_stack = self.load_template_root(template, root_idx); // Write all the attributes below this root - self.write_attrs_on_root(dynamic_attrs, root_idx, root_on_stack, template); + self.write_attrs_on_root(dynamic_attrs, root_idx as u8, root_on_stack, template); // Load in all of the placeholder or dynamic content under this root too - self.load_placeholders(dynamic_nodes, root_idx, template); + self.load_placeholders(dynamic_nodes_iter, dynamic_nodes, root_idx as u8, template); 1 } @@ -126,22 +241,32 @@ impl<'b> VirtualDom { /// } /// } /// ``` + #[allow(unused)] fn load_placeholders( &mut self, - dynamic_nodes: &mut Peekable>>, - root_idx: usize, + dynamic_nodes_iter: &mut Peekable>, + dynamic_nodes: &[(usize, &'static [u8])], + root_idx: u8, template: &'b VNode<'b>, ) { - let (start, end) = match collect_dyn_node_range(dynamic_nodes, root_idx) { + let (start, end) = match collect_dyn_node_range(dynamic_nodes_iter, root_idx) { Some((a, b)) => (a, b), None => return, }; - for idx in (start..=end).rev() { + // If hot reloading is enabled, we need to map the sorted index to the original index of the dynamic node. If it is disabled, we can just use the sorted index + #[cfg(not(debug_assertions))] + let reversed_iter = (start..=end).rev(); + #[cfg(debug_assertions)] + let reversed_iter = (start..=end) + .rev() + .map(|sorted_index| dynamic_nodes[sorted_index].0); + + for idx in reversed_iter { let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx); if m > 0 { // The path is one shorter because the top node is the root - let path = &template.template.node_paths[idx][1..]; + let path = &template.template.get().node_paths[idx][1..]; self.mutations.push(ReplacePlaceholder { m, path }); } } @@ -149,12 +274,14 @@ impl<'b> VirtualDom { fn write_attrs_on_root( &mut self, - attrs: &mut Peekable>>, - root_idx: usize, + attrs: &mut Peekable>, + root_idx: u8, root: ElementId, node: &VNode, ) { - while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p[0] == root_idx as u8) { + while let Some((mut attr_id, path)) = + attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx)) + { let id = self.assign_static_node_as_dynamic(path, root, node, attr_id); loop { @@ -169,7 +296,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); @@ -177,9 +304,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, @@ -188,32 +323,16 @@ 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!(), } } fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId { // Get an ID for this root since it's a real root let this_id = self.next_root(template, root_idx); - template.root_ids[root_idx].set(Some(this_id)); + template.root_ids.set(root_idx, this_id); self.mutations.push(LoadTemplate { - name: template.template.name, + name: template.template.get().name, index: root_idx, id: this_id, }); @@ -241,7 +360,7 @@ impl<'b> VirtualDom { // if attribute is on a root node, then we've already created the element // Else, it's deep in the template and we should create a new id for it - let id = self.next_element(template, template.template.attr_paths[attr_id]); + let id = self.next_element(template, template.template.get().attr_paths[attr_id]); self.mutations.push(Mutation::AssignId { path: &path[1..], @@ -252,14 +371,67 @@ impl<'b> VirtualDom { } /// Insert a new template into the VirtualDom's template registry - fn register_template(&mut self, template: &'b VNode<'b>) { + pub(crate) fn register_template_first_byte_index(&mut self, mut template: Template<'static>) { // First, make sure we mark the template as seen, regardless if we process it - self.templates - .insert(template.template.name, template.template); + let (path, _) = template.name.rsplit_once(':').unwrap(); + if let Some((_, old_template)) = self + .templates + .entry(path) + .or_default() + .iter_mut() + .min_by_key(|(byte_index, _)| **byte_index) + { + // the byte index of the hot reloaded template could be different + template.name = old_template.name; + *old_template = template; + } else { + // This is a template without any current instances + self.templates + .entry(path) + .or_default() + .insert(usize::MAX, template); + } // If it's all dynamic nodes, then we don't need to register it - if !template.template.is_completely_dynamic() { - self.mutations.templates.push(template.template); + if !template.is_completely_dynamic() { + self.mutations.templates.push(template); + } + } + + /// Insert a new template into the VirtualDom's template registry + // used in conditional compilation + #[allow(unused_mut)] + pub(crate) fn register_template(&mut self, mut template: Template<'static>) { + let (path, byte_index) = template.name.rsplit_once(':').unwrap(); + let byte_index = byte_index.parse::().unwrap(); + // First, check if we've already seen this template + if self + .templates + .get(&path) + .filter(|set| set.contains_key(&byte_index)) + .is_none() + { + // 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); + + // If it's all dynamic nodes, then we don't need to register it + if !template.is_completely_dynamic() { + self.mutations.templates.push(template); + } } } @@ -272,9 +444,9 @@ impl<'b> VirtualDom { use DynamicNode::*; match node { Text(text) => self.create_dynamic_text(template, text, idx), - Fragment(frag) => self.create_fragment(frag), - Placeholder(frag) => self.create_placeholder(frag, template, idx), + Placeholder(place) => self.create_placeholder(place, template, idx), Component(component) => self.create_component_node(template, component, idx), + Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(), } } @@ -285,7 +457,7 @@ impl<'b> VirtualDom { idx: usize, ) -> usize { // Allocate a dynamic element reference for this text node - let new_id = self.next_element(template, template.template.node_paths[idx]); + let new_id = self.next_element(template, template.template.get().node_paths[idx]); // Make sure the text node is assigned to the correct element text.id.set(Some(new_id)); @@ -296,7 +468,7 @@ impl<'b> VirtualDom { // Add the mutation to the list self.mutations.push(HydrateText { id: new_id, - path: &template.template.node_paths[idx][1..], + path: &template.template.get().node_paths[idx][1..], value, }); @@ -311,14 +483,14 @@ impl<'b> VirtualDom { idx: usize, ) -> usize { // Allocate a dynamic element reference for this text node - let id = self.next_element(template, template.template.node_paths[idx]); + let id = self.next_element(template, template.template.get().node_paths[idx]); // Make sure the text node is assigned to the correct element placeholder.id.set(Some(id)); // Assign the ID to the existing node in the template self.mutations.push(AssignId { - path: &template.template.node_paths[idx][1..], + path: &template.template.get().node_paths[idx][1..], id, }); @@ -326,40 +498,38 @@ impl<'b> VirtualDom { 0 } - pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize { - nodes.iter().map(|child| self.create(child)).sum() - } - pub(super) fn create_component_node( &mut self, template: &'b VNode<'b>, component: &'b VComponent<'b>, idx: usize, ) -> usize { - let scope = match component.props.take() { - Some(props) => { - let unbounded_props: Box = unsafe { std::mem::transmute(props) }; - let scope = self.new_scope(unbounded_props, component.name); - scope.id - } + use RenderReturn::*; - // Component is coming back, it probably still exists, right? - None => component.scope.get().unwrap(), - }; + // Load up a ScopeId for this vcomponent + let scope = self.load_scope_from_vcomponent(component); component.scope.set(Some(scope)); - let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() }; - - use RenderReturn::*; - - match return_nodes { - Sync(Some(t)) => self.mount_component(scope, template, t, idx), - Sync(None) => todo!("Propogate error upwards"), - Async(_) => self.mount_component_placeholder(template, idx, scope), + match unsafe { self.run_scope(scope).extend_lifetime_ref() } { + Ready(t) => self.mount_component(scope, template, t, idx), + Aborted(t) => self.mount_aborted(template, t), + Pending(_) => self.mount_async(template, idx, scope), } } + /// Load a scope from a vcomponent. If the props don't exist, that means the component is currently "live" + fn load_scope_from_vcomponent(&mut self, component: &VComponent) -> ScopeId { + component + .props + .take() + .map(|props| { + let unbounded_props: Box = unsafe { std::mem::transmute(props) }; + self.new_scope(unbounded_props, component.name).id + }) + .unwrap_or_else(|| component.scope.get().unwrap()) + } + fn mount_component( &mut self, scope: ScopeId, @@ -380,16 +550,16 @@ impl<'b> VirtualDom { } // If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense - let boundary = match self.scopes[scope.0].has_context::>() { + let boundary = match self.scopes[scope].has_context::>() { Some(boundary) => boundary, _ => return created, }; // Since this is a boundary, use its placeholder within the template as the placeholder for the suspense tree - let new_id = self.next_element(new, parent.template.node_paths[idx]); + let new_id = self.next_element(new, parent.template.get().node_paths[idx]); // Now connect everything to the boundary - self.scopes[scope.0].placeholder.set(Some(new_id)); + self.scopes[scope].placeholder.set(Some(new_id)); // This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary // Note that we break off dynamic mutations only - since static mutations aren't rendered immediately @@ -408,30 +578,32 @@ impl<'b> VirtualDom { // Now assign the placeholder in the DOM self.mutations.push(AssignId { id: new_id, - path: &parent.template.node_paths[idx][1..], + path: &parent.template.get().node_paths[idx][1..], }); 0 } + fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize { + let id = self.next_element(parent, &[]); + self.mutations.push(Mutation::CreatePlaceholder { id }); + placeholder.id.set(Some(id)); + 1 + } + /// Take the rendered nodes from a component and handle them if they were async /// /// IE simply assign an ID to the placeholder - fn mount_component_placeholder( - &mut self, - template: &VNode, - idx: usize, - scope: ScopeId, - ) -> usize { - let new_id = self.next_element(template, template.template.node_paths[idx]); + fn mount_async(&mut self, template: &VNode, idx: usize, scope: ScopeId) -> usize { + let new_id = self.next_element(template, template.template.get().node_paths[idx]); // Set the placeholder of the scope - self.scopes[scope.0].placeholder.set(Some(new_id)); + self.scopes[scope].placeholder.set(Some(new_id)); // Since the placeholder is already in the DOM, we don't create any new nodes self.mutations.push(AssignId { id: new_id, - path: &template.template.node_paths[idx][1..], + path: &template.template.get().node_paths[idx][1..], }); 0 @@ -443,24 +615,26 @@ impl<'b> VirtualDom { slot: &'b Cell>, id: usize, ) -> ElementId { - let id = self.next_element(template, template.template.node_paths[id]); + let id = self.next_element(template, template.template.get().node_paths[id]); slot.set(Some(id)); id } } fn collect_dyn_node_range( - dynamic_nodes: &mut Peekable>>, - root_idx: usize, + dynamic_nodes: &mut Peekable>, + root_idx: u8, ) -> Option<(usize, usize)> { let start = match dynamic_nodes.peek() { - Some((idx, p)) if p[0] == root_idx as u8 => *idx, + Some(((_, idx), [first, ..])) if *first == root_idx => *idx, _ => return None, }; let mut end = start; - while let Some((idx, p)) = dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) { + while let Some(((_, idx), p)) = + dynamic_nodes.next_if(|(_, p)| matches!(p, [idx, ..] if *idx == root_idx)) + { if p.len() == 1 { continue; } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index abf938929..56a2d05de 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}, @@ -15,7 +15,7 @@ use DynamicNode::*; impl<'b> VirtualDom { pub(super) fn diff_scope(&mut self, scope: ScopeId) { - let scope_state = &mut self.scopes[scope.0]; + let scope_state = &mut self.scopes[scope]; self.scope_stack.push(scope); unsafe { @@ -30,31 +30,79 @@ impl<'b> VirtualDom { .try_load_node() .expect("Call rebuild before diffing"); - use RenderReturn::{Async, Sync}; + use RenderReturn::{Aborted, Pending, Ready}; match (old, new) { - (Sync(Some(l)), Sync(Some(r))) => self.diff_node(l, r), + // Normal pathway + (Ready(l), Ready(r)) => self.diff_node(l, r), - // Err cases - (Sync(Some(l)), Sync(None)) => self.diff_ok_to_err(l), - (Sync(None), Sync(Some(r))) => self.diff_err_to_ok(r), - (Sync(None), Sync(None)) => { /* nothing */ } + // Unwind the mutations if need be + (Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p), - // Async - (Sync(Some(_l)), Async(_)) => todo!(), - (Sync(None), Async(_)) => todo!(), - (Async(_), Sync(Some(_r))) => todo!(), - (Async(_), Sync(None)) => { /* nothing */ } - (Async(_), Async(_)) => { /* nothing */ } + // Just move over the placeholder + (Aborted(l), Aborted(r)) => r.id.set(l.id.get()), + + // Becomes async, do nothing while we wait + (Ready(_nodes), Pending(_fut)) => self.diff_ok_to_async(_nodes, scope), + + // Placeholder becomes something + // We should also clear the error now + (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), + + (Aborted(_), Pending(_)) => todo!("async should not resolve here"), + (Pending(_), Ready(_)) => todo!("async should not resolve here"), + (Pending(_), Aborted(_)) => todo!("async should not resolve here"), + (Pending(_), Pending(_)) => { + // All suspense should resolve before we diff it again + panic!("Should not roll from suspense to suspense."); + } }; } self.scope_stack.pop(); } - fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>) {} - fn diff_err_to_ok(&mut self, _l: &'b VNode<'b>) {} + fn diff_ok_to_async(&mut self, _new: &'b VNode<'b>, _scope: ScopeId) { + // + } + + fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { + let id = self.next_null(); + p.id.set(Some(id)); + self.mutations.push(Mutation::CreatePlaceholder { id }); + + let pre_edits = self.mutations.edits.len(); + + self.remove_node(l, true); + + // We should always have a remove mutation + // Eventually we don't want to generate placeholders, so this might not be true. But it's true today + assert!(self.mutations.edits.len() > pre_edits); + + // We want to optimize the replace case to use one less mutation if possible + // Since mutations are done in reverse, the last node removed will be the first in the stack + // Instead of *just* removing it, we can use the replace mutation + match self.mutations.edits.pop().unwrap() { + Mutation::Remove { id } => self.mutations.push(Mutation::ReplaceWith { id, m: 1 }), + _ => panic!("Expected remove mutation from remove_node"), + }; + } fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { + // If hot reloading is enabled, we need to make sure we're using the latest template + #[cfg(debug_assertions)] + { + let (path, byte_index) = right_template.template.get().name.rsplit_once(':').unwrap(); + if let Some(map) = self.templates.get(path) { + let byte_index = byte_index.parse::().unwrap(); + if let Some(&template) = map.get(&byte_index) { + right_template.template.set(template); + if template != left_template.template.get() { + return self.replace(left_template, [right_template]); + } + } + } + } + // If the templates are the same, we don't need to do anything, nor do we want to if templates_are_the_same(left_template, right_template) { return; @@ -99,11 +147,14 @@ impl<'b> VirtualDom { }); // Make sure the roots get transferred over while we're here - left_template - .root_ids - .iter() - .zip(right_template.root_ids.iter()) - .for_each(|(left, right)| right.set(left.get())); + right_template.root_ids.transfer(&left_template.root_ids); + + // Update the node refs + for i in 0..right_template.root_ids.len() { + if let Some(root_id) = right_template.root_ids.get(i) { + self.update_template(root_id, right_template); + } + } } fn diff_dynamic_node( @@ -118,28 +169,22 @@ impl<'b> VirtualDom { (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), (Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx), - (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, right), + (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right), (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), }; } - 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( @@ -164,7 +209,7 @@ impl<'b> VirtualDom { right.scope.set(Some(scope_id)); // copy out the box for both - let old = self.scopes[scope_id.0].props.as_ref(); + let old = self.scopes[scope_id].props.as_ref(); let new: Box = right.props.take().unwrap(); let new: Box = unsafe { std::mem::transmute(new) }; @@ -176,14 +221,14 @@ impl<'b> VirtualDom { } // First, move over the props from the old to the new, dropping old props in the process - self.scopes[scope_id.0].props = Some(new); + self.scopes[scope_id].props = Some(new); // Now run the component and diff it self.run_scope(scope_id); self.diff_scope(scope_id); self.dirty_scopes.remove(&DirtyScope { - height: self.scopes[scope_id.0].height, + height: self.scopes[scope_id].height, id: scope_id, }); } @@ -197,8 +242,12 @@ impl<'b> VirtualDom { ) { let m = self.create_component_node(right_template, right, idx); + let pre_edits = self.mutations.edits.len(); + self.remove_component_node(left, true); + assert!(self.mutations.edits.len() > pre_edits); + // We want to optimize the replace case to use one less mutation if possible // Since mutations are done in reverse, the last node removed will be the first in the stack // Instead of *just* removing it, we can use the replace mutation @@ -586,10 +635,12 @@ impl<'b> VirtualDom { } let id = self.find_last_element(&new[last]); - self.mutations.push(Mutation::InsertAfter { - id, - m: nodes_created, - }); + if nodes_created > 0 { + self.mutations.push(Mutation::InsertAfter { + id, + m: nodes_created, + }) + } nodes_created = 0; } @@ -610,10 +661,12 @@ impl<'b> VirtualDom { } let id = self.find_first_element(&new[last]); - self.mutations.push(Mutation::InsertBefore { - id, - m: nodes_created, - }); + if nodes_created > 0 { + self.mutations.push(Mutation::InsertBefore { + id, + m: nodes_created, + }); + } nodes_created = 0; } @@ -634,16 +687,19 @@ impl<'b> VirtualDom { } let id = self.find_first_element(&new[first_lis]); - self.mutations.push(Mutation::InsertBefore { - id, - m: nodes_created, - }); + if nodes_created > 0 { + self.mutations.push(Mutation::InsertBefore { + id, + m: nodes_created, + }); + } } } /// Push all the real nodes on the stack fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize { node.template + .get() .roots .iter() .enumerate() @@ -652,7 +708,7 @@ impl<'b> VirtualDom { Some(node) => node, None => { self.mutations.push(Mutation::PushRoot { - id: node.root_ids[idx].get().unwrap(), + id: node.root_ids.get(idx).unwrap(), }); return 1; } @@ -674,18 +730,19 @@ impl<'b> VirtualDom { Fragment(nodes) => nodes .iter() .map(|node| self.push_all_real_nodes(node)) - .count(), + .sum(), Component(comp) => { let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(node)) => self.push_all_real_nodes(node), + match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } { + RenderReturn::Ready(node) => self.push_all_real_nodes(node), + RenderReturn::Aborted(_node) => todo!(), _ => todo!(), } } } }) - .count() + .sum() } fn create_children(&mut self, nodes: impl IntoIterator>) -> usize { @@ -707,7 +764,11 @@ impl<'b> VirtualDom { } /// Simply replace a placeholder with a list of nodes - fn replace_placeholder(&mut self, l: &'b VPlaceholder, r: &'b [VNode<'b>]) { + fn replace_placeholder( + &mut self, + l: &'b VPlaceholder, + r: impl IntoIterator>, + ) { let m = self.create_children(r); let id = l.id.get().unwrap(); self.mutations.push(Mutation::ReplaceWith { id, m }); @@ -779,11 +840,11 @@ impl<'b> VirtualDom { } fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) { - for (idx, _) in node.template.roots.iter().enumerate() { + for (idx, _) in node.template.get().roots.iter().enumerate() { if let Some(dy) = node.dynamic_root(idx) { self.remove_dynamic_node(dy, gen_muts); } else { - let id = node.root_ids[idx].get().unwrap(); + let id = node.root_ids.get(idx).unwrap(); if gen_muts { self.mutations.push(Mutation::Remove { id }); } @@ -796,8 +857,19 @@ impl<'b> VirtualDom { let mut id = None; for (idx, attr) in node.dynamic_attrs.iter().enumerate() { // We'll clean up the root nodes either way, so don't worry - if node.template.attr_paths[idx].len() == 1 { - continue; + let path_len = node + .template + .get() + .attr_paths + .get(idx) + .map(|path| path.len()); + // if the path is 1 the attribute is in the root, so we don't need to clean it up + // if the path is 0, the attribute is a not attached at all, so we don't need to clean it up + + if let Some(len) = path_len { + if (..=1).contains(&len) { + continue; + } } let next_id = attr.mounted_element.get(); @@ -814,12 +886,16 @@ impl<'b> VirtualDom { fn remove_nested_dyn_nodes(&mut self, node: &VNode) { for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() { - // Roots are cleaned up automatically above - if node.template.node_paths[idx].len() == 1 { - continue; + let path_len = node + .template + .get() + .node_paths + .get(idx) + .map(|path| path.len()); + // Roots are cleaned up automatically above and nodes with a empty path are placeholders + if let Some(2..) = path_len { + self.remove_dynamic_node(dyn_node, false) } - - self.remove_dynamic_node(dyn_node, false); } } @@ -853,40 +929,37 @@ impl<'b> VirtualDom { } fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) { - let scope = comp.scope.take().unwrap(); + // Remove the component reference from the vcomponent so they're not tied together + let scope = comp + .scope + .take() + .expect("VComponents to always have a scope"); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => { - println!("Removing component node sync {:?}", gen_muts); - self.remove_node(t, gen_muts) - } - _ => todo!("cannot handle nonstandard nodes"), + // Remove the component from the dom + match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } { + RenderReturn::Ready(t) => self.remove_node(t, gen_muts), + RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts), + _ => todo!(), }; - let props = self.scopes[scope.0].props.take(); - - self.dirty_scopes.remove(&DirtyScope { - height: self.scopes[scope.0].height, - id: scope, - }); - + // Restore the props back to the vcomponent in case it gets rendered again + let props = self.scopes[scope].props.take(); *comp.props.borrow_mut() = unsafe { std::mem::transmute(props) }; - // make sure to wipe any of its props and listeners - self.ensure_drop_safety(scope); - self.scopes.remove(scope.0); + // Now drop all the resouces + self.drop_scope(scope, false); } fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { match node.dynamic_root(0) { - None => node.root_ids[0].get().unwrap(), + None => node.root_ids.get(0).unwrap(), Some(Text(t)) => t.id.get().unwrap(), Some(Fragment(t)) => self.find_first_element(&t[0]), Some(Placeholder(t)) => t.id.get().unwrap(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => self.find_first_element(t), + match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } { + RenderReturn::Ready(t) => self.find_first_element(t), _ => todo!("cannot handle nonstandard nodes"), } } @@ -894,15 +967,15 @@ impl<'b> VirtualDom { } fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId { - match node.dynamic_root(node.template.roots.len() - 1) { - None => node.root_ids.last().unwrap().get().unwrap(), + match node.dynamic_root(node.template.get().roots.len() - 1) { + None => node.root_ids.last().unwrap(), Some(Text(t)) => t.id.get().unwrap(), Some(Fragment(t)) => self.find_last_element(t.last().unwrap()), Some(Placeholder(t)) => t.id.get().unwrap(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => self.find_last_element(t), + match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } { + RenderReturn::Ready(t) => self.find_last_element(t), _ => todo!("cannot handle nonstandard nodes"), } } @@ -917,27 +990,30 @@ impl<'b> VirtualDom { /// We use the pointer of the dynamic_node list in this case fn templates_are_the_same<'b>(left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) -> bool { std::ptr::eq(left_template, right_template) - || std::ptr::eq(left_template.dynamic_nodes, right_template.dynamic_nodes) } fn templates_are_different(left_template: &VNode, right_template: &VNode) -> bool { - !std::ptr::eq(left_template.template.name, right_template.template.name) - && left_template.template.name != right_template.template.name + let left_template_name = left_template.template.get().name; + let right_template_name = right_template.template.get().name; + // we want to re-create the node if the template name is different by pointer even if the value is the same so that we can detect when hot reloading changes the template + !std::ptr::eq(left_template_name, right_template_name) } fn matching_components<'a>( left: &'a VNode<'a>, right: &'a VNode<'a>, ) -> Option, &'a VComponent<'a>)>> { - if left.template.roots.len() != right.template.roots.len() { + let left_template = left.template.get(); + let right_template = right.template.get(); + if left_template.roots.len() != right_template.roots.len() { return None; } // run through the components, ensuring they're the same - left.template + left_template .roots .iter() - .zip(right.template.roots.iter()) + .zip(right_template.roots.iter()) .map(|(l, r)| { let (l, r) = match (l, r) { (TemplateNode::Dynamic { id: l }, TemplateNode::Dynamic { id: r }) => (l, r), @@ -962,11 +1038,12 @@ fn matching_components<'a>( /// - for appending children we can use AppendChildren #[allow(dead_code)] fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool { - let path = node.template.node_paths[idx]; + let template = node.template.get(); + let path = template.node_paths[idx]; // use a loop to index every static node's children until the path has run out // only break if the last path index is a dynamic node - let mut static_node = &node.template.roots[path[0] as usize]; + let mut static_node = &template.roots[path[0] as usize]; for i in 1..path.len() - 1 { match static_node { diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index fd8810f1a..071fbe48b 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,19 +1,203 @@ -use std::cell::RefCell; - -use crate::ScopeId; +use crate::{ScopeId, ScopeState}; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + fmt::Debug, +}; /// A boundary that will capture any errors from child components -#[allow(dead_code)] pub struct ErrorBoundary { - error: RefCell>, - id: ScopeId, + error: RefCell>, + _id: ScopeId, +} + +/// An instance of an error captured by a descendant component. +pub struct CapturedError { + /// The error captured by the error boundary + pub error: Box, + + /// The scope that threw the error + pub scope: ScopeId, +} + +impl CapturedError { + /// Downcast the error type into a concrete error type + pub fn downcast(&self) -> Option<&T> { + if TypeId::of::() == self.error.type_id() { + let raw = self.error.as_ref() as *const _ as *const T; + Some(unsafe { &*raw }) + } else { + None + } + } } impl ErrorBoundary { pub fn new(id: ScopeId) -> Self { Self { error: RefCell::new(None), - id, + _id: id, + } + } + + /// Push an error into this Error Boundary + pub fn insert_error(&self, scope: ScopeId, error: Box) { + self.error.replace(Some(CapturedError { error, scope })); + } +} + +/// A trait to allow results to be thrown upwards to the nearest Error Boundary +/// +/// The canonical way of using this trait is to throw results from hooks, aborting rendering +/// through question mark synax. The throw method returns an option that evalutes to None +/// if there is an error, injecting the error to the nearest error boundary. +/// +/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. +/// +/// The call stack is saved for this component and provided to the error boundary +/// +/// ```rust, ignore +/// +/// #[inline_props] +/// fn app(cx: Scope, count: String) -> Element { +/// let id: i32 = count.parse().throw(cx)?; +/// +/// cx.render(rsx! { +/// div { "Count {}" } +/// }) +/// } +/// ``` +pub trait Throw: Sized { + /// The value that will be returned in if the given value is `Ok`. + type Out; + + /// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary. + /// + /// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. + /// + /// The call stack is saved for this component and provided to the error boundary + /// + /// + /// Note that you can also manually throw errors using the throw method on `ScopeState` directly, + /// which is what this trait shells out to. + /// + /// + /// ```rust, ignore + /// + /// #[inline_props] + /// fn app(cx: Scope, count: String) -> Element { + /// let id: i32 = count.parse().throw(cx)?; + /// + /// cx.render(rsx! { + /// div { "Count {}" } + /// }) + /// } + /// ``` + fn throw(self, cx: &ScopeState) -> Option; + + /// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary. + /// + /// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. + /// + /// The call stack is saved for this component and provided to the error boundary + /// + /// + /// Note that you can also manually throw errors using the throw method on `ScopeState` directly, + /// which is what this trait shells out to. + /// + /// + /// ```rust, ignore + /// + /// #[inline_props] + /// fn app(cx: Scope, count: String) -> Element { + /// let id: i32 = count.parse().throw(cx)?; + /// + /// cx.render(rsx! { + /// div { "Count {}" } + /// }) + /// } + /// ``` + fn throw_with( + self, + cx: &ScopeState, + e: impl FnOnce() -> D, + ) -> Option; +} + +/// We call clone on any errors that can be owned out of a reference +impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result { + type Out = &'a T; + + fn throw(self, cx: &ScopeState) -> Option { + match self { + Ok(t) => Some(t), + Err(e) => { + cx.throw(e.to_owned()); + None + } + } + } + + fn throw_with( + self, + cx: &ScopeState, + err: impl FnOnce() -> D, + ) -> Option { + match self { + Ok(t) => Some(t), + Err(_e) => { + cx.throw(err()); + None + } } } } + +/// Or just throw errors we know about +impl Throw for Result { + type Out = T; + + fn throw(self, cx: &ScopeState) -> Option { + match self { + Ok(t) => Some(t), + Err(e) => { + cx.throw(e); + None + } + } + } + + fn throw_with( + self, + cx: &ScopeState, + error: impl FnOnce() -> D, + ) -> Option { + self.ok().or_else(|| { + cx.throw(error()); + None + }) + } +} + +/// Or just throw errors we know about +impl Throw for Option { + type Out = T; + + fn throw(self, cx: &ScopeState) -> Option { + self.or_else(|| { + cx.throw("None error."); + None + }) + } + + fn throw_with( + self, + cx: &ScopeState, + error: impl FnOnce() -> D, + ) -> Option { + self.or_else(|| { + cx.throw(error()); + None + }) + } +} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 9ab01e4d9..a5b33f6b1 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,12 +100,14 @@ 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() } } +#[doc(hidden)] + /// The callback type generated by the `rsx!` macro when an `on` field is specified for components. /// /// This makes it possible to pass `move |evt| {}` style closures into components as property fields. diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index d36031556..a9e919bfa 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -31,8 +31,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { Some(VNode { key: children.key, parent: children.parent, - template: children.template, - root_ids: children.root_ids, + template: children.template.clone(), + root_ids: children.root_ids.clone(), dynamic_nodes: children.dynamic_nodes, dynamic_attrs: children.dynamic_attrs, }) diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 2b7c4feb3..811dbb734 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -11,6 +11,7 @@ //! The logic for this was borrowed from . Unfortunately, this crate does not //! support non-static closures, so we've implemented the core logic of `ValueA` in this module. +#[allow(unused_imports)] use smallbox::{smallbox, space::S16, SmallBox}; use crate::{innerlude::VNode, ScopeState}; diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index dcf7d7862..304197dab 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -70,10 +70,11 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - fc_to_builder, Attribute, AttributeValue, 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, VPlaceholder, VText, + VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types @@ -81,9 +82,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, - 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..27976c157 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,14 +61,14 @@ 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 { /// The ID of the element being mounted to id: ElementId, - /// The number of nodes on the stack + /// The number of nodes on the stack to append to the target element m: usize, }, @@ -155,7 +155,7 @@ pub enum Mutation<'a> { /// The ID of the node we're going to replace with id: ElementId, - /// The number of nodes on the stack to use to replace + /// The number of nodes on the stack to replace the target element with m: usize, }, @@ -167,7 +167,7 @@ pub enum Mutation<'a> { /// `[0,1,2]` represents 1st child's 2nd child's 3rd child. path: &'static [u8], - /// The number of nodes on the stack to use to replace + /// The number of nodes on the stack to replace the target element with m: usize, }, @@ -176,7 +176,7 @@ pub enum Mutation<'a> { /// The ID of the node to insert after. id: ElementId, - /// The ids of the nodes to insert after the target node. + /// The number of nodes on the stack to insert after the target node. m: usize, }, @@ -185,7 +185,7 @@ pub enum Mutation<'a> { /// The ID of the node to insert before. id: ElementId, - /// The ids of the nodes to insert before the target node. + /// The number of nodes on the stack to insert before the target node. m: usize, }, @@ -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 99b7499c9..da139e78a 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -5,8 +5,8 @@ use bumpalo::boxed::Box as BumpBox; use bumpalo::Bump; use std::{ any::{Any, TypeId}, - cell::{Cell, RefCell}, - fmt::Arguments, + cell::{Cell, RefCell, UnsafeCell}, + fmt::{Arguments, Debug}, future::Future, }; @@ -21,10 +21,22 @@ pub type TemplateId = &'static str; /// you might need to handle the case where there's no node immediately ready. pub enum RenderReturn<'a> { /// A currently-available element - Sync(Element<'a>), + Ready(VNode<'a>), + + /// The component aborted rendering early. It might've thrown an error. + /// + /// In its place we've produced a placeholder to locate its spot in the dom when + /// it recovers. + Aborted(VPlaceholder), /// An ongoing future that will resolve to a [`Element`] - Async(BumpBox<'a, dyn Future> + 'a>), + Pending(BumpBox<'a, dyn Future> + 'a>), +} + +impl<'a> Default for RenderReturn<'a> { + fn default() -> Self { + RenderReturn::Aborted(VPlaceholder::default()) + } } /// A reference to a template along with any context needed to hydrate it @@ -42,11 +54,11 @@ pub struct VNode<'a> { pub parent: Option, /// The static nodes and static descriptor of the template - pub template: Template<'static>, + pub template: Cell>, /// The IDs for the roots of this template - to be used when moving the template around and removing it from /// the actual Dom - pub root_ids: &'a [Cell>], + pub root_ids: BoxedCellSlice, /// The dynamic parts of the template pub dynamic_nodes: &'a [DynamicNode<'a>], @@ -55,21 +67,120 @@ pub struct VNode<'a> { pub dynamic_attrs: &'a [Attribute<'a>], } +// Saftey: There is no way to get references to the internal data of this struct so no refrences will be invalidated by mutating the data with a immutable reference (The same principle behind Cell) +#[derive(Debug, Default)] +pub struct BoxedCellSlice(UnsafeCell>>); + +impl Clone for BoxedCellSlice { + fn clone(&self) -> Self { + Self(UnsafeCell::new(unsafe { (*self.0.get()).clone() })) + } +} + +impl BoxedCellSlice { + pub fn last(&self) -> Option { + unsafe { + (*self.0.get()) + .as_ref() + .and_then(|inner| inner.as_ref().last().copied()) + } + } + + pub fn get(&self, idx: usize) -> Option { + unsafe { + (*self.0.get()) + .as_ref() + .and_then(|inner| inner.as_ref().get(idx).copied()) + } + } + + pub unsafe fn get_unchecked(&self, idx: usize) -> Option { + (*self.0.get()) + .as_ref() + .and_then(|inner| inner.as_ref().get(idx).copied()) + } + + pub fn set(&self, idx: usize, new: ElementId) { + unsafe { + if let Some(inner) = &mut *self.0.get() { + inner[idx] = new; + } + } + } + + pub fn intialize(&self, contents: Box<[ElementId]>) { + unsafe { + *self.0.get() = Some(contents); + } + } + + pub fn transfer(&self, other: &Self) { + unsafe { + *self.0.get() = (*other.0.get()).clone(); + } + } + + pub fn take_from(&self, other: &Self) { + unsafe { + *self.0.get() = (*other.0.get()).take(); + } + } + + pub fn len(&self) -> usize { + unsafe { + (*self.0.get()) + .as_ref() + .map(|inner| inner.len()) + .unwrap_or(0) + } + } +} + +impl<'a> IntoIterator for &'a BoxedCellSlice { + type Item = ElementId; + + type IntoIter = BoxedCellSliceIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + BoxedCellSliceIter { + index: 0, + borrow: self, + } + } +} + +pub struct BoxedCellSliceIter<'a> { + index: usize, + borrow: &'a BoxedCellSlice, +} + +impl Iterator for BoxedCellSliceIter<'_> { + type Item = ElementId; + + fn next(&mut self) -> Option { + let result = self.borrow.get(self.index); + if result.is_some() { + self.index += 1; + } + result + } +} + impl<'a> VNode<'a> { /// Create a template with no nodes that will be skipped over during diffing pub fn empty() -> Element<'a> { Some(VNode { key: None, parent: None, - root_ids: &[], + root_ids: BoxedCellSlice::default(), dynamic_nodes: &[], dynamic_attrs: &[], - template: Template { + template: Cell::new(Template { name: "dioxus-empty", roots: &[], node_paths: &[], attr_paths: &[], - }, + }), }) } @@ -77,21 +188,13 @@ impl<'a> VNode<'a> { /// /// Returns [`None`] if the root is actually a static node (Element/Text) pub fn dynamic_root(&self, idx: usize) -> Option<&'a DynamicNode<'a>> { - match &self.template.roots[idx] { + match &self.template.get().roots[idx] { TemplateNode::Element { .. } | TemplateNode::Text { text: _ } => None, TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { Some(&self.dynamic_nodes[*id]) } } } - - pub(crate) fn clear_listeners(&self) { - for attr in self.dynamic_attrs { - if let AttributeValue::Listener(l) = &attr.value { - l.borrow_mut().take(); - } - } - } } /// A static layout of a UI tree that describes a set of dynamic and static nodes. @@ -102,32 +205,85 @@ impl<'a> VNode<'a> { /// /// For this to work properly, the [`Template::name`] *must* be unique across your entire project. This can be done via variety of /// ways, with the suggested approach being the unique code location (file, line, col, etc). -#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] pub struct Template<'a> { /// The name of the template. This must be unique across your entire program for template diffing to work properly /// /// If two templates have the same name, it's likely that Dioxus will panic when diffing. + #[cfg_attr( + feature = "serialize", + serde(deserialize_with = "deserialize_string_leaky") + )] pub name: &'a str, /// The list of template nodes that make up the template /// /// Unlike react, calls to `rsx!` can have multiple roots. This list supports that paradigm. + #[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_leaky"))] pub roots: &'a [TemplateNode<'a>], /// The paths of each node relative to the root of the template. /// /// These will be one segment shorter than the path sent to the renderer since those paths are relative to the /// topmost element, not the `roots` field. + #[cfg_attr( + feature = "serialize", + serde(deserialize_with = "deserialize_bytes_leaky") + )] pub node_paths: &'a [&'a [u8]], /// The paths of each dynamic attribute relative to the root of the template /// /// These will be one segment shorter than the path sent to the renderer since those paths are relative to the /// topmost element, not the `roots` field. + #[cfg_attr( + feature = "serialize", + serde(deserialize_with = "deserialize_bytes_leaky") + )] pub attr_paths: &'a [&'a [u8]], } +#[cfg(feature = "serialize")] +fn deserialize_string_leaky<'a, 'de, D>(deserializer: D) -> Result<&'a str, D::Error> +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + + let deserialized = String::deserialize(deserializer)?; + Ok(&*Box::leak(deserialized.into_boxed_str())) +} + +#[cfg(feature = "serialize")] +fn deserialize_bytes_leaky<'a, 'de, D>(deserializer: D) -> Result<&'a [&'a [u8]], D::Error> +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + + let deserialized = Vec::>::deserialize(deserializer)?; + let deserialized = deserialized + .into_iter() + .map(|v| &*Box::leak(v.into_boxed_slice())) + .collect::>(); + Ok(&*Box::leak(deserialized.into_boxed_slice())) +} + +#[cfg(feature = "serialize")] +fn deserialize_leaky<'a, 'de, T: serde::Deserialize<'de>, D>( + deserializer: D, +) -> Result<&'a [T], D::Error> +where + T: serde::Deserialize<'de>, + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + + let deserialized = Box::<[T]>::deserialize(deserializer)?; + Ok(&*Box::leak(deserialized)) +} + impl<'a> Template<'a> { /// Is this template worth caching at all, since it's completely runtime? /// @@ -144,7 +300,11 @@ impl<'a> Template<'a> { /// /// This can be created at compile time, saving the VirtualDom time when diffing the tree #[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize), serde(tag = "type"))] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] pub enum TemplateNode<'a> { /// An statically known element in the dom. /// @@ -164,9 +324,11 @@ pub enum TemplateNode<'a> { /// A list of possibly dynamic attribues for this element /// /// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`. + #[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_leaky"))] attrs: &'a [TemplateAttribute<'a>], /// A list of template nodes that define another set of template nodes + #[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_leaky"))] children: &'a [TemplateNode<'a>], }, @@ -353,13 +515,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 { @@ -383,29 +640,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 } } @@ -416,7 +680,10 @@ pub trait ComponentReturn<'a, A = ()> { impl<'a> ComponentReturn<'a> for Element<'a> { fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> { - RenderReturn::Sync(self) + match self { + Some(node) => RenderReturn::Ready(node), + None => RenderReturn::default(), + } } } @@ -428,7 +695,7 @@ where { fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> { let f: &mut dyn Future> = cx.bump().alloc(self); - RenderReturn::Async(unsafe { BumpBox::from_raw(f) }) + RenderReturn::Pending(unsafe { BumpBox::from_raw(f) }) } } @@ -490,15 +757,21 @@ impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { } } -impl<'a> IntoDynNode<'_> for &'a str { - fn into_vnode(self, cx: &ScopeState) -> DynamicNode { - cx.text_node(format_args!("{}", self)) +impl<'a, 'b> IntoDynNode<'b> for &'a str { + fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { + DynamicNode::Text(VText { + value: bumpalo::collections::String::from_str_in(self, cx.bump()).into_bump_str(), + id: Default::default(), + }) } } impl IntoDynNode<'_> for String { fn into_vnode(self, cx: &ScopeState) -> DynamicNode { - cx.text_node(format_args!("{}", self)) + DynamicNode::Text(VText { + value: cx.bump().alloc(self), + id: Default::default(), + }) } } @@ -512,8 +785,8 @@ impl<'a> IntoDynNode<'a> for &'a VNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(_cx.bump().alloc([VNode { parent: self.parent, - template: self.template, - root_ids: self.root_ids, + template: self.template.clone(), + root_ids: self.root_ids.clone(), key: self.key, dynamic_nodes: self.dynamic_nodes, dynamic_attrs: self.dynamic_attrs, @@ -544,7 +817,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 @@ -569,26 +841,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; @@ -597,3 +879,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/mod.rs b/packages/core/src/scheduler/mod.rs index a6442184d..8fb476fbe 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -4,11 +4,9 @@ use slab::Slab; mod suspense; mod task; mod wait; -mod waker; pub use suspense::*; pub use task::*; -pub use waker::RcWake; /// The type of message that can be sent to the scheduler. /// @@ -31,10 +29,10 @@ pub(crate) struct Scheduler { pub sender: futures_channel::mpsc::UnboundedSender, /// Tasks created with cx.spawn - pub tasks: RefCell>>, + pub tasks: RefCell>, /// Async components - pub leaves: RefCell>>, + pub leaves: RefCell>, } impl Scheduler { diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index 8d7d7337e..39e404198 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -1,11 +1,14 @@ -use super::{waker::RcWake, SchedulerMsg}; +use futures_util::task::ArcWake; + +use super::SchedulerMsg; use crate::ElementId; use crate::{innerlude::Mutations, Element, ScopeId}; use std::future::Future; +use std::sync::Arc; +use std::task::Waker; use std::{ cell::{Cell, RefCell}, collections::HashSet, - rc::Rc, }; /// An ID representing an ongoing suspended component @@ -35,16 +38,19 @@ impl SuspenseContext { } pub(crate) struct SuspenseLeaf { - pub(crate) id: SuspenseId, pub(crate) scope_id: ScopeId, - pub(crate) tx: futures_channel::mpsc::UnboundedSender, pub(crate) notified: Cell, pub(crate) task: *mut dyn Future>, + pub(crate) waker: Waker, } -impl RcWake for SuspenseLeaf { - fn wake_by_ref(arc_self: &Rc) { - arc_self.notified.set(true); +pub struct SuspenseHandle { + pub(crate) id: SuspenseId, + pub(crate) tx: futures_channel::mpsc::UnboundedSender, +} + +impl ArcWake for SuspenseHandle { + fn wake_by_ref(arc_self: &Arc) { _ = arc_self .tx .unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id)); diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index fb4378104..fc668b364 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -1,8 +1,12 @@ -use super::{waker::RcWake, Scheduler, SchedulerMsg}; +use futures_util::task::ArcWake; + +use super::{Scheduler, SchedulerMsg}; use crate::ScopeId; use std::cell::RefCell; use std::future::Future; -use std::{pin::Pin, rc::Rc}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::Waker; /// A task's unique identifier. /// @@ -16,8 +20,7 @@ pub struct TaskId(pub usize); pub(crate) struct LocalTask { pub scope: ScopeId, pub(super) task: RefCell + 'static>>>, - id: TaskId, - tx: futures_channel::mpsc::UnboundedSender, + pub waker: Waker, } impl Scheduler { @@ -32,15 +35,20 @@ impl Scheduler { /// will only occur when the VirtuaalDom itself has been dropped. pub fn spawn(&self, scope: ScopeId, task: impl Future + 'static) -> TaskId { let mut tasks = self.tasks.borrow_mut(); + let entry = tasks.vacant_entry(); let task_id = TaskId(entry.key()); - entry.insert(Rc::new(LocalTask { - id: task_id, - tx: self.sender.clone(), + let task = LocalTask { task: RefCell::new(Box::pin(task)), scope, - })); + waker: futures_util::task::waker(Arc::new(LocalTaskHandle { + id: task_id, + tx: self.sender.clone(), + })), + }; + + entry.insert(task); self.sender .unbounded_send(SchedulerMsg::TaskNotified(task_id)) @@ -51,15 +59,21 @@ 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); } } -impl RcWake for LocalTask { - fn wake_by_ref(arc_self: &Rc) { - _ = arc_self +pub struct LocalTaskHandle { + id: TaskId, + tx: futures_channel::mpsc::UnboundedSender, +} + +impl ArcWake for LocalTaskHandle { + fn wake_by_ref(arc_self: &Arc) { + // This can fail if the scheduler has been dropped while the application is shutting down + let _ = arc_self .tx .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)); } diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 8dff17b01..77bea3a18 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -10,7 +10,7 @@ use crate::{ ScopeId, TaskId, VNode, VirtualDom, }; -use super::{waker::RcWake, SuspenseId}; +use super::SuspenseId; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -22,42 +22,37 @@ impl VirtualDom { let task = match tasks.get(id.0) { Some(task) => task, + // The task was removed from the scheduler, so we can just ignore it None => return, }; - let waker = task.waker(); - let mut cx = Context::from_waker(&waker); + let mut cx = Context::from_waker(&task.waker); // 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]; + scope.spawned_tasks.borrow_mut().remove(&id); // Remove it from the scheduler - tasks.remove(id.0); + tasks.try_remove(id.0); } } pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc { - self.scopes[id.0] + self.scopes[id] .consume_context::>() .unwrap() } pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) { - let leaf = self - .scheduler - .leaves - .borrow_mut() - .get(id.0) - .unwrap() - .clone(); + let leaves = self.scheduler.leaves.borrow_mut(); + let leaf = leaves.get(id.0).unwrap(); let scope_id = leaf.scope_id; // todo: cache the waker - let waker = leaf.waker(); - let mut cx = Context::from_waker(&waker); + let mut cx = Context::from_waker(&leaf.waker); // Safety: the future is always pinned to the bump arena let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; @@ -67,18 +62,21 @@ impl VirtualDom { // we should attach them to that component and then render its children // continue rendering the tree until we hit yet another suspended component if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { - // safety: we're not going to modify the suspense context but we don't want to make a clone of it let fiber = self.acquire_suspense_boundary(leaf.scope_id); - let scope = &mut self.scopes[scope_id.0]; + let scope = &self.scopes[scope_id]; let arena = scope.current_frame(); - let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes)); + let ret = arena.bump().alloc(match new_nodes { + Some(new) => RenderReturn::Ready(new), + None => RenderReturn::default(), + }); + arena.node.set(ret); fiber.waiting_on.borrow_mut().remove(&id); - if let RenderReturn::Sync(Some(template)) = ret { + if let RenderReturn::Ready(template) = ret { let mutations_ref = &mut fiber.mutations.borrow_mut(); let mutations = &mut **mutations_ref; let template: &VNode = unsafe { std::mem::transmute(template) }; @@ -88,6 +86,9 @@ impl VirtualDom { let place_holder_id = scope.placeholder.get().unwrap(); self.scope_stack.push(scope_id); + + drop(leaves); + let created = self.create(template); self.scope_stack.pop(); mutations.push(Mutation::ReplaceWith { diff --git a/packages/core/src/scheduler/waker.rs b/packages/core/src/scheduler/waker.rs deleted file mode 100644 index 762020167..000000000 --- a/packages/core/src/scheduler/waker.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::task::{RawWaker, RawWakerVTable, Waker}; -use std::{mem, rc::Rc}; - -pub trait RcWake: Sized { - /// Create a waker from this self-wakening object - fn waker(self: &Rc) -> Waker { - unsafe fn rc_vtable() -> &'static RawWakerVTable { - &RawWakerVTable::new( - |data| { - let arc = mem::ManuallyDrop::new(Rc::::from_raw(data.cast::())); - let _rc_clone: mem::ManuallyDrop<_> = arc.clone(); - RawWaker::new(data, rc_vtable::()) - }, - |data| Rc::from_raw(data.cast::()).wake(), - |data| { - let arc = mem::ManuallyDrop::new(Rc::::from_raw(data.cast::())); - RcWake::wake_by_ref(&arc); - }, - |data| drop(Rc::::from_raw(data.cast::())), - ) - } - - unsafe { - Waker::from_raw(RawWaker::new( - Rc::into_raw(self.clone()).cast(), - rc_vtable::(), - )) - } - } - - fn wake_by_ref(arc_self: &Rc); - - fn wake(self: Rc) { - Self::wake_by_ref(&self) - } -} diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 9c0f96db3..548329354 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -2,18 +2,16 @@ use crate::{ any_props::AnyProps, bump_frame::BumpFrame, innerlude::DirtyScope, - innerlude::{SuspenseId, SuspenseLeaf}, + innerlude::{SuspenseHandle, SuspenseId, SuspenseLeaf}, nodes::RenderReturn, - scheduler::RcWake, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; -use bumpalo::Bump; use futures_util::FutureExt; use std::{ mem, pin::Pin, - rc::Rc, + sync::Arc, task::{Context, Poll}, }; @@ -26,9 +24,9 @@ impl VirtualDom { let parent = self.acquire_current_scope_raw(); let entry = self.scopes.vacant_entry(); let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) }; - let id = ScopeId(entry.key()); + let id = entry.key(); - entry.insert(Box::new(ScopeState { + entry.insert(ScopeState { parent, id, height, @@ -45,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)?; + Some(scope) } pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { @@ -63,42 +60,37 @@ 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].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]; - // 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 let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref(); let props: &dyn AnyProps = mem::transmute(props); + props.render(scope).extend_lifetime() }; // immediately resolve futures that can be resolved - if let RenderReturn::Async(task) = &mut new_nodes { + if let RenderReturn::Pending(task) = &mut new_nodes { let mut leaves = self.scheduler.leaves.borrow_mut(); let entry = leaves.vacant_entry(); let suspense_id = SuspenseId(entry.key()); - let leaf = Rc::new(SuspenseLeaf { + let leaf = SuspenseLeaf { scope_id, task: task.as_mut(), - id: suspense_id, - tx: self.scheduler.sender.clone(), notified: Default::default(), - }); + waker: futures_util::task::waker(Arc::new(SuspenseHandle { + id: suspense_id, + tx: self.scheduler.sender.clone(), + })), + }; - let waker = leaf.waker(); - let mut cx = Context::from_waker(&waker); + let mut cx = Context::from_waker(&leaf.waker); // safety: the task is already pinned in the bump arena let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) }; @@ -108,7 +100,11 @@ impl VirtualDom { match pinned.poll_unpin(&mut cx) { // If nodes are produced, then set it and we can break Poll::Ready(nodes) => { - new_nodes = RenderReturn::Sync(nodes); + new_nodes = match nodes { + Some(nodes) => RenderReturn::Ready(nodes), + None => RenderReturn::default(), + }; + break; } @@ -131,13 +127,13 @@ impl VirtualDom { } }; - let scope = &self.scopes[scope_id.0]; + let scope = &self.scopes[scope_id]; // We write on top of the previous frame and then make it the current by pushing the generation forward 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 @@ -150,6 +146,6 @@ impl VirtualDom { }); // rebind the lifetime now that its stored internally - unsafe { mem::transmute(allocated) } + unsafe { allocated.extend_lifetime_ref() } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 6ceaa5eae..5601d57fe 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -4,18 +4,21 @@ use crate::{ arena::ElementId, bump_frame::BumpFrame, innerlude::{DynamicNode, EventHandler, VComponent, VText}, - innerlude::{Scheduler, SchedulerMsg}, + 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 bumpslab::{BumpSlab, Slot}; use rustc_hash::{FxHashMap, FxHashSet}; +use slab::{Slab, VacantEntry}; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, - fmt::Arguments, + fmt::{Arguments, Debug}, future::Future, + ops::{Index, IndexMut}, rc::Rc, sync::Arc, }; @@ -63,6 +66,95 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> { #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] pub struct ScopeId(pub usize); +/// A thin wrapper around a BumpSlab that uses ids to index into the slab. +pub(crate) struct ScopeSlab { + slab: BumpSlab, + // a slab of slots of stable pointers to the ScopeState in the bump slab + entries: Slab>, +} + +impl Drop for ScopeSlab { + fn drop(&mut self) { + // Bump slab doesn't drop its contents, so we need to do it manually + for slot in self.entries.drain() { + self.slab.remove(slot); + } + } +} + +impl Default for ScopeSlab { + fn default() -> Self { + Self { + slab: BumpSlab::new(), + entries: Slab::new(), + } + } +} + +impl ScopeSlab { + pub(crate) fn get(&self, id: ScopeId) -> Option<&ScopeState> { + self.entries.get(id.0).map(|slot| unsafe { &*slot.ptr() }) + } + + pub(crate) fn get_mut(&mut self, id: ScopeId) -> Option<&mut ScopeState> { + self.entries + .get(id.0) + .map(|slot| unsafe { &mut *slot.ptr_mut() }) + } + + pub(crate) fn vacant_entry(&mut self) -> ScopeSlabEntry { + let entry = self.entries.vacant_entry(); + ScopeSlabEntry { + slab: &mut self.slab, + entry, + } + } + + pub(crate) fn remove(&mut self, id: ScopeId) { + self.slab.remove(self.entries.remove(id.0)); + } + + pub(crate) fn contains(&self, id: ScopeId) -> bool { + self.entries.contains(id.0) + } + + pub(crate) fn iter(&self) -> impl Iterator { + self.entries.iter().map(|(_, slot)| unsafe { &*slot.ptr() }) + } +} + +pub(crate) struct ScopeSlabEntry<'a> { + slab: &'a mut BumpSlab, + entry: VacantEntry<'a, Slot<'static, ScopeState>>, +} + +impl<'a> ScopeSlabEntry<'a> { + pub(crate) fn key(&self) -> ScopeId { + ScopeId(self.entry.key()) + } + + pub(crate) fn insert(self, scope: ScopeState) -> &'a ScopeState { + let slot = self.slab.push(scope); + // this is safe because the slot is only ever accessed with the lifetime of the borrow of the slab + let slot = unsafe { std::mem::transmute(slot) }; + let entry = self.entry.insert(slot); + unsafe { &*entry.ptr() } + } +} + +impl Index for ScopeSlab { + type Output = ScopeState; + fn index(&self, id: ScopeId) -> &Self::Output { + self.get(id).unwrap() + } +} + +impl IndexMut for ScopeSlab { + fn index_mut(&mut self, id: ScopeId) -> &mut Self::Output { + self.get_mut(id).unwrap() + } +} + /// A component's state separate from its props. /// /// This struct exists to provide a common interface for all scopes without relying on generics. @@ -73,7 +165,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 +177,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 +203,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 +223,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 @@ -289,17 +373,12 @@ impl<'src> ScopeState { None } - /// Expose state to children further down the [`crate::VirtualDom`] Tree. Does not require `clone` on the context, - /// though we do recommend it. + /// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree. /// /// This is a "fundamental" operation and should only be called during initialization of a hook. /// /// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead. /// - /// If a state is provided that already exists, the new value will not be inserted. Instead, this method will - /// return the existing value. This behavior is chosen so shared values do not need to be `Clone`. This particular - /// behavior might change in the future. - /// /// # Example /// /// ```rust, ignore @@ -327,7 +406,9 @@ impl<'src> ScopeState { /// Pushes the future onto the poll queue to be polled after the component renders. pub fn push_future(&self, fut: impl Future + 'static) -> TaskId { - self.tasks.spawn(self.id, fut) + let id = self.tasks.spawn(self.id, fut); + self.spawned_tasks.borrow_mut().insert(id); + id } /// Spawns the future but does not return the [`TaskId`] @@ -348,6 +429,8 @@ impl<'src> ScopeState { .unbounded_send(SchedulerMsg::TaskNotified(id)) .expect("Scheduler should exist"); + self.spawned_tasks.borrow_mut().insert(id); + id } @@ -374,19 +457,25 @@ 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); + } + + _ => (), } } let mut props = self.borrowed_props.borrow_mut(); for node in element.dynamic_nodes { if let DynamicNode::Component(comp) = node { - let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) }; - props.push(unbounded); + if !comp.static_props { + let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) }; + props.push(unbounded); + } } } @@ -499,9 +588,9 @@ 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 +598,30 @@ 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 + /// itself will downcast the error into a trait object. + pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { + if let Some(cx) = self.consume_context::>() { + cx.insert_error(self.scope_id(), Box::new(error)); + } + + // Always return none during a throw + None + } + /// Store a value between renders. The foundational hook for all other hooks. /// /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`. @@ -528,7 +641,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 217768c50..2269815aa 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -5,7 +5,7 @@ use crate::{ any_props::VProps, arena::{ElementId, ElementRef}, - innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg}, + innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg, ScopeSlab}, mutations::Mutation, nodes::RenderReturn, nodes::{Template, TemplateId}, @@ -24,7 +24,9 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future /// /// Components are defined as simple functions that take [`Scope`] and return an [`Element`]. /// -/// ```rust, ignore +/// ```rust +/// # use dioxus::prelude::*; +/// /// #[derive(Props, PartialEq)] /// struct AppProps { /// title: String @@ -39,7 +41,17 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future /// /// Components may be composed to make complex apps. /// -/// ```rust, ignore +/// ```rust +/// # #![allow(unused)] +/// # use dioxus::prelude::*; +/// +/// # #[derive(Props, PartialEq)] +/// # struct AppProps { +/// # title: String +/// # } +/// +/// static ROUTES: &str = ""; +/// /// fn App(cx: Scope) -> Element { /// cx.render(rsx!( /// NavBar { routes: ROUTES } @@ -47,12 +59,33 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future /// Footer {} /// )) /// } +/// +/// #[inline_props] +/// fn NavBar(cx: Scope, routes: &'static str) -> Element { +/// cx.render(rsx! { +/// div { "Routes: {routes}" } +/// }) +/// } +/// +/// fn Footer(cx: Scope) -> Element { +/// cx.render(rsx! { div { "Footer" } }) +/// } +/// +/// #[inline_props] +/// fn Title<'a>(cx: Scope<'a>, children: Element<'a>) -> Element { +/// cx.render(rsx! { +/// div { id: "title", children } +/// }) +/// } /// ``` /// /// To start an app, create a [`VirtualDom`] and call [`VirtualDom::rebuild`] to get the list of edits required to /// draw the UI. /// -/// ```rust, ignore +/// ```rust +/// # use dioxus::prelude::*; +/// # fn App(cx: Scope) -> Element { cx.render(rsx! { div {} }) } +/// /// let mut vdom = VirtualDom::new(App); /// let edits = vdom.rebuild(); /// ``` @@ -142,8 +175,9 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future /// } /// ``` pub struct VirtualDom { - pub(crate) templates: FxHashMap>, - pub(crate) scopes: Slab>, + // Maps a template path to a map of byteindexes to templates + pub(crate) templates: FxHashMap>>, + pub(crate) scopes: ScopeSlab, pub(crate) dirty_scopes: BTreeSet, pub(crate) scheduler: Rc, @@ -224,7 +258,7 @@ impl VirtualDom { rx, scheduler: Scheduler::new(tx), templates: Default::default(), - scopes: Slab::default(), + scopes: Default::default(), elements: Default::default(), scope_stack: Vec::new(), dirty_scopes: BTreeSet::new(), @@ -248,7 +282,7 @@ impl VirtualDom { root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0)))); // the root element is always given element ID 0 since it's the container for the entire tree - dom.elements.insert(ElementRef::null()); + dom.elements.insert(ElementRef::none()); dom } @@ -257,14 +291,14 @@ impl VirtualDom { /// /// This is useful for inserting or removing contexts from a scope, or rendering out its root node pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { - self.scopes.get(id.0).map(|f| f.as_ref()) + self.scopes.get(id) } /// Get the single scope at the top of the VirtualDom tree that will always be around /// /// This scope has a ScopeId of 0 and is the root of the tree pub fn base_scope(&self) -> &ScopeState { - self.scopes.get(0).unwrap() + self.scopes.get(ScopeId(0)).unwrap() } /// Build the virtualdom with a global context inserted into the base scope @@ -279,7 +313,7 @@ impl VirtualDom { /// /// Whenever the VirtualDom "works", it will re-render this scope pub fn mark_dirty(&mut self, id: ScopeId) { - if let Some(scope) = self.scopes.get(id.0) { + if let Some(scope) = self.scopes.get(id) { let height = scope.height; self.dirty_scopes.insert(DirtyScope { height, id }); } @@ -290,7 +324,7 @@ impl VirtualDom { /// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is /// currently suspended. pub fn is_scope_suspended(&self, id: ScopeId) -> bool { - !self.scopes[id.0] + !self.scopes[id] .consume_context::>() .unwrap() .waiting_on @@ -346,22 +380,24 @@ 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, }; // Loop through each dynamic attribute in this template before moving up to the template's parent. while let Some(el_ref) = parent_path { // safety: we maintain references of all vnodes in the element slab - let template = unsafe { &*el_ref.template }; + let template = unsafe { el_ref.template.unwrap().as_ref() }; + let node_template = template.template.get(); let target_path = el_ref.path; for (idx, attr) in template.dynamic_attrs.iter().enumerate() { - let this_path = template.template.attr_paths[idx]; + let this_path = node_template.attr_paths[idx]; - // listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing - // we should fix this so that we look for "onclick" instead of "click" - if &attr.name[2..] == name && target_path.is_ascendant(&this_path) { + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + if attr.name.trim_start_matches("on") == name + && target_path.is_decendant(&this_path) + { listeners.push(&attr.value); // Break if the event doesn't bubble anyways @@ -386,7 +422,7 @@ impl VirtualDom { cb(uievent.clone()); } - if !uievent.propogates.get() { + if !uievent.propagates.get() { return; } } @@ -454,6 +490,30 @@ impl VirtualDom { } } + /// Replace a template at runtime. This will re-render all components that use this template. + /// This is the primitive that enables hot-reloading. + /// + /// The caller must ensure that the template refrences the same dynamic attributes and nodes as the original template. + /// + /// This will only replace the the parent template, not any nested templates. + pub fn replace_template(&mut self, template: Template<'static>) { + self.register_template_first_byte_index(template); + // iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine + for scope in self.scopes.iter() { + if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() { + if sync.template.get().name.rsplit_once(':').unwrap().0 + == template.name.rsplit_once(':').unwrap().0 + { + let height = scope.height; + self.dirty_scopes.insert(DirtyScope { + height, + id: scope.id, + }); + } + } + } + } + /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch. /// /// The mutations item expects the RealDom's stack to be the root of the application. @@ -477,7 +537,7 @@ impl VirtualDom { pub fn rebuild(&mut self) -> Mutations { match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root - RenderReturn::Sync(Some(node)) => { + RenderReturn::Ready(node) => { let m = self.create_scope(ScopeId(0), node); self.mutations.edits.push(Mutation::AppendChildren { id: ElementId(0), @@ -485,8 +545,8 @@ impl VirtualDom { }); } // If an error occurs, we should try to render the default error component and context where the error occured - RenderReturn::Sync(None) => panic!("Cannot catch errors during rebuild"), - RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"), + RenderReturn::Aborted(_placeholder) => panic!("Cannot catch errors during rebuild"), + RenderReturn::Pending(_) => unreachable!("Root scope cannot be an async component"), } self.finalize() @@ -523,7 +583,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]; let context = scope.has_context::>().unwrap(); self.mutations @@ -547,7 +607,7 @@ impl VirtualDom { self.dirty_scopes.remove(&dirty); // If the scope doesn't exist for whatever reason, then we should skip it - if !self.scopes.contains(dirty.id.0) { + if !self.scopes.contains(dirty.id) { continue; } @@ -566,7 +626,7 @@ impl VirtualDom { // If suspended leaves are present, then we should find the boundary for this scope and attach things // No placeholder necessary since this is a diff if !self.collected_leaves.is_empty() { - let mut boundary = self.scopes[dirty.id.0] + let mut boundary = self.scopes[dirty.id] .consume_context::>() .unwrap(); @@ -615,16 +675,13 @@ 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) } } impl Drop for VirtualDom { fn drop(&mut self) { // Simply drop this scope which drops all of its children - self.drop_scope(ScopeId(0)); + self.drop_scope(ScopeId(0), true); } } diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs index 8889f77c1..683f02260 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::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/error_boundary.rs b/packages/core/tests/error_boundary.rs new file mode 100644 index 000000000..ce26fba31 --- /dev/null +++ b/packages/core/tests/error_boundary.rs @@ -0,0 +1,34 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; + +#[test] +fn catches_panic() { + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Title" } + + NoneChild {} + ThrowChild {} + } + }) +} + +fn NoneChild(_cx: Scope) -> Element { + None +} + +fn ThrowChild(cx: Scope) -> Element { + cx.throw(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?; + + let _g: i32 = "123123".parse().throw(cx)?; + + cx.render(rsx! { + div {} + }) +} diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs new file mode 100644 index 000000000..75dd84c33 --- /dev/null +++ b/packages/core/tests/fuzzing.rs @@ -0,0 +1,305 @@ +use dioxus::prelude::Props; +use dioxus_core::*; +use std::cell::Cell; + +fn random_ns() -> Option<&'static str> { + let namespace = rand::random::() % 2; + match namespace { + 0 => None, + 1 => Some(Box::leak( + format!("ns{}", rand::random::()).into_boxed_str(), + )), + _ => unreachable!(), + } +} + +fn create_random_attribute(attr_idx: &mut usize) -> TemplateAttribute<'static> { + match rand::random::() % 2 { + 0 => TemplateAttribute::Static { + name: Box::leak(format!("attr{}", rand::random::()).into_boxed_str()), + value: Box::leak(format!("value{}", rand::random::()).into_boxed_str()), + namespace: random_ns(), + }, + 1 => TemplateAttribute::Dynamic { + id: { + let old_idx = *attr_idx; + *attr_idx += 1; + old_idx + }, + }, + _ => unreachable!(), + } +} + +fn create_random_template_node( + dynamic_node_types: &mut Vec, + template_idx: &mut usize, + attr_idx: &mut usize, + depth: usize, +) -> TemplateNode<'static> { + match rand::random::() % 4 { + 0 => { + let attrs = { + let attrs: Vec<_> = (0..(rand::random::() % 10)) + .map(|_| create_random_attribute(attr_idx)) + .collect(); + Box::leak(attrs.into_boxed_slice()) + }; + TemplateNode::Element { + tag: Box::leak(format!("tag{}", rand::random::()).into_boxed_str()), + namespace: random_ns(), + attrs, + children: { + if depth > 4 { + &[] + } else { + let children: Vec<_> = (0..(rand::random::() % 3)) + .map(|_| { + create_random_template_node( + dynamic_node_types, + template_idx, + attr_idx, + depth + 1, + ) + }) + .collect(); + Box::leak(children.into_boxed_slice()) + } + }, + } + } + 1 => TemplateNode::Text { + text: Box::leak(format!("{}", rand::random::()).into_boxed_str()), + }, + 2 => TemplateNode::DynamicText { + id: { + let old_idx = *template_idx; + *template_idx += 1; + dynamic_node_types.push(DynamicNodeType::Text); + old_idx + }, + }, + 3 => TemplateNode::Dynamic { + id: { + let old_idx = *template_idx; + *template_idx += 1; + dynamic_node_types.push(DynamicNodeType::Other); + old_idx + }, + }, + _ => unreachable!(), + } +} + +fn generate_paths( + node: &TemplateNode<'static>, + current_path: &[u8], + node_paths: &mut Vec>, + attr_paths: &mut Vec>, +) { + match node { + TemplateNode::Element { children, attrs, .. } => { + for attr in *attrs { + match attr { + TemplateAttribute::Static { .. } => {} + TemplateAttribute::Dynamic { .. } => { + attr_paths.push(current_path.to_vec()); + } + } + } + for (i, child) in children.iter().enumerate() { + let mut current_path = current_path.to_vec(); + current_path.push(i as u8); + generate_paths(child, ¤t_path, node_paths, attr_paths); + } + } + TemplateNode::Text { .. } => {} + TemplateNode::DynamicText { .. } => { + node_paths.push(current_path.to_vec()); + } + TemplateNode::Dynamic { .. } => { + node_paths.push(current_path.to_vec()); + } + } +} + +enum DynamicNodeType { + Text, + Other, +} + +fn create_random_template(name: &'static str) -> (Template<'static>, Vec) { + let mut dynamic_node_type = Vec::new(); + let mut template_idx = 0; + let mut attr_idx = 0; + let roots = (0..(1 + rand::random::() % 5)) + .map(|_| { + create_random_template_node(&mut dynamic_node_type, &mut template_idx, &mut attr_idx, 0) + }) + .collect::>(); + assert!(!roots.is_empty()); + let roots = Box::leak(roots.into_boxed_slice()); + let mut node_paths = Vec::new(); + let mut attr_paths = Vec::new(); + for (i, root) in roots.iter().enumerate() { + generate_paths(root, &[i as u8], &mut node_paths, &mut attr_paths); + } + let node_paths = Box::leak( + node_paths + .into_iter() + .map(|v| &*Box::leak(v.into_boxed_slice())) + .collect::>() + .into_boxed_slice(), + ); + let attr_paths = Box::leak( + attr_paths + .into_iter() + .map(|v| &*Box::leak(v.into_boxed_slice())) + .collect::>() + .into_boxed_slice(), + ); + ( + Template { name, roots, node_paths, attr_paths }, + dynamic_node_type, + ) +} + +fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { + let range = if depth > 3 { 1 } else { 3 }; + match rand::random::() % range { + 0 => DynamicNode::Placeholder(Default::default()), + 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| VNode { + key: None, + parent: Default::default(), + template: Cell::new(Template { + name: concat!(file!(), ":", line!(), ":", column!(), ":0"), + roots: &[TemplateNode::Dynamic { id: 0 }], + node_paths: &[&[0]], + attr_paths: &[], + }), + root_ids: Default::default(), + dynamic_nodes: cx.bump().alloc([cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + )]), + dynamic_attrs: &[], + })), + 2 => cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + ), + _ => unreachable!(), + } +} + +fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { + let value = match rand::random::() % 6 { + 0 => AttributeValue::Text(Box::leak( + format!("{}", rand::random::()).into_boxed_str(), + )), + 1 => AttributeValue::Float(rand::random()), + 2 => AttributeValue::Int(rand::random()), + 3 => AttributeValue::Bool(rand::random()), + 4 => cx.any_value(rand::random::()), + 5 => AttributeValue::None, + // Listener(RefCell>>), + _ => unreachable!(), + }; + Attribute { + name: Box::leak(format!("attr{}", rand::random::()).into_boxed_str()), + value, + namespace: random_ns(), + mounted_element: Default::default(), + volatile: rand::random(), + } +} + +static mut TEMPLATE_COUNT: usize = 0; + +#[derive(PartialEq, Props)] +struct DepthProps { + depth: usize, + root: bool, +} + +fn create_random_element(cx: Scope) -> Element { + cx.needs_update(); + let range = if cx.props.root { 2 } else { 3 }; + let node = match rand::random::() % range { + 0 | 1 => { + let (template, dynamic_node_types) = create_random_template(Box::leak( + format!( + "{}{}", + concat!(file!(), ":", line!(), ":", column!(), ":"), + { + unsafe { + let old = TEMPLATE_COUNT; + TEMPLATE_COUNT += 1; + old + } + } + ) + .into_boxed_str(), + )); + println!("{template:#?}"); + let node = VNode { + key: None, + parent: None, + template: Cell::new(template), + root_ids: Default::default(), + dynamic_nodes: { + let dynamic_nodes: Vec<_> = dynamic_node_types + .iter() + .map(|ty| match ty { + DynamicNodeType::Text => DynamicNode::Text(VText { + value: Box::leak( + format!("{}", rand::random::()).into_boxed_str(), + ), + id: Default::default(), + }), + DynamicNodeType::Other => { + create_random_dynamic_node(cx, cx.props.depth + 1) + } + }) + .collect(); + cx.bump().alloc(dynamic_nodes) + }, + dynamic_attrs: cx.bump().alloc( + (0..template.attr_paths.len()) + .map(|_| create_random_dynamic_attr(cx)) + .collect::>(), + ), + }; + Some(node) + } + _ => None, + }; + println!("{node:#?}"); + node +} + +// test for panics when creating random nodes and templates +#[test] +fn create() { + for _ in 0..100 { + let mut vdom = + VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); + let _ = vdom.rebuild(); + } +} + +// test for panics when diffing random nodes +// This test will change the template every render which is not very realistic, but it helps stress the system +#[test] +fn diff() { + for _ in 0..10 { + let mut vdom = + VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); + let _ = vdom.rebuild(); + for _ in 0..10 { + let _ = vdom.render_immediate(); + } + } +} 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/miri_stress.rs b/packages/core/tests/miri_stress.rs index 160baa25d..15b7ebf41 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -137,213 +137,78 @@ fn free_works_on_root_hooks() { assert_eq!(Rc::strong_count(&ptr), 1); } -// #[test] -// fn old_props_arent_stale() { -// fn app(cx: Scope) -> Element { -// dbg!("rendering parent"); -// let cnt = cx.use_hook(|| 0); -// *cnt += 1; +#[test] +fn supports_async() { + use std::time::Duration; + use tokio::time::sleep; -// if *cnt == 1 { -// render!(div { Child { a: "abcdef".to_string() } }) -// } else { -// render!(div { Child { a: "abcdef".to_string() } }) -// } -// } + fn app(cx: Scope) -> Element { + let colors = use_state(cx, || vec!["green", "blue", "red"]); + let padding = use_state(cx, || 10); -// #[derive(Props, PartialEq)] -// struct ChildProps { -// a: String, -// } -// fn Child(cx: Scope) -> Element { -// dbg!("rendering child", &cx.props.a); -// render!(div { "child {cx.props.a}" }) -// } + use_effect(cx, colors, |colors| async move { + sleep(Duration::from_millis(1000)).await; + colors.with_mut(|colors| colors.reverse()); + }); -// let mut dom = new_dom(app, ()); -// let _ = dom.rebuild(); + use_effect(cx, padding, |padding| async move { + sleep(Duration::from_millis(10)).await; + padding.with_mut(|padding| { + if *padding < 65 { + *padding += 1; + } else { + *padding = 5; + } + }); + }); -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); -// dom.work_with_deadline(|| false); + let big = colors[0]; + let mid = colors[1]; + let small = colors[2]; -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); -// dom.work_with_deadline(|| false); + cx.render(rsx! { + div { + background: "{big}", + height: "stretch", + width: "stretch", + padding: "50", + label { + "hello", + } + div { + background: "{mid}", + height: "auto", + width: "stretch", + padding: "{padding}", + label { + "World", + } + div { + background: "{small}", + height: "auto", + width: "stretch", + padding: "20", + label { + "ddddddd", + } + } + }, + } + }) + } -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); -// dom.work_with_deadline(|| false); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap(); -// dbg!("forcing update to child"); + rt.block_on(async { + let mut dom = VirtualDom::new(app); + let _ = dom.rebuild(); -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); -// dom.work_with_deadline(|| false); - -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); -// dom.work_with_deadline(|| false); - -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); -// dom.work_with_deadline(|| false); -// } - -// #[test] -// fn basic() { -// fn app(cx: Scope) -> Element { -// render!(div { -// Child { a: "abcdef".to_string() } -// }) -// } - -// #[derive(Props, PartialEq)] -// struct ChildProps { -// a: String, -// } - -// fn Child(cx: Scope) -> Element { -// dbg!("rendering child", &cx.props.a); -// render!(div { "child {cx.props.a}" }) -// } - -// let mut dom = new_dom(app, ()); -// let _ = dom.rebuild(); - -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); -// dom.work_with_deadline(|| false); - -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); -// dom.work_with_deadline(|| false); -// } - -// #[test] -// fn leak_thru_children() { -// fn app(cx: Scope) -> Element { -// cx.render(rsx! { -// Child { -// name: "asd".to_string(), -// } -// }); -// cx.render(rsx! { -// div {} -// }) -// } - -// #[inline_props] -// fn Child(cx: Scope, name: String) -> Element { -// render!(div { "child {name}" }) -// } - -// let mut dom = new_dom(app, ()); -// let _ = dom.rebuild(); - -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); -// dom.work_with_deadline(|| false); - -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); -// dom.work_with_deadline(|| false); -// } - -// #[test] -// fn test_pass_thru() { -// #[inline_props] -// fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element { -// cx.render(rsx! { -// header { -// nav { children } -// } -// }) -// } - -// fn NavMenu(cx: Scope) -> Element { -// render!( NavBrand {} -// div { -// NavStart {} -// NavEnd {} -// } -// ) -// } - -// fn NavBrand(cx: Scope) -> Element { -// render!(div {}) -// } - -// fn NavStart(cx: Scope) -> Element { -// render!(div {}) -// } - -// fn NavEnd(cx: Scope) -> Element { -// render!(div {}) -// } - -// #[inline_props] -// fn MainContainer<'a>( -// cx: Scope, -// nav: Element<'a>, -// body: Element<'a>, -// footer: Element<'a>, -// ) -> Element { -// cx.render(rsx! { -// div { -// class: "columns is-mobile", -// div { -// class: "column is-full", -// nav, -// body, -// footer, -// } -// } -// }) -// } - -// fn app(cx: Scope) -> Element { -// let nav = cx.render(rsx! { -// NavContainer { -// NavMenu {} -// } -// }); -// let body = cx.render(rsx! { -// div {} -// }); -// let footer = cx.render(rsx! { -// div {} -// }); - -// cx.render(rsx! { -// MainContainer { -// nav: nav, -// body: body, -// footer: footer, -// } -// }) -// } - -// let mut dom = new_dom(app, ()); -// let _ = dom.rebuild(); - -// for _ in 0..40 { -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); -// dom.work_with_deadline(|| false); -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); -// dom.work_with_deadline(|| false); -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); -// dom.work_with_deadline(|| false); - -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); -// dom.work_with_deadline(|| false); -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); -// dom.work_with_deadline(|| false); -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); -// dom.work_with_deadline(|| false); - -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); -// dom.work_with_deadline(|| false); -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); -// dom.work_with_deadline(|| false); -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); -// dom.work_with_deadline(|| false); - -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); -// dom.work_with_deadline(|| false); -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); -// dom.work_with_deadline(|| false); -// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); -// dom.work_with_deadline(|| false); -// } -// } + for _ in 0..10 { + dom.wait_for_work().await; + let _edits = dom.render_immediate(); + } + }); +} 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/ARCHITECTURE.md b/packages/desktop/ARCHITECTURE.md deleted file mode 100644 index 5a937dbca..000000000 --- a/packages/desktop/ARCHITECTURE.md +++ /dev/null @@ -1,14 +0,0 @@ -# dioxus webview arch - - -dioxus webview represents a usecase of dioxus to stream edits from one dom to another. In this particular case, we stream edits from a native process into the webview's runtime. WebView is really portable, so we can leverage the UI of iOS, Android, and all desktop platforms while also maintaining direct access to platform-specific APIs. It's the best of both worlds! - - -For this to work properly, we need to ship a universal client that receives Dioxus edits from a remote process. To make our lives (as developers) a bit easier, the client code is currently implemented in JS (subject to change!). - -The provider (the thing that talks to the client on the other side of the network) is still written in Rust, and is supplied through the dioxus-webview crate. The client is much more "dumb" than the provider - the provider must handle NodeID generation, flushing changes, priorities, managing events, and more. - - -## update; wry! - -Wry is an effort by the Tauri team to make fully native diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 4aaae6c64..5722a42a2 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" @@ -8,19 +8,20 @@ license = "MIT/Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" -keywords = ["dom", "ui", "gui", "react", "wasm"] +keywords = ["dom", "ui", "gui", "react"] # 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" } +dioxus-hot-reload = { path = "../hot-reload", optional = true } serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" log = "0.4.14" -wry = { version = "0.23.4" } +wry = { version = "0.27.2" } futures-channel = "0.3.21" tokio = { version = "1.16.1", features = [ "sync", @@ -32,8 +33,8 @@ tokio = { version = "1.16.1", features = [ webbrowser = "0.8.0" infer = "0.11.0" dunce = "1.0.2" +slab = "0.4" -interprocess = { version = "1.1.1", optional = true} futures-util = "0.3.25" [target.'cfg(target_os = "ios")'.dependencies] @@ -49,7 +50,7 @@ tokio_runtime = ["tokio"] fullscreen = ["wry/fullscreen"] transparent = ["wry/transparent"] tray = ["wry/tray"] -hot-reload = ["interprocess"] +hot-reload = ["dioxus-hot-reload"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } diff --git a/packages/desktop/README.md b/packages/desktop/README.md index df0af040b..964f90495 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -1,5 +1,57 @@ -# Dioxus-Desktop +# Dioxus Desktop (webview) -Because the host VirtualDOM is running in its own native process, native applications can unlock their full potential. Dioxus-Desktop is designed to be a 100% rust alternative to ElectronJS without the memory overhead or bloat of ElectronJS apps. -By bridging the native process, desktop apps can access full multithreading power, peripheral support, hardware access, and native filesystem controls without the hassle of web technologies. Our goal with this desktop crate is to make it easy to ship both a web and native application, and quickly see large performance boosts without having to re-write the whole stack. As the dioxus ecosystem grows, we hope to see 3rd parties providing wrappers for storage, offline mode, etc that supports both web and native technologies. +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-desktop.svg +[crates-url]: https://crates.io/crates/dioxus-desktop + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) | +[API Docs](https://docs.rs/dioxus-desktop/latest/dioxus_desktop) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +`dioxus-desktop` provides a webview-based desktop renderer for the Dioxus VirtualDom. + +This requires that webview is installed on the target system. WebView is installed by default on macOS and iOS devices, but might not come preinstalled on Windows or Linux devices. To fix these issues, follow the [instructions in the guide](guide-url). + +[guide-url]: https://dioxuslabs.com/docs/0.3/guide/en/getting_started/desktop.html + + + +## Features + +- Simple, one-line launch for desktop apps +- Dioxus VirtualDom running on a native thread +- Full HTML/CSS support via `wry` and `tao` +- Exposed `window` and `Proxy` types from tao for direct window manipulation +- Helpful hooks for accessing the window, WebView, and running javascript. + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT without any additional +terms or conditions. diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 4456eb2d7..d7f1b5dfa 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::path::PathBuf; use wry::application::window::Icon; @@ -16,9 +17,9 @@ pub struct Config { pub(crate) file_drop_handler: Option, pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, - // pub(crate) event_handler: Option>, pub(crate) disable_context_menu: bool, pub(crate) resource_dir: Option, + pub(crate) data_dir: Option, pub(crate) custom_head: Option, pub(crate) custom_index: Option, pub(crate) root_name: String, @@ -28,7 +29,7 @@ type DropHandler = Box bool>; pub(crate) type WryProtocol = ( String, - Box>) -> WryResult>> + 'static>, + Box>) -> WryResult>> + 'static>, ); impl Config { @@ -45,6 +46,7 @@ impl Config { pre_rendered: None, disable_context_menu: !cfg!(debug_assertions), resource_dir: None, + data_dir: None, custom_head: None, custom_index: None, root_name: "main".to_string(), @@ -57,6 +59,14 @@ impl Config { self } + /// set the directory where data will be stored in release mode. + /// + /// > Note: This **must** be set when bundling on Windows. + pub fn with_data_directory(mut self, path: impl Into) -> Self { + self.data_dir = Some(path.into()); + self + } + /// Set whether or not the right-click context menu should be disabled. pub fn with_disable_context_menu(mut self, disable: bool) -> Self { self.disable_context_menu = disable; @@ -77,15 +87,6 @@ impl Config { self } - // /// Set a custom event handler - // pub fn with_event_handler( - // mut self, - // handler: impl Fn(&mut EventLoop<()>, &mut WebView) + 'static, - // ) -> Self { - // self.event_handler = Some(Box::new(handler)); - // self - // } - /// Set a file drop handler pub fn with_file_drop_handler( mut self, @@ -98,7 +99,7 @@ impl Config { /// Set a custom protocol pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self where - F: Fn(&HttpRequest>) -> WryResult>> + 'static, + F: Fn(&HttpRequest>) -> WryResult>> + 'static, { self.protocols.push((name, Box::new(handler))); self diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs deleted file mode 100644 index c732f5a8b..000000000 --- a/packages/desktop/src/controller.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::desktop_context::{DesktopContext, UserWindowEvent}; -use dioxus_core::*; -use dioxus_html::HtmlEvent; -use futures_channel::mpsc::{unbounded, UnboundedSender}; -use futures_util::StreamExt; -#[cfg(target_os = "ios")] -use objc::runtime::Object; -use serde_json::Value; -use std::{ - collections::HashMap, - sync::Arc, - sync::{atomic::AtomicBool, Mutex}, - time::Duration, -}; -use wry::{ - self, - application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId}, - webview::WebView, -}; - -pub(super) struct DesktopController { - pub(super) webviews: HashMap, - pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, - pub(super) pending_edits: Arc>>, - pub(super) quit_app_on_close: bool, - pub(super) is_ready: Arc, - pub(super) proxy: EventLoopProxy, - pub(super) event_tx: UnboundedSender, - - #[cfg(target_os = "ios")] - pub(super) views: Vec<*mut Object>, -} - -impl DesktopController { - // Launch the virtualdom on its own thread managed by tokio - // returns the desktop state - pub(super) fn new_on_tokio( - root: Component

, - props: P, - proxy: EventLoopProxy, - ) -> Self { - let edit_queue = Arc::new(Mutex::new(Vec::new())); - let (event_tx, mut event_rx) = unbounded(); - let proxy2 = proxy.clone(); - - let pending_edits = edit_queue.clone(); - let desktop_context_proxy = proxy.clone(); - let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::(); - - std::thread::spawn(move || { - // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads - // I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it - - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - - runtime.block_on(async move { - let mut dom = VirtualDom::new_with_props(root, props) - .with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever)); - { - let edits = dom.rebuild(); - let mut queue = edit_queue.lock().unwrap(); - queue.push(serde_json::to_string(&edits).unwrap()); - proxy.send_event(UserWindowEvent::EditsReady).unwrap(); - } - - loop { - tokio::select! { - _ = dom.wait_for_work() => {} - Some(json_value) = event_rx.next() => { - if let Ok(value) = serde_json::from_value::(json_value) { - let HtmlEvent { - name, - element, - bubbles, - data - } = value; - dom.handle_event(&name, data.into_any(), element, bubbles); - } - } - } - - let muts = dom - .render_with_deadline(tokio::time::sleep(Duration::from_millis(16))) - .await; - - edit_queue - .lock() - .unwrap() - .push(serde_json::to_string(&muts).unwrap()); - let _ = proxy.send_event(UserWindowEvent::EditsReady); - } - }) - }); - - Self { - pending_edits, - eval_sender, - webviews: HashMap::new(), - is_ready: Arc::new(AtomicBool::new(false)), - quit_app_on_close: true, - proxy: proxy2, - event_tx, - #[cfg(target_os = "ios")] - views: vec![], - } - } - - pub(super) fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { - self.webviews.remove(&window_id); - - if self.webviews.is_empty() && self.quit_app_on_close { - *control_flow = ControlFlow::Exit; - } - } - - pub(super) fn try_load_ready_webviews(&mut self) { - if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { - let mut new_queue = Vec::new(); - - { - let mut queue = self.pending_edits.lock().unwrap(); - std::mem::swap(&mut new_queue, &mut *queue); - } - - let (_id, view) = self.webviews.iter_mut().next().unwrap(); - - for edit in new_queue.drain(..) { - view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) - .unwrap(); - } - } - } - - pub(crate) fn set_template(&self, _serialized_template: String) { - todo!("hot reloading currently WIP") - } -} diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 756814de0..2ed4bbee9 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,19 +1,32 @@ +use std::cell::RefCell; use std::rc::Rc; +use std::rc::Weak; -use crate::controller::DesktopController; +use crate::create_new_window; +use crate::eval::EvalResult; +use crate::events::IpcMessage; +use crate::shortcut::IntoKeyCode; +use crate::shortcut::IntoModifersState; +use crate::shortcut::ShortcutId; +use crate::shortcut::ShortcutRegistry; +use crate::shortcut::ShortcutRegistryError; +use crate::Config; +use crate::WebviewHandler; use dioxus_core::ScopeState; -use serde::de::Error; +use dioxus_core::VirtualDom; +#[cfg(all(feature = "hot-reload", debug_assertions))] +use dioxus_hot_reload::HotReloadMsg; use serde_json::Value; -use std::future::Future; -use std::future::IntoFuture; -use std::pin::Pin; -use wry::application::event_loop::ControlFlow; +use slab::Slab; +use wry::application::event::Event; use wry::application::event_loop::EventLoopProxy; +use wry::application::event_loop::EventLoopWindowTarget; #[cfg(target_os = "ios")] use wry::application::platform::ios::WindowExtIOS; use wry::application::window::Fullscreen as WryFullscreen; - -use UserWindowEvent::*; +use wry::application::window::Window; +use wry::application::window::WindowId; +use wry::webview::WebView; pub type ProxyType = EventLoopProxy; @@ -24,6 +37,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. @@ -39,21 +54,92 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { #[derive(Clone)] pub struct DesktopContext { /// The wry/tao proxy to the current window + pub webview: Rc, + + /// The proxy to the event loop pub proxy: ProxyType, - pub(super) eval_reciever: Rc>>, + + /// 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, + + pub(crate) event_handlers: WindowEventHandlers, + + pub(crate) shortcut_manager: ShortcutRegistry, + + #[cfg(target_os = "ios")] + pub(crate) views: Rc>>, +} + +/// A smart pointer to the current window. +impl std::ops::Deref for DesktopContext { + type Target = Window; + + fn deref(&self) -> &Self::Target { + self.webview.window() + } } impl DesktopContext { pub(crate) fn new( + webview: Rc, proxy: ProxyType, - eval_reciever: tokio::sync::mpsc::UnboundedReceiver, + event_loop: EventLoopWindowTarget, + webviews: WebviewQueue, + event_handlers: WindowEventHandlers, + shortcut_manager: ShortcutRegistry, ) -> Self { Self { + webview, proxy, - eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)), + event_loop, + eval: tokio::sync::broadcast::channel(8).0, + pending_windows: webviews, + event_handlers, + shortcut_manager, + #[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, + &self.event_handlers, + self.shortcut_manager.clone(), + ); + + 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. @@ -63,284 +149,187 @@ impl DesktopContext { /// onmousedown: move |_| { desktop.drag_window(); } /// ``` pub fn drag(&self) { - let _ = self.proxy.send_event(DragWindow); + let window = self.webview.window(); + + // if the drag_window has any errors, we don't do anything + if window.fullscreen().is_none() { + window.drag_window().unwrap(); + } } - /// set window minimize state - pub fn set_minimized(&self, minimized: bool) { - let _ = self.proxy.send_event(Minimize(minimized)); - } - - /// set window maximize state - pub fn set_maximized(&self, maximized: bool) { - let _ = self.proxy.send_event(Maximize(maximized)); - } - - /// toggle window maximize state + /// Toggle whether the window is maximized or not pub fn toggle_maximized(&self) { - let _ = self.proxy.send_event(MaximizeToggle); - } + let window = self.webview.window(); - /// set window visible or not - pub fn set_visible(&self, visible: bool) { - let _ = self.proxy.send_event(Visible(visible)); + window.set_maximized(!window.is_maximized()) } /// close window pub fn close(&self) { - let _ = self.proxy.send_event(CloseWindow); + let _ = self + .proxy + .send_event(UserWindowEvent(EventData::CloseWindow, self.id())); } - /// set window to focus - pub fn focus(&self) { - let _ = self.proxy.send_event(FocusWindow); + /// close window + pub fn close_window(&self, id: WindowId) { + let _ = self + .proxy + .send_event(UserWindowEvent(EventData::CloseWindow, id)); } /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { - let _ = self.proxy.send_event(Fullscreen(fullscreen)); - } - - /// set resizable state - pub fn set_resizable(&self, resizable: bool) { - let _ = self.proxy.send_event(Resizable(resizable)); - } - - /// set the window always on top - pub fn set_always_on_top(&self, top: bool) { - let _ = self.proxy.send_event(AlwaysOnTop(top)); - } - - /// set cursor visible or not - pub fn set_cursor_visible(&self, visible: bool) { - let _ = self.proxy.send_event(CursorVisible(visible)); - } - - /// set cursor grab - pub fn set_cursor_grab(&self, grab: bool) { - let _ = self.proxy.send_event(CursorGrab(grab)); - } - - /// set window title - pub fn set_title(&self, title: &str) { - let _ = self.proxy.send_event(SetTitle(String::from(title))); - } - - /// change window to borderless - pub fn set_decorations(&self, decoration: bool) { - let _ = self.proxy.send_event(SetDecorations(decoration)); - } - - /// set window zoom level - pub fn set_zoom_level(&self, scale_factor: f64) { - let _ = self.proxy.send_event(SetZoomLevel(scale_factor)); + if let Some(handle) = self.webview.window().current_monitor() { + self.webview + .window() + .set_fullscreen(fullscreen.then_some(WryFullscreen::Borderless(Some(handle)))); + } } /// launch print modal pub fn print(&self) { - let _ = self.proxy.send_event(Print); + if let Err(e) = self.webview.print() { + log::warn!("Open print modal failed: {e}"); + } + } + + /// Set the zoom level of the webview + pub fn set_zoom_level(&self, level: f64) { + self.webview.zoom(level); } /// opens DevTool window pub fn devtool(&self) { - let _ = self.proxy.send_event(DevTool); + #[cfg(debug_assertions)] + self.webview.open_devtools(); + + #[cfg(not(debug_assertions))] + log::warn!("Devtools are disabled in release builds"); } - /// run (evaluate) a script in the WebView context - pub fn eval(&self, script: impl std::string::ToString) { - let _ = self.proxy.send_event(Eval(script.to_string())); + /// Evaluate a javascript expression + pub fn eval(&self, code: &str) -> EvalResult { + // Embed the return of the eval in a function so we can send it back to the main thread + let script = format!( + r#" + window.ipc.postMessage( + JSON.stringify({{ + "method":"eval_result", + "params": ( + function(){{ + {code} + }} + )() + }}) + ); + "# + ); + + if let Err(e) = self.webview.evaluate_script(&script) { + // send an error to the eval receiver + log::warn!("Eval script error: {e}"); + } + + EvalResult::new(self.eval.clone()) } - /// Push view + /// Create a wry event handler that listens for wry events. + /// This event handler is scoped to the currently active window and will only recieve events that are either global or related to the current window. + /// + /// The id this function returns can be used to remove the event handler with [`DesktopContext::remove_wry_event_handler`] + pub fn create_wry_event_handler( + &self, + handler: impl FnMut(&Event, &EventLoopWindowTarget) + 'static, + ) -> WryEventHandlerId { + self.event_handlers.add(self.id(), handler) + } + + /// Remove a wry event handler created with [`DesktopContext::create_wry_event_handler`] + pub fn remove_wry_event_handler(&self, id: WryEventHandlerId) { + self.event_handlers.remove(id) + } + + /// Create a global shortcut + /// + /// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information. + pub fn create_shortcut( + &self, + key: impl IntoKeyCode, + modifiers: impl IntoModifersState, + callback: impl FnMut() + 'static, + ) -> Result { + self.shortcut_manager.add_shortcut( + modifiers.into_modifiers_state(), + key.into_key_code(), + Box::new(callback), + ) + } + + /// Remove a global shortcut + pub fn remove_shortcut(&self, id: ShortcutId) { + self.shortcut_manager.remove_shortcut(id) + } + + /// Remove all global shortcuts + pub fn remove_all_shortcuts(&self) { + self.shortcut_manager.remove_all() + } + + /// Push an objc view to the window #[cfg(target_os = "ios")] pub fn push_view(&self, view: objc_id::ShareId) { - let _ = self.proxy.send_event(PushView(view)); + let window = self.webview.window(); + + unsafe { + use objc::runtime::Object; + use objc::*; + assert!(is_main_thread()); + let ui_view = window.ui_view() as *mut Object; + let ui_view_frame: *mut Object = msg_send![ui_view, frame]; + let _: () = msg_send![view, setFrame: ui_view_frame]; + let _: () = msg_send![view, setAutoresizingMask: 31]; + + let ui_view_controller = window.ui_view_controller() as *mut Object; + let _: () = msg_send![ui_view_controller, setView: view]; + self.views.borrow_mut().push(ui_view); + } } - /// Push view + /// Pop an objc view from the window #[cfg(target_os = "ios")] pub fn pop_view(&self) { - let _ = self.proxy.send_event(PopView); - } -} - -#[derive(Debug)] -pub enum UserWindowEvent { - EditsReady, - Initialize, - - CloseWindow, - DragWindow, - FocusWindow, - - /// Set a new Dioxus template for hot-reloading - /// - /// Is a no-op in release builds. Must fit the right format for templates - SetTemplate(String), - - Visible(bool), - Minimize(bool), - Maximize(bool), - MaximizeToggle, - Resizable(bool), - AlwaysOnTop(bool), - Fullscreen(bool), - - CursorVisible(bool), - CursorGrab(bool), - - SetTitle(String), - SetDecorations(bool), - - SetZoomLevel(f64), - - Print, - DevTool, - - Eval(String), - - #[cfg(target_os = "ios")] - PushView(objc_id::ShareId), - #[cfg(target_os = "ios")] - PopView, -} - -impl DesktopController { - pub(super) fn handle_event( - &mut self, - user_event: UserWindowEvent, - control_flow: &mut ControlFlow, - ) { - // currently dioxus-desktop supports a single window only, - // so we can grab the only webview from the map; - // on wayland it is possible that a user event is emitted - // before the webview is initialized. ignore the event. - let webview = if let Some(webview) = self.webviews.values().next() { - webview - } else { - return; - }; - - let window = webview.window(); - - match user_event { - Initialize | EditsReady => self.try_load_ready_webviews(), - SetTemplate(template) => self.set_template(template), - CloseWindow => *control_flow = ControlFlow::Exit, - DragWindow => { - // if the drag_window has any errors, we don't do anything - window.fullscreen().is_none().then(|| window.drag_window()); - } - Visible(state) => window.set_visible(state), - Minimize(state) => window.set_minimized(state), - Maximize(state) => window.set_maximized(state), - MaximizeToggle => window.set_maximized(!window.is_maximized()), - Fullscreen(state) => { - if let Some(handle) = window.current_monitor() { - window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle)))); - } - } - FocusWindow => window.set_focus(), - Resizable(state) => window.set_resizable(state), - AlwaysOnTop(state) => window.set_always_on_top(state), - - Eval(code) => { - let script = format!( - r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{ - {} - }})()}}));"#, - code - ); - if let Err(e) = webview.evaluate_script(&script) { - // we can't panic this error. - log::warn!("Eval script error: {e}"); - } - } - CursorVisible(state) => window.set_cursor_visible(state), - CursorGrab(state) => { - let _ = window.set_cursor_grab(state); - } - - SetTitle(content) => window.set_title(&content), - SetDecorations(state) => window.set_decorations(state), - - SetZoomLevel(scale_factor) => webview.zoom(scale_factor), - - Print => { - if let Err(e) = webview.print() { - // we can't panic this error. - log::warn!("Open print modal failed: {e}"); - } - } - DevTool => { - #[cfg(debug_assertions)] - webview.open_devtools(); - #[cfg(not(debug_assertions))] - log::warn!("Devtools are disabled in release builds"); - } - - #[cfg(target_os = "ios")] - PushView(view) => unsafe { - use objc::runtime::Object; - use objc::*; - assert!(is_main_thread()); - let ui_view = window.ui_view() as *mut Object; - let ui_view_frame: *mut Object = msg_send![ui_view, frame]; - let _: () = msg_send![view, setFrame: ui_view_frame]; - let _: () = msg_send![view, setAutoresizingMask: 31]; + let window = self.webview.window(); + unsafe { + use objc::runtime::Object; + use objc::*; + assert!(is_main_thread()); + if let Some(view) = self.views.borrow_mut().pop() { let ui_view_controller = window.ui_view_controller() as *mut Object; let _: () = msg_send![ui_view_controller, setView: view]; - self.views.push(ui_view); - }, - - #[cfg(target_os = "ios")] - PopView => unsafe { - use objc::runtime::Object; - use objc::*; - assert!(is_main_thread()); - if let Some(view) = self.views.pop() { - let ui_view_controller = window.ui_view_controller() as *mut Object; - let _: () = msg_send![ui_view_controller, setView: view]; - } - }, - } - } -} - -/// Get a closure that executes any JavaScript in the WebView context. -pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult { - let desktop = use_window(cx).clone(); - cx.use_hook(|| { - move |script| { - desktop.eval(script); - let recv = desktop.eval_reciever.clone(); - EvalResult { reciever: recv } - } - }) -} - -/// A future that resolves to the result of a JavaScript evaluation. -pub struct EvalResult { - reciever: Rc>>, -} - -impl IntoFuture for EvalResult { - type Output = Result; - - type IntoFuture = Pin>>>; - - fn into_future(self) -> Self::IntoFuture { - Box::pin(async move { - let mut reciever = self.reciever.lock().await; - match reciever.recv().await { - Some(result) => Ok(result), - None => Err(serde_json::Error::custom("No result returned")), } - }) as Pin>>> + } } } +#[derive(Debug, Clone)] +pub struct UserWindowEvent(pub EventData, pub WindowId); + +#[derive(Debug, Clone)] +pub enum EventData { + Poll, + + Ipc(IpcMessage), + + #[cfg(all(feature = "hot-reload", debug_assertions))] + HotReloadEvent(HotReloadMsg), + + NewWindow, + + CloseWindow, +} + #[cfg(target_os = "ios")] fn is_main_thread() -> bool { use objc::runtime::{Class, BOOL, NO}; @@ -350,3 +339,114 @@ fn is_main_thread() -> bool { let result: BOOL = unsafe { msg_send![cls, isMainThread] }; result != NO } + +/// The unique identifier of a window event handler. This can be used to later remove the handler. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WryEventHandlerId(usize); + +#[derive(Clone, Default)] +pub(crate) struct WindowEventHandlers { + handlers: Rc>>, +} + +impl WindowEventHandlers { + pub(crate) fn add( + &self, + window_id: WindowId, + handler: impl FnMut(&Event, &EventLoopWindowTarget) + 'static, + ) -> WryEventHandlerId { + WryEventHandlerId( + self.handlers + .borrow_mut() + .insert(WryWindowEventHandlerInner { + window_id, + handler: Box::new(handler), + }), + ) + } + + pub(crate) fn remove(&self, id: WryEventHandlerId) { + self.handlers.borrow_mut().try_remove(id.0); + } + + pub(crate) fn apply_event( + &self, + event: &Event, + target: &EventLoopWindowTarget, + ) { + for (_, handler) in self.handlers.borrow_mut().iter_mut() { + handler.apply_event(event, target); + } + } +} + +struct WryWindowEventHandlerInner { + window_id: WindowId, + handler: WryEventHandlerCallback, +} + +type WryEventHandlerCallback = + Box, &EventLoopWindowTarget) + 'static>; + +impl WryWindowEventHandlerInner { + fn apply_event( + &mut self, + event: &Event, + target: &EventLoopWindowTarget, + ) { + // if this event does not apply to the window this listener cares about, return + match event { + Event::WindowEvent { window_id, .. } + | Event::MenuEvent { + window_id: Some(window_id), + .. + } => { + if *window_id != self.window_id { + return; + } + } + _ => (), + } + (self.handler)(event, target) + } +} + +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_wry_event_handler( + cx: &ScopeState, + handler: impl FnMut(&Event, &EventLoopWindowTarget) + 'static, +) -> &WryEventHandler { + let desktop = use_window(cx); + cx.use_hook(move || { + let desktop = desktop.clone(); + + let id = desktop.create_wry_event_handler(handler); + + WryEventHandler { + handlers: desktop.event_handlers, + id, + } + }) +} + +/// A wry event handler that is scoped to the current component and window. The event handler will only receive events for the window it was created for and global events. +/// +/// This will automatically be removed when the component is unmounted. +pub struct WryEventHandler { + handlers: WindowEventHandlers, + /// The unique identifier of the event handler. + pub id: WryEventHandlerId, +} + +impl WryEventHandler { + /// Remove the event handler. + pub fn remove(&self) { + self.handlers.remove(self.id); + } +} + +impl Drop for WryEventHandler { + fn drop(&mut self) { + self.handlers.remove(self.id); + } +} diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs new file mode 100644 index 000000000..7a92bedf5 --- /dev/null +++ b/packages/desktop/src/eval.rs @@ -0,0 +1,45 @@ +use std::rc::Rc; + +use crate::use_window; +use dioxus_core::ScopeState; +use serde::de::Error; +use std::future::Future; +use std::future::IntoFuture; +use std::pin::Pin; + +/// A future that resolves to the result of a JavaScript evaluation. +pub struct EvalResult { + pub(crate) broadcast: tokio::sync::broadcast::Sender, +} + +impl EvalResult { + pub(crate) fn new(sender: tokio::sync::broadcast::Sender) -> Self { + Self { broadcast: sender } + } +} + +impl IntoFuture for EvalResult { + type Output = Result; + + type IntoFuture = Pin>>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + let mut reciever = self.broadcast.subscribe(); + match reciever.recv().await { + Ok(result) => Ok(result), + Err(_) => Err(serde_json::Error::custom("No result returned")), + } + }) as Pin>>> + } +} + +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_eval(cx: &ScopeState) -> &Rc EvalResult> { + let desktop = use_window(cx); + &*cx.use_hook(|| { + let desktop = desktop.clone(); + + Rc::new(move |script: String| desktop.eval(&script)) as Rc EvalResult> + }) +} diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index f9b3fe5cb..7059a7c85 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -2,8 +2,8 @@ use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize)] -pub(crate) struct IpcMessage { +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct IpcMessage { method: String, params: serde_json::Value, } @@ -17,13 +17,3 @@ impl IpcMessage { self.params } } - -pub(crate) fn parse_ipc_message(payload: &str) -> Option { - match serde_json::from_str(payload) { - Ok(message) => Some(message), - Err(e) => { - log::error!("could not parse IPC message, error: {}", e); - None - } - } -} diff --git a/packages/desktop/src/hot_reload.rs b/packages/desktop/src/hot_reload.rs deleted file mode 100644 index e2628cdd4..000000000 --- a/packages/desktop/src/hot_reload.rs +++ /dev/null @@ -1,56 +0,0 @@ -#![allow(dead_code)] - -use dioxus_core::VirtualDom; - -use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; -use std::io::{BufRead, BufReader}; -use std::time::Duration; -use std::{sync::Arc, sync::Mutex}; - -fn handle_error(connection: std::io::Result) -> Option { - connection - .map_err(|error| eprintln!("Incoming connection failed: {}", error)) - .ok() -} - -pub(crate) fn init(_dom: &VirtualDom) { - let latest_in_connection: Arc>>> = - Arc::new(Mutex::new(None)); - - let latest_in_connection_handle = latest_in_connection.clone(); - - // connect to processes for incoming data - std::thread::spawn(move || { - let temp_file = std::env::temp_dir().join("@dioxusin"); - - if let Ok(listener) = LocalSocketListener::bind(temp_file) { - for conn in listener.incoming().filter_map(handle_error) { - *latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn)); - } - } - }); - - std::thread::spawn(move || { - loop { - if let Some(conn) = &mut *latest_in_connection.lock().unwrap() { - let mut buf = String::new(); - match conn.read_line(&mut buf) { - Ok(_) => { - todo!() - // let msg: SetTemplateMsg = serde_json::from_str(&buf).unwrap(); - // channel - // .start_send(SchedulerMsg::SetTemplate(Box::new(msg))) - // .unwrap(); - } - Err(err) => { - if err.kind() != std::io::ErrorKind::WouldBlock { - break; - } - } - } - } - // give the error handler time to take the mutex - std::thread::sleep(Duration::from_millis(100)); - } - }); -} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0080e671b..434cf3305 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -4,36 +4,40 @@ #![deny(missing_docs)] mod cfg; -mod controller; mod desktop_context; mod escape; +mod eval; mod events; mod protocol; - -#[cfg(all(feature = "hot-reload", debug_assertions))] -mod hot_reload; - -use std::sync::atomic::AtomicBool; -use std::sync::Arc; - -use desktop_context::UserWindowEvent; -pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult}; -use futures_channel::mpsc::UnboundedSender; -pub use wry; -pub use wry::application as tao; +mod shortcut; +mod waker; +mod webview; pub use cfg::Config; -use controller::DesktopController; +pub use desktop_context::{ + use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId, +}; +use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; -use events::parse_ipc_message; +use dioxus_html::HtmlEvent; +pub use eval::{use_eval, EvalResult}; +use futures_util::{pin_mut, FutureExt}; +use shortcut::ShortcutRegistry; +pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; +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}, event_loop::{ControlFlow, EventLoop}, - window::Window, }; -use wry::webview::WebViewBuilder; +pub use wry; +pub use wry::application as tao; +use wry::webview::WebView; +use wry::{application::window::WindowId, webview::WebContext}; /// Launch the WebView and run the event loop. /// @@ -81,7 +85,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// Launch the WebView and run the event loop, with configuration and root props. /// -/// This function will start a multithreaded Tokio runtime as well the WebView event loop. +/// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread. /// /// You can configure the WebView window with a configuration closure /// @@ -89,7 +93,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// use dioxus::prelude::*; /// /// fn main() { -/// dioxus_desktop::launch_cfg(app, AppProps { name: "asd" }, |c| c); +/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); /// } /// /// struct AppProps { @@ -102,162 +106,237 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// }) /// } /// ``` -pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { - let event_loop = EventLoop::with_user_event(); - let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy()); +pub fn launch_with_props(root: Component

, props: P, cfg: Config) { + let event_loop = EventLoop::::with_user_event(); + + let proxy = event_loop.create_proxy(); + + // Intialize hot reloading if it is enabled + #[cfg(all(feature = "hot-reload", debug_assertions))] + { + let proxy = proxy.clone(); + dioxus_hot_reload::connect(move |template| { + let _ = proxy.send_event(UserWindowEvent( + EventData::HotReloadEvent(template), + unsafe { WindowId::dummy() }, + )); + }); + } + + // We start the tokio runtime *on this thread* + // Any future we poll later will use this runtime to spawn tasks and for IO + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + // We enter the runtime but we poll futures manually, circumventing the per-task runtime budget + let _guard = rt.enter(); + + // We only have one webview right now, but we'll have more later + // Store them in a hashmap so we can remove them when they're closed + let mut webviews = HashMap::::new(); + + // We use this to allow dynamically adding and removing window event handlers + let event_handlers = WindowEventHandlers::default(); + + let queue = WebviewQueue::default(); + + let shortcut_manager = ShortcutRegistry::new(&event_loop); + + // 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_handlers, + shortcut_manager.clone(), + )); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; - match window_event { - Event::NewEvents(StartCause::Init) => desktop.start(&mut cfg, event_loop), + event_handlers.apply_event(&window_event, event_loop); + match window_event { Event::WindowEvent { event, window_id, .. } => match event { - WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow), + WindowEvent::CloseRequested => { + webviews.remove(&window_id); + + if webviews.is_empty() { + *control_flow = ControlFlow::Exit + } + } + WindowEvent::Destroyed { .. } => { + webviews.remove(&window_id); + + if webviews.is_empty() { + *control_flow = ControlFlow::Exit; + } + } _ => {} }, - Event::UserEvent(user_event) => desktop.handle_event(user_event, control_flow), - Event::MainEventsCleared => {} - Event::Resumed => {} - Event::Suspended => {} - Event::LoopDestroyed => {} - Event::RedrawRequested(_id) => {} - _ => {} - } - }) -} - -impl DesktopController { - fn start( - &mut self, - cfg: &mut Config, - event_loop: &tao::event_loop::EventLoopWindowTarget, - ) { - let webview = build_webview( - cfg, - event_loop, - self.is_ready.clone(), - self.proxy.clone(), - self.eval_sender.clone(), - self.event_tx.clone(), - ); - - self.webviews.insert(webview.window().id(), webview); - } -} - -fn build_webview( - cfg: &mut Config, - event_loop: &tao::event_loop::EventLoopWindowTarget, - is_ready: Arc, - proxy: tao::event_loop::EventLoopProxy, - eval_sender: tokio::sync::mpsc::UnboundedSender, - event_tx: UnboundedSender, -) -> wry::webview::WebView { - let builder = cfg.window.clone(); - 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(); - - // We assume that if the icon is None in cfg, then the user just didnt set it - if cfg.window.window.window_icon.is_none() { - window.set_window_icon(Some( - tao::window::Icon::from_rgba( - include_bytes!("./assets/default_icon.bin").to_vec(), - 460, - 460, - ) - .expect("image parse failed"), - )); - } - - let mut webview = WebViewBuilder::new(window) - .unwrap() - .with_transparent(cfg.window.window.transparent) - .with_url("dioxus://index.html/") - .unwrap() - .with_ipc_handler(move |_window: &Window, payload: String| { - let message = match parse_ipc_message(&payload) { - Some(message) => message, - None => { - log::error!("Failed to parse IPC message: {}", payload); - return; + 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)); } - }; + } - match message.method() { - "eval_result" => { - let result = message.params(); - eval_sender.send(result).unwrap(); + Event::UserEvent(event) => match event.0 { + #[cfg(all(feature = "hot-reload", debug_assertions))] + EventData::HotReloadEvent(msg) => match msg { + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { + for webview in webviews.values_mut() { + webview.dom.replace_template(template); + + poll_vdom(webview); + } + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + *control_flow = ControlFlow::Exit; + } + }, + + EventData::CloseWindow => { + webviews.remove(&event.1); + + if webviews.is_empty() { + *control_flow = ControlFlow::Exit + } } - "user_event" => { - _ = event_tx.unbounded_send(message.params()); + + EventData::Poll => { + if let Some(view) = webviews.get_mut(&event.1) { + poll_vdom(view); + } } - "initialize" => { - is_ready.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = proxy.send_event(UserWindowEvent::EditsReady); + + 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); } - "browser_open" => { - let data = message.params(); - log::trace!("Open browser: {:?}", data); - if let Some(temp) = data.as_object() { + + 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 url = temp.get("href").unwrap().as_str().unwrap(); - if let Err(e) = webbrowser::open(url) { + let open = webbrowser::open(temp["href"].as_str().unwrap()); + if let Err(e) = open { log::error!("Open Browser error: {:?}", e); } } } } - _ => (), - } - }) - .with_custom_protocol(String::from("dioxus"), move |r| { - protocol::desktop_handler( - r, - resource_dir.clone(), - custom_head.clone(), - index_file.clone(), - &root_name, - ) - }) - .with_file_drop_handler(move |window, evet| { - file_handler - .as_ref() - .map(|handler| handler(window, evet)) - .unwrap_or_default() - }); - for (name, handler) in cfg.protocols.drain(..) { - webview = webview.with_custom_protocol(name, handler) + _ => {} + }, + Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id), + _ => {} + } + }) +} + +fn create_new_window( + mut cfg: Config, + event_loop: &EventLoopWindowTarget, + proxy: &EventLoopProxy, + dom: VirtualDom, + queue: &WebviewQueue, + event_handlers: &WindowEventHandlers, + shortcut_manager: ShortcutRegistry, +) -> WebviewHandler { + let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone()); + + dom.base_scope().provide_context(DesktopContext::new( + webview.clone(), + proxy.clone(), + event_loop.clone(), + queue.clone(), + event_handlers.clone(), + shortcut_manager, + )); + + 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), + web_context, } +} + +struct WebviewHandler { + dom: VirtualDom, + webview: Rc, + waker: Waker, + web_context: WebContext, +} - if cfg.disable_context_menu { - // in release mode, we don't want to show the dev tool or reload menus - webview = webview.with_initialization_script( - r#" - if (document.addEventListener) { - document.addEventListener('contextmenu', function(e) { - e.preventDefault(); - }, false); - } else { - document.attachEvent('oncontextmenu', function() { - window.event.returnValue = false; - }); - } - "#, - ) - } else { - // in debug, we are okay with the reload menu showing and dev tool - webview = webview.with_devtools(true); +/// Poll the virtualdom until it's pending +/// +/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again +/// +/// All IO is done on the tokio runtime we started earlier +fn poll_vdom(view: &mut WebviewHandler) { + let mut cx = std::task::Context::from_waker(&view.waker); + + loop { + { + let fut = view.dom.wait_for_work(); + pin_mut!(fut); + + match fut.poll_unpin(&mut cx) { + std::task::Poll::Ready(_) => {} + std::task::Poll::Pending => break, + } + } + + send_edits(view.dom.render_immediate(), &view.webview); } +} + +/// Send a list of mutations to the webview +fn send_edits(edits: Mutations, webview: &WebView) { + let serialized = serde_json::to_string(&edits).unwrap(); - webview.build().unwrap() + // todo: use SSE and binary data to send the edits with lower overhead + _ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})")); } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 8835078f2..efdc849a5 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,5 +1,8 @@ use dioxus_interpreter_js::INTERPRETER_JS; -use std::path::{Path, PathBuf}; +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; use wry::{ http::{status::StatusCode, Request, Response}, Result, @@ -11,87 +14,75 @@ fn module_loader(root_name: &str) -> String { -"#, - root_name +"# ) } 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() { +) -> Result>> { + // 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(Cow::from(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(Cow::from(std::fs::read(asset)?)) + .map_err(From::from); + } + + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Cow::from(String::from("Not Found").into_bytes())) + .map_err(From::from) } #[allow(unreachable_code)] @@ -128,7 +119,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 +134,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/readme.md b/packages/desktop/src/readme.md index 3ac0d66df..cd4dbe41a 100644 --- a/packages/desktop/src/readme.md +++ b/packages/desktop/src/readme.md @@ -6,7 +6,7 @@ Render the Dioxus VirtualDom using the platform's native WebView implementation. One of Dioxus' flagship features is the ability to quickly build a native desktop app that looks and feels the same across platforms. 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. -Dioxus Desktop is built off Tauri. 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. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. +Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over the 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. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. ## Getting Set up @@ -18,10 +18,11 @@ $ cargo new --bin demo $ cd app ``` -Add Dioxus with the `desktop` feature: +Add Dioxus and the `desktop` renderer feature: ```shell -$ cargo add dioxus --features desktop +$ cargo add dioxus +$ cargo add dioxus-desktop ``` Edit your `main.rs`: @@ -48,4 +49,4 @@ To configure the webview, menubar, and other important desktop-specific features ## Future Steps -Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! +Make sure to read the [Dioxus Guide](https://dioxuslabs.com/docs/0.3/guide/en) if you already haven't! diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs new file mode 100644 index 000000000..5232bdb8b --- /dev/null +++ b/packages/desktop/src/shortcut.rs @@ -0,0 +1,332 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use dioxus_core::ScopeState; +use dioxus_html::input_data::keyboard_types::Modifiers; +use slab::Slab; +use wry::application::{ + accelerator::{Accelerator, AcceleratorId}, + event_loop::EventLoopWindowTarget, + global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError}, + keyboard::{KeyCode, ModifiersState}, +}; + +use crate::{use_window, DesktopContext}; + +#[derive(Clone)] +pub(crate) struct ShortcutRegistry { + manager: Rc>, + shortcuts: ShortcutMap, +} + +type ShortcutMap = Rc>>; + +struct Shortcut { + shortcut: GlobalShortcut, + callbacks: Slab>, +} + +impl Shortcut { + fn insert(&mut self, callback: Box) -> usize { + self.callbacks.insert(callback) + } + + fn remove(&mut self, id: usize) { + let _ = self.callbacks.remove(id); + } + + fn is_empty(&self) -> bool { + self.callbacks.is_empty() + } +} + +impl ShortcutRegistry { + pub fn new(target: &EventLoopWindowTarget) -> Self { + let myself = Self { + manager: Rc::new(RefCell::new(ShortcutManager::new(target))), + shortcuts: Rc::new(RefCell::new(HashMap::new())), + }; + // prevent CTRL+R from reloading the page which breaks apps + let _ = myself.add_shortcut( + Some(ModifiersState::CONTROL), + KeyCode::KeyR, + Box::new(|| {}), + ); + myself + } + + pub(crate) fn call_handlers(&self, id: AcceleratorId) { + if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id) { + for (_, callback) in callbacks.iter_mut() { + (callback)(); + } + } + } + + pub(crate) fn add_shortcut( + &self, + modifiers: impl Into>, + key: KeyCode, + callback: Box, + ) -> Result { + let accelerator = Accelerator::new(modifiers, key); + let accelerator_id = accelerator.clone().id(); + let mut shortcuts = self.shortcuts.borrow_mut(); + Ok( + if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) { + let id = callbacks.insert(callback); + ShortcutId { + id: accelerator_id, + number: id, + } + } else { + match self.manager.borrow_mut().register(accelerator) { + Ok(global_shortcut) => { + let mut slab = Slab::new(); + let id = slab.insert(callback); + let shortcut = Shortcut { + shortcut: global_shortcut, + callbacks: slab, + }; + shortcuts.insert(accelerator_id, shortcut); + ShortcutId { + id: accelerator_id, + number: id, + } + } + Err(ShortcutManagerError::InvalidAccelerator(shortcut)) => { + return Err(ShortcutRegistryError::InvalidShortcut(shortcut)) + } + Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))), + } + }, + ) + } + + pub(crate) fn remove_shortcut(&self, id: ShortcutId) { + let mut shortcuts = self.shortcuts.borrow_mut(); + if let Some(callbacks) = shortcuts.get_mut(&id.id) { + callbacks.remove(id.number); + if callbacks.is_empty() { + if let Some(shortcut) = shortcuts.remove(&id.id) { + let _ = self.manager.borrow_mut().unregister(shortcut.shortcut); + } + } + } + } + + pub(crate) fn remove_all(&self) { + let mut shortcuts = self.shortcuts.borrow_mut(); + shortcuts.clear(); + let _ = self.manager.borrow_mut().unregister_all(); + // prevent CTRL+R from reloading the page which breaks apps + let _ = self.add_shortcut( + Some(ModifiersState::CONTROL), + KeyCode::KeyR, + Box::new(|| {}), + ); + } +} + +#[non_exhaustive] +#[derive(Debug)] +/// An error that can occur when registering a shortcut. +pub enum ShortcutRegistryError { + /// The shortcut is invalid. + InvalidShortcut(String), + /// An unknown error occurred. + Other(Box), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// An global id for a shortcut. +pub struct ShortcutId { + id: AcceleratorId, + number: usize, +} + +/// A global shortcut. This will be automatically removed when it is dropped. +pub struct ShortcutHandle { + desktop: DesktopContext, + /// The id of the shortcut + pub shortcut_id: ShortcutId, +} + +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_global_shortcut( + cx: &ScopeState, + key: impl IntoKeyCode, + modifiers: impl IntoModifersState, + handler: impl FnMut() + 'static, +) -> &Result { + let desktop = use_window(cx); + cx.use_hook(move || { + let desktop = desktop.clone(); + + let id = desktop.create_shortcut( + key.into_key_code(), + modifiers.into_modifiers_state(), + handler, + ); + + Ok(ShortcutHandle { + desktop, + shortcut_id: id?, + }) + }) +} + +impl ShortcutHandle { + /// Remove the shortcut. + pub fn remove(&self) { + self.desktop.remove_shortcut(self.shortcut_id); + } +} + +impl Drop for ShortcutHandle { + fn drop(&mut self) { + self.remove() + } +} + +pub trait IntoModifersState { + fn into_modifiers_state(self) -> ModifiersState; +} + +impl IntoModifersState for ModifiersState { + fn into_modifiers_state(self) -> ModifiersState { + self + } +} + +impl IntoModifersState for Modifiers { + fn into_modifiers_state(self) -> ModifiersState { + let mut state = ModifiersState::empty(); + if self.contains(Modifiers::SHIFT) { + state |= ModifiersState::SHIFT + } + if self.contains(Modifiers::CONTROL) { + state |= ModifiersState::CONTROL + } + if self.contains(Modifiers::ALT) { + state |= ModifiersState::ALT + } + if self.contains(Modifiers::META) || self.contains(Modifiers::SUPER) { + state |= ModifiersState::SUPER + } + state + } +} + +pub trait IntoKeyCode { + fn into_key_code(self) -> KeyCode; +} + +impl IntoKeyCode for KeyCode { + fn into_key_code(self) -> KeyCode { + self + } +} + +impl IntoKeyCode for dioxus_html::KeyCode { + fn into_key_code(self) -> KeyCode { + match self { + dioxus_html::KeyCode::Backspace => KeyCode::Backspace, + dioxus_html::KeyCode::Tab => KeyCode::Tab, + dioxus_html::KeyCode::Clear => KeyCode::NumpadClear, + dioxus_html::KeyCode::Enter => KeyCode::Enter, + dioxus_html::KeyCode::Shift => KeyCode::ShiftLeft, + dioxus_html::KeyCode::Ctrl => KeyCode::ControlLeft, + dioxus_html::KeyCode::Alt => KeyCode::AltLeft, + dioxus_html::KeyCode::Pause => KeyCode::Pause, + dioxus_html::KeyCode::CapsLock => KeyCode::CapsLock, + dioxus_html::KeyCode::Escape => KeyCode::Escape, + dioxus_html::KeyCode::Space => KeyCode::Space, + dioxus_html::KeyCode::PageUp => KeyCode::PageUp, + dioxus_html::KeyCode::PageDown => KeyCode::PageDown, + dioxus_html::KeyCode::End => KeyCode::End, + dioxus_html::KeyCode::Home => KeyCode::Home, + dioxus_html::KeyCode::LeftArrow => KeyCode::ArrowLeft, + dioxus_html::KeyCode::UpArrow => KeyCode::ArrowUp, + dioxus_html::KeyCode::RightArrow => KeyCode::ArrowRight, + dioxus_html::KeyCode::DownArrow => KeyCode::ArrowDown, + dioxus_html::KeyCode::Insert => KeyCode::Insert, + dioxus_html::KeyCode::Delete => KeyCode::Delete, + dioxus_html::KeyCode::Num0 => KeyCode::Numpad0, + dioxus_html::KeyCode::Num1 => KeyCode::Numpad1, + dioxus_html::KeyCode::Num2 => KeyCode::Numpad2, + dioxus_html::KeyCode::Num3 => KeyCode::Numpad3, + dioxus_html::KeyCode::Num4 => KeyCode::Numpad4, + dioxus_html::KeyCode::Num5 => KeyCode::Numpad5, + dioxus_html::KeyCode::Num6 => KeyCode::Numpad6, + dioxus_html::KeyCode::Num7 => KeyCode::Numpad7, + dioxus_html::KeyCode::Num8 => KeyCode::Numpad8, + dioxus_html::KeyCode::Num9 => KeyCode::Numpad9, + dioxus_html::KeyCode::A => KeyCode::KeyA, + dioxus_html::KeyCode::B => KeyCode::KeyB, + dioxus_html::KeyCode::C => KeyCode::KeyC, + dioxus_html::KeyCode::D => KeyCode::KeyD, + dioxus_html::KeyCode::E => KeyCode::KeyE, + dioxus_html::KeyCode::F => KeyCode::KeyF, + dioxus_html::KeyCode::G => KeyCode::KeyG, + dioxus_html::KeyCode::H => KeyCode::KeyH, + dioxus_html::KeyCode::I => KeyCode::KeyI, + dioxus_html::KeyCode::J => KeyCode::KeyJ, + dioxus_html::KeyCode::K => KeyCode::KeyK, + dioxus_html::KeyCode::L => KeyCode::KeyL, + dioxus_html::KeyCode::M => KeyCode::KeyM, + dioxus_html::KeyCode::N => KeyCode::KeyN, + dioxus_html::KeyCode::O => KeyCode::KeyO, + dioxus_html::KeyCode::P => KeyCode::KeyP, + dioxus_html::KeyCode::Q => KeyCode::KeyQ, + dioxus_html::KeyCode::R => KeyCode::KeyR, + dioxus_html::KeyCode::S => KeyCode::KeyS, + dioxus_html::KeyCode::T => KeyCode::KeyT, + dioxus_html::KeyCode::U => KeyCode::KeyU, + dioxus_html::KeyCode::V => KeyCode::KeyV, + dioxus_html::KeyCode::W => KeyCode::KeyW, + dioxus_html::KeyCode::X => KeyCode::KeyX, + dioxus_html::KeyCode::Y => KeyCode::KeyY, + dioxus_html::KeyCode::Z => KeyCode::KeyZ, + dioxus_html::KeyCode::Numpad0 => KeyCode::Numpad0, + dioxus_html::KeyCode::Numpad1 => KeyCode::Numpad1, + dioxus_html::KeyCode::Numpad2 => KeyCode::Numpad2, + dioxus_html::KeyCode::Numpad3 => KeyCode::Numpad3, + dioxus_html::KeyCode::Numpad4 => KeyCode::Numpad4, + dioxus_html::KeyCode::Numpad5 => KeyCode::Numpad5, + dioxus_html::KeyCode::Numpad6 => KeyCode::Numpad6, + dioxus_html::KeyCode::Numpad7 => KeyCode::Numpad7, + dioxus_html::KeyCode::Numpad8 => KeyCode::Numpad8, + dioxus_html::KeyCode::Numpad9 => KeyCode::Numpad9, + dioxus_html::KeyCode::Multiply => KeyCode::NumpadMultiply, + dioxus_html::KeyCode::Add => KeyCode::NumpadAdd, + dioxus_html::KeyCode::Subtract => KeyCode::NumpadSubtract, + dioxus_html::KeyCode::DecimalPoint => KeyCode::NumpadDecimal, + dioxus_html::KeyCode::Divide => KeyCode::NumpadDivide, + dioxus_html::KeyCode::F1 => KeyCode::F1, + dioxus_html::KeyCode::F2 => KeyCode::F2, + dioxus_html::KeyCode::F3 => KeyCode::F3, + dioxus_html::KeyCode::F4 => KeyCode::F4, + dioxus_html::KeyCode::F5 => KeyCode::F5, + dioxus_html::KeyCode::F6 => KeyCode::F6, + dioxus_html::KeyCode::F7 => KeyCode::F7, + dioxus_html::KeyCode::F8 => KeyCode::F8, + dioxus_html::KeyCode::F9 => KeyCode::F9, + dioxus_html::KeyCode::F10 => KeyCode::F10, + dioxus_html::KeyCode::F11 => KeyCode::F11, + dioxus_html::KeyCode::F12 => KeyCode::F12, + dioxus_html::KeyCode::NumLock => KeyCode::NumLock, + dioxus_html::KeyCode::ScrollLock => KeyCode::ScrollLock, + dioxus_html::KeyCode::Semicolon => KeyCode::Semicolon, + dioxus_html::KeyCode::EqualSign => KeyCode::Equal, + dioxus_html::KeyCode::Comma => KeyCode::Comma, + dioxus_html::KeyCode::Period => KeyCode::Period, + dioxus_html::KeyCode::ForwardSlash => KeyCode::Slash, + dioxus_html::KeyCode::GraveAccent => KeyCode::Backquote, + dioxus_html::KeyCode::OpenBracket => KeyCode::BracketLeft, + dioxus_html::KeyCode::BackSlash => KeyCode::Backslash, + dioxus_html::KeyCode::CloseBraket => KeyCode::BracketRight, + dioxus_html::KeyCode::SingleQuote => KeyCode::Quote, + key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key), + } + } +} diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs new file mode 100644 index 000000000..1e34baffc --- /dev/null +++ b/packages/desktop/src/waker.rs @@ -0,0 +1,34 @@ +use crate::desktop_context::{EventData, UserWindowEvent}; +use futures_util::task::ArcWake; +use std::sync::Arc; +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, 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 + unsafe impl Send for DomHandle {} + unsafe impl Sync for DomHandle {} + + impl ArcWake for DomHandle { + fn wake_by_ref(arc_self: &Arc) { + _ = arc_self + .proxy + .send_event(UserWindowEvent(EventData::Poll, arc_self.id)); + } + } + + 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 new file mode 100644 index 000000000..c6909972f --- /dev/null +++ b/packages/desktop/src/webview.rs @@ -0,0 +1,85 @@ +use std::rc::Rc; + +use crate::desktop_context::EventData; +use crate::protocol; +use crate::{desktop_context::UserWindowEvent, Config}; +use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; +pub use wry; +pub use wry::application as tao; +use wry::application::window::Window; +use wry::webview::{WebContext, WebView, WebViewBuilder}; + +pub fn build( + cfg: &mut Config, + event_loop: &EventLoopWindowTarget, + proxy: EventLoopProxy, +) -> (Rc, WebContext) { + let builder = cfg.window.clone(); + let window = builder.build(event_loop).unwrap(); + let file_handler = cfg.file_drop_handler.take(); + let custom_head = cfg.custom_head.clone(); + let index_file = cfg.custom_index.clone(); + let root_name = cfg.root_name.clone(); + + // We assume that if the icon is None in cfg, then the user just didnt set it + if cfg.window.window.window_icon.is_none() { + window.set_window_icon(Some( + tao::window::Icon::from_rgba( + include_bytes!("./assets/default_icon.bin").to_vec(), + 460, + 460, + ) + .expect("image parse failed"), + )); + } + + let mut web_context = WebContext::new(cfg.data_dir.clone()); + + let mut webview = WebViewBuilder::new(window) + .unwrap() + .with_transparent(cfg.window.window.transparent) + .with_url("dioxus://index.html/") + .unwrap() + .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(EventData::Ipc(message), window.id())); + } + }) + .with_custom_protocol(String::from("dioxus"), move |r| { + protocol::desktop_handler(r, custom_head.clone(), index_file.clone(), &root_name) + }) + .with_file_drop_handler(move |window, evet| { + file_handler + .as_ref() + .map(|handler| handler(window, evet)) + .unwrap_or_default() + }) + .with_web_context(&mut web_context); + + for (name, handler) in cfg.protocols.drain(..) { + webview = webview.with_custom_protocol(name, handler) + } + + if cfg.disable_context_menu { + // in release mode, we don't want to show the dev tool or reload menus + webview = webview.with_initialization_script( + r#" + if (document.addEventListener) { + document.addEventListener('contextmenu', function(e) { + e.preventDefault(); + }, false); + } else { + document.attachEvent('oncontextmenu', function() { + window.event.returnValue = false; + }); + } + "#, + ) + } else { + // in debug, we are okay with the reload menu showing and dev tool + webview = webview.with_devtools(true); + } + + (Rc::new(webview.build().unwrap()), web_context) +} diff --git a/packages/tui/.gitignore b/packages/dioxus-tui/.gitignore similarity index 100% rename from packages/tui/.gitignore rename to packages/dioxus-tui/.gitignore diff --git a/packages/tui/.vscode/spellright.dict b/packages/dioxus-tui/.vscode/spellright.dict similarity index 100% rename from packages/tui/.vscode/spellright.dict rename to packages/dioxus-tui/.vscode/spellright.dict diff --git a/packages/dioxus-tui/Cargo.toml b/packages/dioxus-tui/Cargo.toml new file mode 100644 index 000000000..43e568b0f --- /dev/null +++ b/packages/dioxus-tui/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "dioxus-tui" +version = "0.2.2" +authors = ["Jonathan Kelley, @dementhos"] +edition = "2021" +description = "TUI-based renderer for Dioxus" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react", "terminal"] +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.3.0" } +dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] } +dioxus-html = { path = "../html", version = "^0.3.0" } +dioxus-native-core = { path = "../native-core", version = "^0.2.0", features = ["dioxus"] } +dioxus-native-core-macro = { path = "../native-core-macro", version = "^0.3.0" } +dioxus-hot-reload = { path = "../hot-reload", optional = true } +rink = { path = "../rink" } + +crossterm = "0.23.0" +tokio = { version = "1.15.0", features = ["full"] } +futures = "0.3.19" +taffy = "0.2.1" + +[dev-dependencies] +dioxus = { path = "../dioxus" } +tokio = { version = "1" } +criterion = "0.3.5" + +[[bench]] +name = "update" +harness = false + +[features] +default = ["hot-reload"] +hot-reload = ["dioxus-hot-reload"] diff --git a/packages/tui/README.md b/packages/dioxus-tui/README.md similarity index 60% rename from packages/tui/README.md rename to packages/dioxus-tui/README.md index ccce60ea3..cae1cecce 100644 --- a/packages/tui/README.md +++ b/packages/dioxus-tui/README.md @@ -1,5 +1,5 @@

-

Rink

+

Dioxus TUI

Beautiful terminal user interfaces in Rust with Dioxus .

@@ -37,7 +37,6 @@
-
Leverage React-like patterns, CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces with Dioxus. @@ -61,34 +60,36 @@ fn app(cx: Scope) -> Element { ## Background -You can use Html-like semantics with stylesheets, inline styles, tree hierarchy, components, and more in your [`text-based user interface (TUI)`](https://en.wikipedia.org/wiki/Text-based_user_interface) application. +You can use Html-like semantics with inline styles, tree hierarchy, components, and more in your [`text-based user interface (TUI)`](https://en.wikipedia.org/wiki/Text-based_user_interface) application. -Rink is basically a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/) and [`Dioxus`](https://dioxuslabs.com/). Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful. +Dioxus TUI is essentially a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/) and [`Dioxus`](https://dioxuslabs.com/). Dioxus TUI doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful. ## Limitations - **Subset of Html** -Terminals can only render a subset of HTML. We support as much as we can. + Terminals can only render a subset of HTML. We support as much as we can. - **Particular frontend design** -Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers. - + Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers. ## Status -**WARNING: Rink is currently under construction!** +**WARNING: Dioxus TUI is currently under construction!** Rendering a VirtualDom works fine, but the ecosystem of hooks is not yet ready. Additionally, some bugs in the flexbox implementation might be quirky at times. ## Features -Rink features: -- [x] Flexbox based layout system +Dioxus TUI features: + +- [x] Flexbox-based layout system - [ ] CSS selectors - [x] inline CSS support - [x] Built-in focusing system -- [ ] high-quality keyboard support -* [ ] Support for events, hooks, and callbacks1 -* [ ] Html tags2 -1 Basic keyboard and mouse events are implemented. -2 Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality. +* [x] Widgets1 +* [ ] Support for events, hooks, and callbacks2 +* [ ] Html tags3 + +1 Currently only a subset of the input element is implemented as a component (not an element). The `Input` component supports sliders, text, numbers, passwords, buttons, and checkboxes. +2 Basic keyboard, mouse, and focus events are implemented. +3 Currently, most HTML tags don't translate into any meaning inside of Dioxus TUI. So an `input` _element_ won't mean anything nor does it have any additional functionality. diff --git a/packages/dioxus-tui/benches/update.rs b/packages/dioxus-tui/benches/update.rs new file mode 100644 index 000000000..348277328 --- /dev/null +++ b/packages/dioxus-tui/benches/update.rs @@ -0,0 +1,165 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use dioxus::prelude::*; +use dioxus_tui::{Config, TuiContext}; + +criterion_group!(mbenches, tui_update); +criterion_main!(mbenches); + +/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. +fn tui_update(c: &mut Criterion) { + { + let mut group = c.benchmark_group("Update boxes"); + + for size in 1..=20usize { + let parameter_string = format!("{}", (size).pow(2)); + group.bench_with_input( + BenchmarkId::new("size", parameter_string), + &size, + |b, size| { + b.iter(|| { + dioxus_tui::launch_cfg_with_props( + app, + GridProps { + size: *size, + update_count: 1, + }, + Config::default().with_headless(), + ) + }) + }, + ); + } + } + + { + let mut group = c.benchmark_group("Update many boxes"); + + for update_count in 1..=20usize { + let update_count = update_count * 20; + let parameter_string = update_count.to_string(); + group.bench_with_input( + BenchmarkId::new("update count", parameter_string), + &update_count, + |b, update_count| { + b.iter(|| { + dioxus_tui::launch_cfg_with_props( + app, + GridProps { + size: 20, + update_count: *update_count, + }, + Config::default().with_headless(), + ) + }) + }, + ); + } + } +} + +#[derive(Props, PartialEq)] +struct BoxProps { + x: usize, + y: usize, + hue: f32, + alpha: f32, +} +#[allow(non_snake_case)] +fn Box(cx: Scope) -> Element { + let count = use_state(cx, || 0); + + let x = cx.props.x * 2; + let y = cx.props.y * 2; + let hue = cx.props.hue; + let display_hue = cx.props.hue as u32 / 10; + let count = count.get(); + let alpha = cx.props.alpha + (count % 100) as f32; + + cx.render(rsx! { + div { + left: "{x}%", + top: "{y}%", + width: "100%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", + align_items: "center", + p{"{display_hue:03}"} + } + }) +} + +#[derive(Props, PartialEq)] +struct GridProps { + size: usize, + update_count: usize, +} +#[allow(non_snake_case)] +fn Grid(cx: Scope) -> Element { + let size = cx.props.size; + let count = use_state(cx, || 0); + let counts = use_ref(cx, || vec![0; size * size]); + + let ctx: TuiContext = cx.consume_context().unwrap(); + if *count.get() + cx.props.update_count >= (size * size) { + ctx.quit(); + } else { + for _ in 0..cx.props.update_count { + counts.with_mut(|c| { + let i = *count.current(); + c[i] += 1; + c[i] %= 360; + }); + count.with_mut(|i| { + *i += 1; + *i %= size * size; + }); + } + } + + render! { + div{ + width: "100%", + height: "100%", + flex_direction: "column", + (0..size).map(|x| + { + rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + (0..size).map(|y| + { + let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; + let key = format!("{}-{}", x, y); + rsx! { + Box{ + x: x, + y: y, + alpha: 100.0, + hue: alpha, + key: "{key}", + } + } + } + ) + } + } + } + ) + } + } +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: cx.props.size, + update_count: cx.props.update_count, + } + } + }) +} diff --git a/packages/tui/examples/tui_all_events.rs b/packages/dioxus-tui/examples/all_events.rs similarity index 100% rename from packages/tui/examples/tui_all_events.rs rename to packages/dioxus-tui/examples/all_events.rs diff --git a/packages/tui/examples/tui_border.rs b/packages/dioxus-tui/examples/border.rs similarity index 100% rename from packages/tui/examples/tui_border.rs rename to packages/dioxus-tui/examples/border.rs diff --git a/packages/tui/examples/tui_buttons.rs b/packages/dioxus-tui/examples/buttons.rs similarity index 100% rename from packages/tui/examples/tui_buttons.rs rename to packages/dioxus-tui/examples/buttons.rs diff --git a/packages/tui/examples/tui_color_test.rs b/packages/dioxus-tui/examples/color_test.rs similarity index 100% rename from packages/tui/examples/tui_color_test.rs rename to packages/dioxus-tui/examples/color_test.rs diff --git a/packages/tui/examples/tui_colorpicker.rs b/packages/dioxus-tui/examples/colorpicker.rs similarity index 68% rename from packages/tui/examples/tui_colorpicker.rs rename to packages/dioxus-tui/examples/colorpicker.rs index 244eea94b..1de6b8209 100644 --- a/packages/tui/examples/tui_colorpicker.rs +++ b/packages/dioxus-tui/examples/colorpicker.rs @@ -1,6 +1,7 @@ use dioxus::core::RenderReturn; use dioxus::prelude::*; -use dioxus_tui::query::Query; +use dioxus_tui::DioxusElementToNodeId; +use dioxus_tui::Query; use dioxus_tui::Size; fn main() { @@ -10,16 +11,17 @@ fn main() { fn app(cx: Scope) -> Element { let hue = use_state(cx, || 0.0); let brightness = use_state(cx, || 0.0); - let tui_query: &Query = cx.consume_context().unwrap(); + let tui_query: Query = cx.consume_context().unwrap(); + let mapping: DioxusElementToNodeId = cx.consume_context().unwrap(); // disable templates so that every node has an id and can be queried cx.render(rsx! { div{ width: "100%", background_color: "hsl({hue}, 70%, {brightness}%)", onmousemove: move |evt| { - if let RenderReturn::Sync(Some(node)) = cx.root_node() { - if let Some(id) = node.root_ids[0].get() { - let node = tui_query.get(id); + if let RenderReturn::Ready(node) = cx.root_node() { + if let Some(id) = node.root_ids.get(0){ + let node = tui_query.get(mapping.get_node_id(id).unwrap()); let Size{width, height} = node.size().unwrap(); let pos = evt.inner().element_coordinates(); hue.set((pos.x as f32/width as f32)*255.0); diff --git a/packages/tui/examples/tui_components.rs b/packages/dioxus-tui/examples/components.rs similarity index 100% rename from packages/tui/examples/tui_components.rs rename to packages/dioxus-tui/examples/components.rs diff --git a/packages/tui/examples/example.png b/packages/dioxus-tui/examples/example.png similarity index 100% rename from packages/tui/examples/example.png rename to packages/dioxus-tui/examples/example.png diff --git a/packages/tui/examples/tui_flex.rs b/packages/dioxus-tui/examples/flex.rs similarity index 100% rename from packages/tui/examples/tui_flex.rs rename to packages/dioxus-tui/examples/flex.rs diff --git a/packages/tui/examples/tui_hover.rs b/packages/dioxus-tui/examples/hover.rs similarity index 100% rename from packages/tui/examples/tui_hover.rs rename to packages/dioxus-tui/examples/hover.rs diff --git a/packages/tui/examples/tui_list.rs b/packages/dioxus-tui/examples/list.rs similarity index 100% rename from packages/tui/examples/tui_list.rs rename to packages/dioxus-tui/examples/list.rs diff --git a/packages/tui/examples/tui_margin.rs b/packages/dioxus-tui/examples/margin.rs similarity index 100% rename from packages/tui/examples/tui_margin.rs rename to packages/dioxus-tui/examples/margin.rs diff --git a/packages/tui/examples/tui_quadrants.rs b/packages/dioxus-tui/examples/quadrants.rs similarity index 100% rename from packages/tui/examples/tui_quadrants.rs rename to packages/dioxus-tui/examples/quadrants.rs diff --git a/packages/tui/examples/tui_readme.rs b/packages/dioxus-tui/examples/readme.rs similarity index 100% rename from packages/tui/examples/tui_readme.rs rename to packages/dioxus-tui/examples/readme.rs diff --git a/packages/dioxus-tui/examples/stress.rs b/packages/dioxus-tui/examples/stress.rs new file mode 100644 index 000000000..0668077cd --- /dev/null +++ b/packages/dioxus-tui/examples/stress.rs @@ -0,0 +1,114 @@ +use dioxus::prelude::*; +use dioxus_tui::{Config, TuiContext}; + +/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. +fn main() { + for size in 1..=20usize { + for _ in 0..10 { + dioxus_tui::launch_cfg_with_props(app, size, Config::default().with_headless()) + } + } +} + +#[derive(Props, PartialEq)] +struct BoxProps { + x: usize, + y: usize, + hue: f32, + alpha: f32, +} +#[allow(non_snake_case)] +fn Box(cx: Scope) -> Element { + let count = use_state(cx, || 0); + + let x = cx.props.x * 2; + let y = cx.props.y * 2; + let hue = cx.props.hue; + let display_hue = cx.props.hue as u32 / 10; + let count = count.get(); + let alpha = cx.props.alpha + (count % 100) as f32; + + cx.render(rsx! { + div { + left: "{x}%", + top: "{y}%", + width: "100%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", + align_items: "center", + p{"{display_hue:03}"} + } + }) +} + +#[derive(Props, PartialEq)] +struct GridProps { + size: usize, +} +#[allow(non_snake_case)] +fn Grid(cx: Scope) -> Element { + let size = cx.props.size; + let count = use_state(cx, || 0); + let counts = use_ref(cx, || vec![0; size * size]); + + let ctx: TuiContext = cx.consume_context().unwrap(); + if *count.get() + 1 >= (size * size) { + ctx.quit(); + } else { + counts.with_mut(|c| { + let i = *count.current(); + c[i] += 1; + c[i] %= 360; + }); + count.with_mut(|i| { + *i += 1; + *i %= size * size; + }); + } + + render! { + div{ + width: "100%", + height: "100%", + flex_direction: "column", + (0..size).map(|x| + { + rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + (0..size).map(|y| + { + let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; + let key = format!("{}-{}", x, y); + rsx! { + Box{ + x: x, + y: y, + alpha: 100.0, + hue: alpha, + key: "{key}", + } + } + } + ) + } + } + } + ) + } + } +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: *cx.props, + } + } + }) +} diff --git a/packages/tui/examples/tui_task.rs b/packages/dioxus-tui/examples/task.rs similarity index 100% rename from packages/tui/examples/tui_task.rs rename to packages/dioxus-tui/examples/task.rs diff --git a/packages/tui/examples/tui_text.rs b/packages/dioxus-tui/examples/text.rs similarity index 98% rename from packages/tui/examples/tui_text.rs rename to packages/dioxus-tui/examples/text.rs index a6662da8c..1136b9a92 100644 --- a/packages/tui/examples/tui_text.rs +++ b/packages/dioxus-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/dioxus-tui/examples/widgets.rs similarity index 99% rename from packages/tui/examples/tui_widgets.rs rename to packages/dioxus-tui/examples/widgets.rs index 793057bc5..04690522c 100644 --- a/packages/tui/examples/tui_widgets.rs +++ b/packages/dioxus-tui/examples/widgets.rs @@ -1,7 +1,6 @@ use dioxus::prelude::*; use dioxus_html::FormData; use dioxus_tui::prelude::*; -use dioxus_tui::Config; fn main() { dioxus_tui::launch_cfg(app, Config::new()); diff --git a/packages/dioxus-tui/src/lib.rs b/packages/dioxus-tui/src/lib.rs new file mode 100644 index 000000000..296fd4697 --- /dev/null +++ b/packages/dioxus-tui/src/lib.rs @@ -0,0 +1,143 @@ +pub mod prelude; +pub mod widgets; + +use std::{ + ops::Deref, + rc::Rc, + sync::{Arc, RwLock}, +}; + +use dioxus_core::{Component, ElementId, VirtualDom}; +use dioxus_html::EventData; +use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt}; +use dioxus_native_core::prelude::*; + +pub use rink::{query::Query, Config, RenderingMode, Size, TuiContext}; +use rink::{render, Driver}; + +pub fn launch(app: Component<()>) { + launch_cfg(app, Config::default()) +} + +pub fn launch_cfg(app: Component<()>, cfg: Config) { + launch_cfg_with_props(app, (), cfg); +} + +pub fn launch_cfg_with_props(app: Component, props: Props, cfg: Config) { + render(cfg, |rdom, taffy, event_tx| { + let dioxus_state = { + let mut rdom = rdom.write().unwrap(); + DioxusState::create(&mut rdom) + }; + let dioxus_state = Rc::new(RwLock::new(dioxus_state)); + let mut vdom = VirtualDom::new_with_props(app, props) + .with_root_context(TuiContext::new(event_tx)) + .with_root_context(Query::new(rdom.clone(), taffy.clone())) + .with_root_context(DioxusElementToNodeId { + mapping: dioxus_state.clone(), + }); + let muts = vdom.rebuild(); + let mut rdom = rdom.write().unwrap(); + dioxus_state + .write() + .unwrap() + .apply_mutations(&mut rdom, muts); + DioxusRenderer { + vdom, + dioxus_state, + #[cfg(all(feature = "hot-reload", debug_assertions))] + hot_reload_rx: { + let (hot_reload_tx, hot_reload_rx) = + tokio::sync::mpsc::unbounded_channel::(); + dioxus_hot_reload::connect(move |msg| { + let _ = hot_reload_tx.send(msg); + }); + hot_reload_rx + }, + } + }) + .unwrap(); +} + +struct DioxusRenderer { + vdom: VirtualDom, + dioxus_state: Rc>, + #[cfg(all(feature = "hot-reload", debug_assertions))] + hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver, +} + +impl Driver for DioxusRenderer { + fn update(&mut self, rdom: &Arc>) { + let muts = self.vdom.render_immediate(); + { + let mut rdom = rdom.write().unwrap(); + self.dioxus_state + .write() + .unwrap() + .apply_mutations(&mut rdom, muts); + } + } + + fn handle_event( + &mut self, + rdom: &Arc>, + id: NodeId, + event: &str, + value: Rc, + bubbles: bool, + ) { + let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() }; + if let Some(id) = id { + self.vdom + .handle_event(event, value.deref().clone().into_any(), id, bubbles); + } + } + + fn poll_async(&mut self) -> std::pin::Pin + '_>> { + #[cfg(all(feature = "hot-reload", debug_assertions))] + return Box::pin(async { + let hot_reload_wait = self.hot_reload_rx.recv(); + let mut hot_reload_msg = None; + let wait_for_work = self.vdom.wait_for_work(); + tokio::select! { + Some(msg) = hot_reload_wait => { + #[cfg(all(feature = "hot-reload", debug_assertions))] + { + hot_reload_msg = Some(msg); + } + #[cfg(not(all(feature = "hot-reload", debug_assertions)))] + let () = msg; + } + _ = wait_for_work => {} + } + // if we have a new template, replace the old one + if let Some(msg) = hot_reload_msg { + match msg { + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { + self.vdom.replace_template(template); + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + std::process::exit(0); + } + } + } + }); + + #[cfg(not(all(feature = "hot-reload", debug_assertions)))] + Box::pin(self.vdom.wait_for_work()) + } +} + +#[derive(Clone)] +pub struct DioxusElementToNodeId { + mapping: Rc>, +} + +impl DioxusElementToNodeId { + pub fn get_node_id(&self, element_id: ElementId) -> Option { + self.mapping + .read() + .unwrap() + .try_element_to_node_id(element_id) + } +} diff --git a/packages/tui/src/prelude/mod.rs b/packages/dioxus-tui/src/prelude/mod.rs similarity index 55% rename from packages/tui/src/prelude/mod.rs rename to packages/dioxus-tui/src/prelude/mod.rs index 2155f7707..cb4fdee65 100644 --- a/packages/tui/src/prelude/mod.rs +++ b/packages/dioxus-tui/src/prelude/mod.rs @@ -1 +1,2 @@ pub use crate::widgets::*; +pub use rink::Config; diff --git a/packages/tui/src/widgets/button.rs b/packages/dioxus-tui/src/widgets/button.rs similarity index 98% rename from packages/tui/src/widgets/button.rs rename to packages/dioxus-tui/src/widgets/button.rs index f3ebc7901..a6578905e 100644 --- a/packages/tui/src/widgets/button.rs +++ b/packages/dioxus-tui/src/widgets/button.rs @@ -37,7 +37,7 @@ pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> { } state.set(new_state); }; - cx.render(rsx! { + render! { div{ width: "{width}", height: "{height}", @@ -55,5 +55,5 @@ pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> { }, "{text}" } - }) + } } diff --git a/packages/tui/src/widgets/checkbox.rs b/packages/dioxus-tui/src/widgets/checkbox.rs similarity index 98% rename from packages/tui/src/widgets/checkbox.rs rename to packages/dioxus-tui/src/widgets/checkbox.rs index 483117212..0b5ebf671 100644 --- a/packages/tui/src/widgets/checkbox.rs +++ b/packages/dioxus-tui/src/widgets/checkbox.rs @@ -61,7 +61,7 @@ pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> { } state.set(new_state); }; - cx.render(rsx! { + render! { div { width: "{width}", height: "{height}", @@ -78,5 +78,5 @@ pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> { }, "{text}" } - }) + } } diff --git a/packages/tui/src/widgets/input.rs b/packages/dioxus-tui/src/widgets/input.rs similarity index 100% rename from packages/tui/src/widgets/input.rs rename to packages/dioxus-tui/src/widgets/input.rs diff --git a/packages/dioxus-tui/src/widgets/mod.rs b/packages/dioxus-tui/src/widgets/mod.rs new file mode 100644 index 000000000..18e3b12b4 --- /dev/null +++ b/packages/dioxus-tui/src/widgets/mod.rs @@ -0,0 +1,22 @@ +mod button; +mod checkbox; +mod input; +mod number; +mod password; +mod slider; +mod textbox; + +use dioxus_core::{RenderReturn, Scope}; +use dioxus_native_core::NodeId; +pub use input::*; + +use crate::DioxusElementToNodeId; + +pub(crate) fn get_root_id(cx: Scope) -> Option { + if let RenderReturn::Ready(sync) = cx.root_node() { + let mapping: DioxusElementToNodeId = cx.consume_context()?; + mapping.get_node_id(sync.root_ids.get(0)?) + } else { + None + } +} diff --git a/packages/tui/src/widgets/number.rs b/packages/dioxus-tui/src/widgets/number.rs similarity index 95% rename from packages/tui/src/widgets/number.rs rename to packages/dioxus-tui/src/widgets/number.rs index 2df1f512b..f82732981 100644 --- a/packages/tui/src/widgets/number.rs +++ b/packages/dioxus-tui/src/widgets/number.rs @@ -1,11 +1,11 @@ use crate::widgets::get_root_id; -use crate::Query; use crossterm::{cursor::MoveTo, execute}; use dioxus::prelude::*; use dioxus_elements::input_data::keyboard_types::Key; use dioxus_html as dioxus_elements; use dioxus_html::FormData; use dioxus_native_core::utils::cursor::{Cursor, Pos}; +use rink::Query; use std::{collections::HashMap, io::stdout}; use taffy::geometry::Point; @@ -40,8 +40,8 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a let dragging = use_state(cx, || false); let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&text); - let end_highlight = cursor.read().last().idx(&text); + let start_highlight = cursor.read().first().idx(&*text); + let end_highlight = cursor.read().last().idx(&*text); let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); let (text_highlighted, text_after_second_cursor) = text_after_first_cursor.split_at(end_highlight - start_highlight); @@ -99,7 +99,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a update(text.clone()); }; - cx.render(rsx! { + render! { div{ width: "{width}", height: "{height}", @@ -113,7 +113,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a }; if is_text{ let mut text = text_ref.write(); - cursor.write().handle_input(&k, &mut text, max_len); + cursor.write().handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len); update(text.clone()); let node = tui_query.get(get_root_id(cx).unwrap()); @@ -165,7 +165,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a } new.row = 0; - new.realize_col(&text_ref.read()); + new.realize_col(text_ref.read().as_str()); cursor.set(Cursor::from_start(new)); dragging.set(true); let node = tui_query_clone.get(get_root_id(cx).unwrap()); @@ -205,5 +205,5 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a "{text_after_second_cursor}" } - }) + } } diff --git a/packages/tui/src/widgets/password.rs b/packages/dioxus-tui/src/widgets/password.rs similarity index 94% rename from packages/tui/src/widgets/password.rs rename to packages/dioxus-tui/src/widgets/password.rs index f82d0a346..07491c140 100644 --- a/packages/tui/src/widgets/password.rs +++ b/packages/dioxus-tui/src/widgets/password.rs @@ -1,11 +1,11 @@ use crate::widgets::get_root_id; -use crate::Query; use crossterm::{cursor::*, execute}; use dioxus::prelude::*; use dioxus_elements::input_data::keyboard_types::Key; use dioxus_html as dioxus_elements; use dioxus_html::FormData; use dioxus_native_core::utils::cursor::{Cursor, Pos}; +use rink::Query; use std::{collections::HashMap, io::stdout}; use taffy::geometry::Point; @@ -40,8 +40,8 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { let dragging = use_state(cx, || false); let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&text); - let end_highlight = cursor.read().last().idx(&text); + let start_highlight = cursor.read().first().idx(&*text); + let end_highlight = cursor.read().last().idx(&*text); let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); let (text_highlighted, text_after_second_cursor) = text_after_first_cursor.split_at(end_highlight - start_highlight); @@ -88,7 +88,9 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { return; } let mut text = text_ref.write(); - cursor.write().handle_input(&k, &mut text, max_len); + cursor + .write() + .handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len); if let Some(input_handler) = &cx.props.raw_oninput { input_handler.call(FormData { value: text.clone(), @@ -114,7 +116,7 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { } }; - cx.render(rsx! { + render! { div { width: "{width}", height: "{height}", @@ -147,7 +149,7 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { // textboxs are only one line tall new.row = 0; - new.realize_col(&text_ref.read()); + new.realize_col(text_ref.read().as_str()); cursor.set(Cursor::from_start(new)); dragging.set(true); let node = tui_query_clone.get(get_root_id(cx).unwrap()); @@ -187,5 +189,5 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { "{text_after_second_cursor}" } - }) + } } diff --git a/packages/tui/src/widgets/slider.rs b/packages/dioxus-tui/src/widgets/slider.rs similarity index 98% rename from packages/tui/src/widgets/slider.rs rename to packages/dioxus-tui/src/widgets/slider.rs index 0d5dbca13..9e8d63cb9 100644 --- a/packages/tui/src/widgets/slider.rs +++ b/packages/dioxus-tui/src/widgets/slider.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use crate::widgets::get_root_id; -use crate::Query; use dioxus::prelude::*; use dioxus_elements::input_data::keyboard_types::Key; use dioxus_html as dioxus_elements; use dioxus_html::FormData; +use rink::Query; #[derive(Props)] pub(crate) struct SliderProps<'a> { @@ -62,7 +62,7 @@ pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> { } }; - cx.render(rsx! { + render! { div{ width: "{width}", height: "{height}", @@ -103,5 +103,5 @@ pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> { background_color: "rgba(10,10,10,0.5)", } } - }) + } } diff --git a/packages/tui/src/widgets/textbox.rs b/packages/dioxus-tui/src/widgets/textbox.rs similarity index 94% rename from packages/tui/src/widgets/textbox.rs rename to packages/dioxus-tui/src/widgets/textbox.rs index 4a58725bc..fcfaf0a90 100644 --- a/packages/tui/src/widgets/textbox.rs +++ b/packages/dioxus-tui/src/widgets/textbox.rs @@ -1,11 +1,11 @@ use crate::widgets::get_root_id; -use crate::Query; use crossterm::{cursor::*, execute}; use dioxus::prelude::*; use dioxus_elements::input_data::keyboard_types::Key; use dioxus_html as dioxus_elements; use dioxus_html::FormData; use dioxus_native_core::utils::cursor::{Cursor, Pos}; +use rink::Query; use std::{collections::HashMap, io::stdout}; use taffy::geometry::Point; @@ -40,8 +40,8 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { let dragging = use_state(cx, || false); let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&text); - let end_highlight = cursor.read().last().idx(&text); + let start_highlight = cursor.read().first().idx(&*text); + let end_highlight = cursor.read().last().idx(&*text); let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); let (text_highlighted, text_after_second_cursor) = text_after_first_cursor.split_at(end_highlight - start_highlight); @@ -79,7 +79,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { "solid" }; - cx.render(rsx! { + render! { div{ width: "{width}", height: "{height}", @@ -90,7 +90,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { return; } let mut text = text_ref.write(); - cursor.write().handle_input(&k, &mut text, max_len); + cursor.write().handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len); if let Some(input_handler) = &cx.props.raw_oninput{ input_handler.call(FormData{ value: text.clone(), @@ -138,7 +138,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { // textboxs are only one line tall new.row = 0; - new.realize_col(&text_ref.read()); + new.realize_col(text_ref.read().as_str()); cursor.set(Cursor::from_start(new)); dragging.set(true); let node = tui_query_clone.get(get_root_id(cx).unwrap()); @@ -178,5 +178,5 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { "{text_after_second_cursor}" } - }) + } } diff --git a/packages/tui/test.html b/packages/dioxus-tui/test.html similarity index 100% rename from packages/tui/test.html rename to packages/dioxus-tui/test.html diff --git a/packages/tui/tests/events.rs b/packages/dioxus-tui/tests/events.rs similarity index 100% rename from packages/tui/tests/events.rs rename to packages/dioxus-tui/tests/events.rs diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 801ef7ac5..bc2098285 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.2" +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,20 +9,24 @@ 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.3", optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +dioxus-hot-reload = { path = "../hot-reload", version = "0.1.0", optional = true } [features] -default = ["macro", "hooks", "html"] +default = ["macro", "hooks", "html", "hot-reload"] macro = ["dioxus-core-macro", "dioxus-rsx"] html = ["dioxus-html"] hooks = ["dioxus-hooks"] +hot-reload = ["dioxus-hot-reload"] [dev-dependencies] diff --git a/notes/README.md b/packages/dioxus/README.md similarity index 95% rename from notes/README.md rename to packages/dioxus/README.md index 2e5a03b1e..31fff88fa 100644 --- a/notes/README.md +++ b/packages/dioxus/README.md @@ -9,9 +9,8 @@ This overview provides a brief introduction to Dioxus. For a more in-depth guide, make sure to check out: -- [Getting Started](https://dioxuslabs.com/guide/setup.html) -- [Book](https://dioxuslabs.com/guide/) -- [Reference](https://dioxuslabs.com/reference) +- [Getting Started](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/index.html) +- [Book](https://dioxuslabs.com/docs/0.3/guide/en/) - [Examples](https://github.com/DioxusLabs/example-projects) # Overview and Goals @@ -229,7 +228,7 @@ of logic. Hooks provide us a way of retrieving state from the `Scope` and using it to render UI elements. By convention, all hooks are functions that should start with `use_`. We can -use hooks to define state and modify it from within listeners. +use hooks to define the state and modify it from within listeners. ```rust, ignore fn app(cx: Scope) -> Element { @@ -249,7 +248,7 @@ In a sense, hooks let us add a field of state to our component without declaring an explicit state struct. However, this means we need to "load" the struct in the right order. If that order is wrong, then the hook will pick the wrong state and panic. -Most hooks you'll write are simply composition of other hooks: +Most hooks you'll write are simply compositions of other hooks: ```rust, ignore fn use_username(cx: &ScopeState, id: Uuid) -> bool { @@ -309,7 +308,7 @@ Beyond this overview, Dioxus supports: Good luck! -## Inspiration, Resources, Alternatives and Credits +## Inspiration, Resources, Alternatives, and Credits Dioxus is inspired by: - React: for its hooks, concurrency, suspense @@ -318,7 +317,7 @@ Dioxus is inspired by: Alternatives to Dioxus include: - Yew: supports function components and web, but no SSR, borrowed data, or bump allocation. Rather slow at times. - Percy: supports function components, web, ssr, but lacks state management -- Sycamore: supports function components, web, ssr, but closer to SolidJS than React +- Sycamore: supports function components, web, ssr, but is closer to SolidJS than React - MoonZoom/Seed: opinionated frameworks based on the Elm model (message, update) - no hooks We've put a lot of work into making Dioxus ergonomic and *familiar*. diff --git a/packages/dioxus/benches/jsframework.rs b/packages/dioxus/benches/jsframework.rs index 24ca2fe69..b3405b2bd 100644 --- a/packages/dioxus/benches/jsframework.rs +++ b/packages/dioxus/benches/jsframework.rs @@ -42,7 +42,7 @@ fn create_rows(c: &mut Criterion) { c.bench_function("create rows", |b| { let mut dom = VirtualDom::new(app); - dom.rebuild(); + let _ = dom.rebuild(); b.iter(|| { let g = dom.rebuild(); diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index fed3d2bd5..5d1371f27 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -31,4 +31,7 @@ pub mod prelude { #[cfg(feature = "html")] pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes}; + + #[cfg(all(not(target_arch = "wasm32"), feature = "hot-reload"))] + pub use dioxus_hot_reload::{self, hot_reload_init}; } diff --git a/packages/fermi/Cargo.toml b/packages/fermi/Cargo.toml index 05ae2ec21..9591a4f25 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" @@ -8,12 +8,12 @@ license = "MIT/Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" -keywords = ["dom", "ui", "gui", "react", "wasm"] +keywords = ["dom", "ui", "gui", "react", "state-management"] # 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/fermi/README.md b/packages/fermi/README.md index 711b1a43a..f04ec5961 100644 --- a/packages/fermi/README.md +++ b/packages/fermi/README.md @@ -40,9 +40,9 @@ Inspired by atom-based state management solutions, all state in Fermi starts as static NAME: Atom<&str> = |_| "Dioxus"; ``` -From anywhere in our app, we can read our the value of our atom: +From anywhere in our app, we can read the value of our atom: -```rust, ignore +```rust, ignores fn NameCard(cx: Scope) -> Element { let name = use_read(cx, NAME); cx.render(rsx!{ h1 { "Hello, {name}"} }) @@ -63,6 +63,26 @@ fn NameCard(cx: Scope) -> Element { } ``` +If needed, we can update the atom's value, based on itself: + +```rust, ignore +static COUNT: Atom = |_| 0; + +fn Counter(cx: Scope) -> Element { + let mut count = use_atom_state(cx, COUNT); + + cx.render(rsx!{ + p { + "{count}" + } + button { + onclick: move |_| count += 1, + "Increment counter" + } + }) +} +``` + It's that simple! ## Installation @@ -83,9 +103,9 @@ $ cargo run --example fermi ## Features -Broadly our feature set to required to be released includes: +Broadly our feature set required to be released includes: - [x] Support for Atoms -- [x] Support for AtomRef (for values that aren't clone) +- [x] Support for AtomRef (for values that aren't `Clone`) - [ ] Support for Atom Families - [ ] Support for memoized Selectors - [ ] Support for memoized SelectorFamilies diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs index 68a5d5d33..99e97bfbb 100644 --- a/packages/fermi/src/root.rs +++ b/packages/fermi/src/root.rs @@ -13,8 +13,8 @@ use crate::Readable; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct AtomId { - pub(crate) ptr: *const (), - pub(crate) type_id: TypeId, + pub ptr: *const (), + pub type_id: TypeId, } pub struct AtomRoot { diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index a9cfa340d..c0e1ea783 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" @@ -8,15 +8,16 @@ license = "MIT/Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" -keywords = ["dom", "ui", "gui", "react", "wasm"] +keywords = ["dom", "ui", "gui", "react"] # 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" } +dioxus = { path = "../../packages/dioxus", version = "^0.3.0" } diff --git a/packages/hooks/README.md b/packages/hooks/README.md index 14c0e4209..6c77867de 100644 --- a/packages/hooks/README.md +++ b/packages/hooks/README.md @@ -1,22 +1,58 @@ -# Common hooks for Dioxus +# Dioxus Hooks -This crate includes some basic useful hooks for dioxus: + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-hooks.svg +[crates-url]: https://crates.io/crates/dioxus-hooks + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) | +[API Docs](https://docs.rs/dioxus-hooks/latest/dioxus_hooks) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +`dioxus-hooks` includes some basic useful hooks for dioxus: - use_state - use_ref - use_future - use_coroutine +- use_callback -## use_state +Unlike React, none of these hooks are foundational since they all build off the primitive `cx.use_hook`. -The primary mechanism of stored state. +This crate also provides a few helpful macros to get around some Rust lifetime management issues in async. -You can always use it "normally" with the `split` method: +- `to_owned![]` +- `use_future!()` +- `use_callback!()` -```rust -// Rusty-smart-pointer usage: -let value = use_state(cx, || 10); -// "Classic" usage: -let (value, set_value) = use_state(cx, || 0).split(); -``` +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT without any additional +terms or conditions. diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 583eccb37..1e718eda3 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -44,3 +44,6 @@ pub use useeffect::*; mod usecallback; pub use usecallback::*; + +mod usememo; +pub use usememo::*; diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index a355ae42a..eb836a211 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -1,6 +1,6 @@ use dioxus_core::{ScopeId, ScopeState}; use std::{ - cell::{Cell, Ref, RefCell, RefMut}, + cell::{Ref, RefCell, RefMut}, collections::HashSet, rc::Rc, sync::Arc, @@ -9,8 +9,8 @@ use std::{ type ProvidedState = Rc>>; // Tracks all the subscribers to a shared State -pub struct ProvidedStateInner { - value: Rc>, +pub(crate) struct ProvidedStateInner { + value: T, notify_any: Arc, consumers: HashSet, } @@ -21,14 +21,6 @@ impl ProvidedStateInner { (self.notify_any)(*consumer); } } - - pub fn write(&self) -> RefMut { - self.value.borrow_mut() - } - - pub fn read(&self) -> Ref { - self.value.borrow() - } } /// This hook provides some relatively light ergonomics around shared state. @@ -38,18 +30,57 @@ impl ProvidedStateInner { /// /// # Example /// -/// ## Provider +/// ```rust +/// # use dioxus::prelude::*; +/// # +/// # fn app(cx: Scope) -> Element { +/// # render! { +/// # Parent{} +/// # } +/// # } /// -/// ```rust, ignore +/// #[derive(Clone, Copy)] +/// enum Theme { +/// Light, +/// Dark, +/// } /// +/// // Provider +/// fn Parent<'a>(cx: Scope<'a>) -> Element<'a> { +/// use_shared_state_provider(cx, || Theme::Dark); +/// let theme = use_shared_state::(cx).unwrap(); /// -/// ``` -/// -/// ## Consumer -/// -/// ```rust, ignore +/// render! { +/// button{ +/// onclick: move |_| { +/// let current_theme = *theme.read(); +/// *theme.write() = match current_theme { +/// Theme::Dark => Theme::Light, +/// Theme::Light => Theme::Dark, +/// }; +/// }, +/// "Change theme" +/// } +/// Child{} +/// } +/// } /// +/// // Consumer +/// fn Child<'a>(cx: Scope<'a>) -> Element<'a> { +/// let theme = use_shared_state::(cx).unwrap(); +/// let current_theme = *theme.read(); /// +/// render! { +/// match &*theme.read() { +/// Theme::Dark => { +/// "Dark mode" +/// } +/// Theme::Light => { +/// "Light mode" +/// } +/// } +/// } +/// } /// ``` /// /// # How it works @@ -57,118 +88,126 @@ impl ProvidedStateInner { /// Any time a component calls `write`, every consumer of the state will be notified - excluding the provider. /// /// Right now, there is not a distinction between read-only and write-only, so every consumer will be notified. -/// -/// -/// -pub fn use_shared_state(cx: &ScopeState) -> Option> { - let state = cx.use_hook(|| { +pub fn use_shared_state(cx: &ScopeState) -> Option<&UseSharedState> { + let state: &Option> = &*cx.use_hook(move || { let scope_id = cx.scope_id(); - let root = cx.consume_context::>(); + let root = cx.consume_context::>()?; - if let Some(root) = root.as_ref() { - root.borrow_mut().consumers.insert(scope_id); - } + root.borrow_mut().consumers.insert(scope_id); - let value = root.as_ref().map(|f| f.borrow().value.clone()); - SharedStateInner { - root, - value, - scope_id, - needs_notification: Cell::new(false), - } + let state = UseSharedState { inner: root }; + let owner = UseSharedStateOwner { state, scope_id }; + Some(owner) }); - - state.needs_notification.set(false); - match (&state.value, &state.root) { - (Some(value), Some(root)) => Some(UseSharedState { - cx, - value, - root, - needs_notification: &state.needs_notification, - }), - _ => None, - } + state.as_ref().map(|s| &s.state) } -struct SharedStateInner { - root: Option>, - value: Option>>, +/// This wrapper detects when the hook is dropped and will unsubscribe when the component is unmounted +struct UseSharedStateOwner { + state: UseSharedState, scope_id: ScopeId, - needs_notification: Cell, } -impl Drop for SharedStateInner { + +impl Drop for UseSharedStateOwner { fn drop(&mut self) { - // we need to unsubscribe when our component is unounted - if let Some(root) = &self.root { - let mut root = root.borrow_mut(); - root.consumers.remove(&self.scope_id); - } + // we need to unsubscribe when our component is unmounted + let mut root = self.state.inner.borrow_mut(); + root.consumers.remove(&self.scope_id); } } -pub struct UseSharedState<'a, T: 'static> { - pub(crate) cx: &'a ScopeState, - pub(crate) value: &'a Rc>, - pub(crate) root: &'a Rc>>, - pub(crate) needs_notification: &'a Cell, +/// State that is shared between components through the context system +pub struct UseSharedState { + pub(crate) inner: Rc>>, } -impl<'a, T: 'static> UseSharedState<'a, T> { +impl UseSharedState { + /// Notify all consumers of the state that it has changed. (This is called automatically when you call "write") + pub fn notify_consumers(&self) { + self.inner.borrow_mut().notify_consumers(); + } + + /// Read the shared value pub fn read(&self) -> Ref<'_, T> { - self.value.borrow() - } - - pub fn notify_consumers(self) { - if !self.needs_notification.get() { - self.root.borrow_mut().notify_consumers(); - self.needs_notification.set(true); - } - } - - pub fn read_write(&self) -> (Ref<'_, T>, &Self) { - (self.read(), self) + Ref::map(self.inner.borrow(), |inner| &inner.value) } /// Calling "write" will force the component to re-render /// /// - /// TODO: We prevent unncessary notifications only in the hook, but we should figure out some more global lock + // TODO: We prevent unncessary notifications only in the hook, but we should figure out some more global lock pub fn write(&self) -> RefMut<'_, T> { - self.cx.needs_update(); - self.notify_consumers(); - self.value.borrow_mut() + let mut value = self.inner.borrow_mut(); + value.notify_consumers(); + RefMut::map(value, |inner| &mut inner.value) } /// Allows the ability to write the value without forcing a re-render pub fn write_silent(&self) -> RefMut<'_, T> { - self.value.borrow_mut() - } - - pub fn inner(&self) -> Rc>> { - self.root.clone() + RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.value) } } -impl Copy for UseSharedState<'_, T> {} -impl<'a, T> Clone for UseSharedState<'a, T> -where - T: 'static, -{ +impl Clone for UseSharedState { fn clone(&self) -> Self { - UseSharedState { - cx: self.cx, - value: self.value, - root: self.root, - needs_notification: self.needs_notification, + Self { + inner: self.inner.clone(), } } } -/// Provide some state for components down the hierarchy to consume without having to drill props. +impl PartialEq for UseSharedState { + fn eq(&self, other: &Self) -> bool { + let first = self.inner.borrow(); + let second = other.inner.borrow(); + first.value == second.value + } +} + +/// Provide some state for components down the hierarchy to consume without having to drill props. See [`use_shared_state`] to consume the state +/// +/// +/// # Example +/// +/// ```rust +/// # use dioxus::prelude::*; +/// # +/// # fn app(cx: Scope) -> Element { +/// # render! { +/// # Parent{} +/// # } +/// # } +/// +/// #[derive(Clone, Copy)] +/// enum Theme { +/// Light, +/// Dark, +/// } +/// +/// // Provider +/// fn Parent<'a>(cx: Scope<'a>) -> Element<'a> { +/// use_shared_state_provider(cx, || Theme::Dark); +/// let theme = use_shared_state::(cx).unwrap(); +/// +/// render! { +/// button{ +/// onclick: move |_| { +/// let current_theme = *theme.read(); +/// *theme.write() = match current_theme { +/// Theme::Dark => Theme::Light, +/// Theme::Light => Theme::Dark, +/// }; +/// }, +/// "Change theme" +/// } +/// // Children components that consume the state... +/// } +/// } +/// ``` pub fn use_shared_state_provider(cx: &ScopeState, f: impl FnOnce() -> T) { cx.use_hook(|| { let state: ProvidedState = Rc::new(RefCell::new(ProvidedStateInner { - value: Rc::new(RefCell::new(f())), + value: f(), notify_any: cx.schedule_update_any(), consumers: HashSet::new(), })); diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 8c735bd80..bc97735b9 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -108,6 +108,12 @@ impl Coroutine { } } +impl PartialEq for Coroutine { + fn eq(&self, other: &Self) -> bool { + self.task == other.task + } +} + #[cfg(test)] mod tests { #![allow(unused)] @@ -120,7 +126,7 @@ mod tests { fn app(cx: Scope, name: String) -> Element { let task = use_coroutine(cx, |mut rx: UnboundedReceiver| async move { while let Some(msg) = rx.next().await { - println!("got message: {}", msg); + println!("got message: {msg}"); } }); @@ -133,7 +139,7 @@ mod tests { async fn view_task(mut rx: UnboundedReceiver) { while let Some(msg) = rx.next().await { - println!("got message: {}", msg); + println!("got message: {msg}"); } } diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index df34a6433..f540e0a6e 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -143,22 +143,21 @@ impl UseFuture { self.task.get() } - /// Get the current stateof the future. + /// Get the current state of the future. pub fn state(&self) -> UseFutureState { - todo!() - // match (&self.task.get(), &self.value) { - // // If we have a task and an existing value, we're reloading - // (Some(_), Some(val)) => UseFutureState::Reloading(val), + match (&self.task.get(), &self.value()) { + // If we have a task and an existing value, we're reloading + (Some(_), Some(val)) => UseFutureState::Reloading(val), - // // no task, but value - we're done - // (None, Some(val)) => UseFutureState::Complete(val), + // no task, but value - we're done + (None, Some(val)) => UseFutureState::Complete(val), - // // no task, no value - something's wrong? return pending - // (None, None) => UseFutureState::Pending, + // no task, no value - something's wrong? return pending + (None, None) => UseFutureState::Pending, - // // Task, no value - we're still pending - // (Some(_), None) => UseFutureState::Pending, - // } + // Task, no value - we're still pending + (Some(_), None) => UseFutureState::Pending, + } } } diff --git a/packages/hooks/src/usememo.rs b/packages/hooks/src/usememo.rs new file mode 100644 index 000000000..8cc5540e6 --- /dev/null +++ b/packages/hooks/src/usememo.rs @@ -0,0 +1,37 @@ +use dioxus_core::ScopeState; + +use crate::UseFutureDep; + +/// A hook that provides a callback that executes after the hooks have been applied +/// +/// Whenever the hooks dependencies change, the callback will be re-evaluated. +/// +/// - dependencies: a tuple of references to values that are PartialEq + Clone +/// +/// ## Examples +/// +/// ```rust, ignore +/// +/// #[inline_props] +/// fn app(cx: Scope, name: &str) -> Element { +/// use_memo(cx, (name,), |(name,)| { +/// expensive_computation(name); +/// })) +/// } +/// ``` +pub fn use_memo(cx: &ScopeState, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T +where + T: 'static, + D: UseFutureDep, +{ + let value = cx.use_hook(|| None); + + let dependancies_vec = cx.use_hook(Vec::new); + + if dependencies.clone().apply(dependancies_vec) || value.is_none() { + // Create the new value + *value = Some(callback(dependencies.out())); + } + + value.as_ref().unwrap() +} 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/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 2e1aa7295..d2b6db6fc 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -329,7 +329,7 @@ impl PartialEq for &UseState { } } -impl PartialEq> for UseState { +impl PartialEq> for UseState { fn eq(&self, other: &UseState) -> bool { Rc::ptr_eq(&self.current_val, &other.current_val) } diff --git a/packages/hot-reload/Cargo.toml b/packages/hot-reload/Cargo.toml new file mode 100644 index 000000000..2d88bf029 --- /dev/null +++ b/packages/hot-reload/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "dioxus-hot-reload" +version = "0.1.1" +edition = "2021" +license = "MIT/Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +description = "Hot reloading utilites for Dioxus" +documentation = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react", "hot-reloading"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-rsx = { path = "../rsx" , version = "^0.0.3" } +dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.2"} +dioxus-html = { path = "../html", features = ["hot-reload-context"], version = "^0.3.0" } + +interprocess = { version = "1.2.1" } +notify = "5.0.0" +chrono = "0.4.23" +serde_json = "1.0.91" +serde = { version = "1", features = ["derive"] } +execute = "0.2.11" +once_cell = "1.17.0" +ignore = "0.4.19" diff --git a/packages/hot-reload/README.md b/packages/hot-reload/README.md new file mode 100644 index 000000000..60b7b9c87 --- /dev/null +++ b/packages/hot-reload/README.md @@ -0,0 +1,170 @@ +# `dioxus-hot-reload`: Hot Reloading Utilites for Dioxus + + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-hot-reload.svg +[crates-url]: https://crates.io/crates/dioxus-hot-reload + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en) | +[API Docs](https://docs.rs/dioxus-hot-reload/latest/dioxus_hot_reload) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +Dioxus supports hot reloading for static parts of rsx macros. This enables changing the styling of your application without recompiling the rust code. This is useful for rapid iteration on the styling of your application. + + +Hot reloading could update the following change without recompiling: +```rust +rsx! { + div { + "Count: {count}", + } +} +``` +=> +```rust +rsx! { + div { + color: "red", + font_size: "2em", + "Count: {count}", + } +} +``` + +But it could not update the following change: +```rust +rsx! { + div { + "Count: {count}", + } +} +``` +=> +```rust +rsx! { + div { + "Count: {count*2}", + onclick: |_| println!("clicked"), + } +} +``` + +## Usage + +> This crate implements hot reloading for native compilation targets not WASM. For hot relaoding with the web renderer, see the [dioxus-cli](https://github.com/DioxusLabs/cli) project. + +Add this to the top of your main function on any renderer that supports hot reloading to start the hot reloading server: + +```rust +fn main(){ + hot_reload_init!(); + // launch your application +} +``` + +By default the dev server watches on the root of the crate the macro is called in and ignores changes in the `/target` directory and any directories set in the `.gitignore` file in the root directory. To watch on custom paths pass call the `with_paths` function on the config builder: + +```rust +fn main(){ + hot_reload_init!(Config::new().with_paths(&["src", "examples", "assets"])); + // launch your application +} +``` + +By default the hot reloading server will output some logs in the console, to disable these logs call the `with_logging` function on the config builder: + +```rust +fn main(){ + hot_reload_init!(Config::new().with_logging(false)); + // launch your application +} +``` + +To rebuild the application when the logic changes, you can use the `with_rebuild_command` function on the config builder. This command will be called when hot reloading fails to quickly update the rsx: + +```rust +fn main(){ + hot_reload_init!(Config::new().with_rebuild_command("cargo run")); + // launch your application +} +``` + +If you are using a namespace other than html, you can implement the [HotReloadingContext](https://docs.rs/dioxus-rsx/latest/dioxus_rsx/trait.HotReloadingContext.html) trait to provide a mapping between the rust names of your elements/attributes and the resulting strings. + +You can then provide the Context to the builder to make hot reloading work with your custom namespace: + +```rust +fn main(){ + // Note: Use default instead of new if you are using a custom namespace + hot_reload_init!(Config::::default()); + // launch your application +} +``` + +## Implementing Hot Reloading for a Custom Renderer + +To add hot reloading support to your custom renderer you can use the connect function. This will connect to the dev server you just need to provide a way to transfer `Template`s to the `VirtualDom`. Once you implement this your users can use the hot_reload_init function just like any other render. + +```rust +async fn launch(app: Component) { + let mut vdom = VirtualDom::new(app); + // ... + + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + dioxus_hot_reload::connect(move |msg| { + let _ = tx.send(msg); + }); + + loop { + tokio::select! { + Some(msg) = rx.recv() => { + match msg{ + HotReloadMsg::Shutdown => { + // ... shutdown the application + } + HotReloadMsg::UpdateTemplate(template) => { + // update the template in the virtual dom + vdom.replace_template(template); + } + } + } + _ = vdom.wait_for_work() => { + // ... + } + } + let mutations = vdom.render_immediate(); + // apply the mutations to the dom + } +} +``` + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT without any additional +terms or conditions. diff --git a/packages/hot-reload/src/lib.rs b/packages/hot-reload/src/lib.rs new file mode 100644 index 000000000..d144fccfc --- /dev/null +++ b/packages/hot-reload/src/lib.rs @@ -0,0 +1,383 @@ +use std::{ + io::{BufRead, BufReader, Write}, + path::PathBuf, + str::FromStr, + sync::{Arc, Mutex}, +}; + +use dioxus_core::Template; +use dioxus_rsx::{ + hot_reload::{FileMap, FileMapBuildResult, UpdateResult}, + HotReloadingContext, +}; +use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; + +pub use dioxus_html::HtmlCtx; +use serde::{Deserialize, Serialize}; + +/// A message the hot reloading server sends to the client +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub enum HotReloadMsg { + /// A template has been updated + #[serde(borrow = "'static")] + UpdateTemplate(Template<'static>), + /// The program needs to be recompiled, and the client should shut down + Shutdown, +} + +pub struct Config { + root_path: &'static str, + listening_paths: &'static [&'static str], + excluded_paths: &'static [&'static str], + log: bool, + rebuild_with: Option bool + Send + 'static>>, + phantom: std::marker::PhantomData, +} + +impl Default for Config { + fn default() -> Self { + Self { + root_path: "", + listening_paths: &[""], + excluded_paths: &["./target"], + log: true, + rebuild_with: None, + phantom: std::marker::PhantomData, + } + } +} + +impl Config { + pub const fn new() -> Self { + Self { + root_path: "", + listening_paths: &[""], + excluded_paths: &["./target"], + log: true, + rebuild_with: None, + phantom: std::marker::PhantomData, + } + } +} + +impl Config { + /// Set the root path of the project (where the Cargo.toml file is). This is automatically set by the [`hot_reload_init`] macro. + pub fn root(self, path: &'static str) -> Self { + Self { + root_path: path, + ..self + } + } + + /// Set whether to enable logs + pub fn with_logging(self, log: bool) -> Self { + Self { log, ..self } + } + + /// Set the command to run to rebuild the project + /// + /// For example to restart the application after a change is made, you could use `cargo run` + pub fn with_rebuild_command(self, rebuild_command: &'static str) -> Self { + self.with_rebuild_callback(move || { + execute::shell(rebuild_command) + .spawn() + .expect("Failed to spawn the rebuild command"); + true + }) + } + + /// Set a callback to run to when the project needs to be rebuilt and returns if the server should shut down + /// + /// For example a CLI application could rebuild the application when a change is made + pub fn with_rebuild_callback( + self, + rebuild_callback: impl FnMut() -> bool + Send + 'static, + ) -> Self { + Self { + rebuild_with: Some(Box::new(rebuild_callback)), + ..self + } + } + + /// Set the paths to listen for changes in to trigger hot reloading. If this is a directory it will listen for changes in all files in that directory recursively. + pub fn with_paths(self, paths: &'static [&'static str]) -> Self { + Self { + listening_paths: paths, + ..self + } + } + + /// Sets paths to ignore changes on. This will override any paths set in the [`Config::with_paths`] method in the case of conflicts. + pub fn excluded_paths(self, paths: &'static [&'static str]) -> Self { + Self { + excluded_paths: paths, + ..self + } + } +} + +/// Initialize the hot reloading listener +pub fn init(cfg: Config) { + let Config { + root_path, + listening_paths, + log, + mut rebuild_with, + excluded_paths, + phantom: _, + } = cfg; + + if let Ok(crate_dir) = PathBuf::from_str(root_path) { + // try to find the gitingore file + let gitignore_file_path = crate_dir.join(".gitignore"); + let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path); + + // convert the excluded paths to absolute paths + let excluded_paths = excluded_paths + .iter() + .map(|path| crate_dir.join(PathBuf::from(path))) + .collect::>(); + + let channels = Arc::new(Mutex::new(Vec::new())); + let FileMapBuildResult { + map: file_map, + errors, + } = FileMap::::create_with_filter(crate_dir.clone(), |path| { + // skip excluded paths + excluded_paths.iter().any(|p| path.starts_with(p)) || + // respect .gitignore + gitignore + .matched_path_or_any_parents(path, path.is_dir()) + .is_ignore() + }) + .unwrap(); + for err in errors { + if log { + println!("hot reloading failed to initialize:\n{err:?}"); + } + } + let file_map = Arc::new(Mutex::new(file_map)); + + if let Ok(local_socket_stream) = LocalSocketListener::bind("@dioxusin") { + let aborted = Arc::new(Mutex::new(false)); + + // listen for connections + std::thread::spawn({ + let file_map = file_map.clone(); + let channels = channels.clone(); + let aborted = aborted.clone(); + let _ = local_socket_stream.set_nonblocking(true); + move || { + loop { + if let Ok(mut connection) = local_socket_stream.accept() { + // send any templates than have changed before the socket connected + let templates: Vec<_> = { + file_map + .lock() + .unwrap() + .map + .values() + .filter_map(|(_, template_slot)| *template_slot) + .collect() + }; + for template in templates { + if !send_msg( + HotReloadMsg::UpdateTemplate(template), + &mut connection, + ) { + continue; + } + } + channels.lock().unwrap().push(connection); + if log { + println!("Connected to hot reloading 🚀"); + } + } + std::thread::sleep(std::time::Duration::from_millis(10)); + if *aborted.lock().unwrap() { + break; + } + } + } + }); + + // watch for changes + std::thread::spawn(move || { + let mut last_update_time = chrono::Local::now().timestamp(); + + let (tx, rx) = std::sync::mpsc::channel(); + + let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap(); + + for path in listening_paths { + let full_path = crate_dir.join(path); + if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) { + if log { + println!( + "hot reloading failed to start watching {full_path:?}:\n{err:?}", + ); + } + } + } + + let mut rebuild = { + let aborted = aborted.clone(); + let channels = channels.clone(); + move || { + if let Some(rebuild_callback) = &mut rebuild_with { + if log { + println!("Rebuilding the application..."); + } + let shutdown = rebuild_callback(); + + if shutdown { + *aborted.lock().unwrap() = true; + } + + for channel in &mut *channels.lock().unwrap() { + send_msg(HotReloadMsg::Shutdown, channel); + } + + return shutdown; + } else if log { + println!( + "Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes." + ); + } + true + } + }; + + for evt in rx { + if chrono::Local::now().timestamp() > last_update_time { + if let Ok(evt) = evt { + let real_paths = evt + .paths + .iter() + .filter(|path| { + // skip non rust files + matches!( + path.extension().and_then(|p| p.to_str()), + Some("rs" | "toml" | "css" | "html" | "js") + ) && + // skip excluded paths + !excluded_paths.iter().any(|p| path.starts_with(p)) && + // respect .gitignore + !gitignore + .matched_path_or_any_parents(path, false) + .is_ignore() + }) + .collect::>(); + + // Give time for the change to take effect before reading the file + if !real_paths.is_empty() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + let mut channels = channels.lock().unwrap(); + for path in real_paths { + // if this file type cannot be hot reloaded, rebuild the application + if path.extension().and_then(|p| p.to_str()) != Some("rs") + && rebuild() + { + return; + } + // find changes to the rsx in the file + match file_map + .lock() + .unwrap() + .update_rsx(path, crate_dir.as_path()) + { + Ok(UpdateResult::UpdatedRsx(msgs)) => { + for msg in msgs { + let mut i = 0; + while i < channels.len() { + let channel = &mut channels[i]; + if send_msg( + HotReloadMsg::UpdateTemplate(msg), + channel, + ) { + i += 1; + } else { + channels.remove(i); + } + } + } + } + Ok(UpdateResult::NeedsRebuild) => { + drop(channels); + if rebuild() { + return; + } + break; + } + Err(err) => { + if log { + println!( + "hot reloading failed to update rsx:\n{err:?}" + ); + } + } + } + } + } + last_update_time = chrono::Local::now().timestamp(); + } + } + }); + } + } +} + +fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool { + if let Ok(msg) = serde_json::to_string(&msg) { + if channel.write_all(msg.as_bytes()).is_err() { + return false; + } + if channel.write_all(&[b'\n']).is_err() { + return false; + } + true + } else { + false + } +} + +/// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected +pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) { + std::thread::spawn(move || { + if let Ok(socket) = LocalSocketStream::connect("@dioxusin") { + let mut buf_reader = BufReader::new(socket); + loop { + let mut buf = String::new(); + match buf_reader.read_line(&mut buf) { + Ok(_) => { + let template: HotReloadMsg = + serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap(); + f(template); + } + Err(err) => { + if err.kind() != std::io::ErrorKind::WouldBlock { + break; + } + } + } + } + } + }); +} + +/// Start the hot reloading server with the current directory as the root +#[macro_export] +macro_rules! hot_reload_init { + () => { + #[cfg(debug_assertions)] + dioxus_hot_reload::init(dioxus_hot_reload::Config::new().root(env!("CARGO_MANIFEST_DIR"))); + }; + + ($cfg: expr) => { + #[cfg(debug_assertions)] + dioxus_hot_reload::init($cfg.root(env!("CARGO_MANIFEST_DIR"))); + }; +} diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 3436dcc93..e60fcd8e5 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -1,17 +1,18 @@ [package] name = "dioxus-html" -version = "0.2.1" +version = "0.3.1" authors = ["Jonathan Kelley"] edition = "2018" description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" license = "MIT/Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" -documentation = "https://docs.rs/dioxus" -keywords = ["dom", "ui", "gui", "react", "wasm"] +documentation = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react"] [dependencies] -dioxus-core = { path = "../core", version = "^0.2.1" } +dioxus-core = { path = "../core", version = "^0.3.0" } +dioxus-rsx = { path = "../rsx", version = "^0.0.3", optional = true } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } wasm-bindgen = { version = "0.2.79", optional = true } @@ -41,9 +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"] diff --git a/packages/html/README.md b/packages/html/README.md index cbc03b0f2..59ec2f9ff 100644 --- a/packages/html/README.md +++ b/packages/html/README.md @@ -1,4 +1,30 @@ -# Html (and SVG) Namespace for Dioxus +# `dioxus-html`: Html (and SVG) Namespace for Dioxus + + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-html.svg +[crates-url]: https://crates.io/crates/dioxus-html + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) | +[API Docs](https://docs.rs/dioxus-html/latest/dioxus_html) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview The Dioxus `rsx!` and `html!` macros can accept any compile-time correct namespace on top of NodeFactory. This crate provides the HTML (and SVG) namespaces which get imported in the Dioxus prelude. @@ -35,7 +61,7 @@ impl DioxusElement for div { All elements should be defined as a zero-sized-struct (also known as unit struct). These structs are zero-cost and just provide the type-level trickery to Rust for compile-time correct templates. -Attributes would then be implemented as methods on these unit structs. +Attributes would then be implemented as constants on these unit structs. The HTML namespace is defined mostly with macros. However, the expanded form would look something like this: ```rust @@ -45,14 +71,8 @@ impl DioxusElement for base { const NAME_SPACE: Option<&'static str> = None; } impl base { - #[inline] - fn href<'a>(&self, f: NodeFactory<'a>, v: Arguments) -> Attribute<'a> { - f.attr("href", v, None, false) - } - #[inline] - fn target<'a>(&self, f: NodeFactory<'a>, v: Arguments) -> Attribute<'a> { - f.attr("target", v, None, false) - } + const href: (&'static str, Option<'static str>, bool) = ("href", None, false); + const target: (&'static str, Option<'static str>, bool) = ("target", None, false); } ``` Because attributes are defined as methods on the unit struct, they guard the attribute creation behind a compile-time correct interface. @@ -73,35 +93,20 @@ mod dioxus_elements { } ``` -## Limitations: -- -## How to work around it: -If an attribute in Dioxus is invalid (defined incorrectly) - first, make an issue - but then, you can work around it. The raw builder API is actually somewhat ergonomic to work with, and the NodeFactory type exposes a bunch of methods to make any type of tree - even invalid ones! So obviously, be careful, but there's basically anything you can do. -```rust -cx.render(rsx!{ - div { - h1 {} - // Oh no! I need a super custom element - {LazyNodes::new(move |f| { - f.raw_element( - // tag name - "custom_element", - // attributes - &[f.attr("billy", format_args!("goat"))], - // listeners - &[f.listener(onclick(move |_| {}))], +## Contributing - // children - &[cx.render(rsx!(div {} ))], +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! - // key - None - ) - })} - } -}) -``` +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT without any additional +terms or conditions. diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index efbb4d797..3fc400ea9 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1,61 +1,258 @@ #![allow(non_upper_case_globals)] - +#[cfg(feature = "hot-reload-context")] +use crate::{map_global_attributes, map_svg_attributes}; use crate::{GlobalAttributes, SvgAttributes}; +#[cfg(feature = "hot-reload-context")] +use dioxus_rsx::HotReloadingContext; pub type AttributeDiscription = (&'static str, Option<&'static str>, bool); +macro_rules! impl_attribute { + ( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident (DEFAULT), + ) => { + pub const $fil: AttributeDiscription = (stringify!($fil), None, false); + }; + + ( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident ($name:literal), + ) => { + pub const $fil: AttributeDiscription = ($name, None, false); + }; + + ( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident (volatile), + ) => { + pub const $fil: AttributeDiscription = (stringify!($fil), None, true); + }; + + ( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident (in $ns:ident), + ) => { + pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($ns)), false) + }; + + ( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident (in $ns:ident : volatile), + ) => { + pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($ns)), true) + }; +} + +#[cfg(feature = "hot-reload-context")] +macro_rules! impl_attribute_match { + ( + $attr:ident $fil:ident: $vil:ident (DEFAULT), + ) => { + if $attr == stringify!($fil) { + return Some((stringify!($fil), None)); + } + }; + + ( + $attr:ident $fil:ident: $vil:ident (volatile), + ) => { + if $attr == stringify!($fil) { + return Some((stringify!($fil), None)); + } + }; + + ( + $attr:ident $fil:ident: $vil:ident ($name:literal), + ) => { + if $attr == stringify!($fil) { + return Some(($name, None)); + } + }; + + ( + $attr:ident $fil:ident: $vil:ident (in $ns:ident), + ) => { + if $attr == stringify!($fil) { + return Some((stringify!(fil), Some(stringify!(ns)))); + } + }; +} + +macro_rules! impl_element { + ( + $(#[$attr:meta])* + $name:ident None { + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + #[allow(non_camel_case_types)] + $(#[$attr])* + pub struct $name; + + impl $name { + pub const TAG_NAME: &'static str = stringify!($name); + pub const NAME_SPACE: Option<&'static str> = None; + + $( + impl_attribute!( + $(#[$attr_method])* + $fil: $vil ($extra), + ); + )* + } + + impl GlobalAttributes for $name {} + }; + + ( + $(#[$attr:meta])* + $name:ident $namespace:tt { + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + #[allow(non_camel_case_types)] + $(#[$attr])* + pub struct $name; + + impl SvgAttributes for $name {} + + impl $name { + pub const TAG_NAME: &'static str = stringify!($name); + pub const NAME_SPACE: Option<&'static str> = Some($namespace); + + $( + impl_attribute!( + $(#[$attr_method])* + $fil: $vil in $namespace $extra + ); + )* + } + } +} + +#[cfg(feature = "hot-reload-context")] +macro_rules! impl_element_match { + ( + $el:ident $name:ident None { + $( + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + if $el == stringify!($name) { + return Some((stringify!($name), None)); + } + }; + + ( + $el:ident $name:ident $namespace:tt { + $( + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + if $el == stringify!($name) { + return Some((stringify!($name), Some(stringify!($namespace)))); + } + }; +} + +#[cfg(feature = "hot-reload-context")] +macro_rules! impl_element_match_attributes { + ( + $el:ident $attr:ident $name:ident None { + $( + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + if $el == stringify!($name) { + $( + impl_attribute_match!( + $attr $fil: $vil ($extra), + ); + )* + } + }; + + ( + $el:ident $attr:ident $name:ident $namespace:tt { + $( + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + if $el == stringify!($name) { + $( + impl_attribute_match!( + $attr $fil: $vil in $namespace $extra + ); + )* + } + } +} + macro_rules! builder_constructors { ( $( $(#[$attr:meta])* - $name:ident { + $name:ident $namespace:tt { $( $(#[$attr_method:meta])* - $fil:ident: $vil:ident, + $fil:ident: $vil:ident $extra:tt, )* }; )* - ) => { - $( - #[allow(non_camel_case_types)] - $(#[$attr])* - pub struct $name; - - - impl $name { - pub const TAG_NAME: &'static str = stringify!($name); - pub const NAME_SPACE: Option<&'static str> = None; + ) => { + #[cfg(feature = "hot-reload-context")] + pub struct HtmlCtx; + #[cfg(feature = "hot-reload-context")] + impl HotReloadingContext for HtmlCtx { + fn map_attribute(element: &str, attribute: &str) -> Option<(&'static str, Option<&'static str>)> { $( - pub const $fil: AttributeDiscription = (stringify!($fil), None, false); + impl_element_match_attributes!( + element attribute $name $namespace { + $( + $fil: $vil $extra, + )* + } + ); )* + map_global_attributes(attribute).or_else(|| map_svg_attributes(attribute)) } - impl GlobalAttributes for $name {} - )* - }; - - ( $( - $(#[$attr:meta])* - $name:ident <> $namespace:tt { - $($fil:ident: $vil:ident,)* - }; - )* ) => { - $( - #[allow(non_camel_case_types)] - $(#[$attr])* - pub struct $name; - - impl SvgAttributes for $name {} - - impl $name { - pub const TAG_NAME: &'static str = stringify!($name); - pub const NAME_SPACE: Option<&'static str> = Some($namespace); - + fn map_element(element: &str) -> Option<(&'static str, Option<&'static str>)> { $( - pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($namespace)), false) + impl_element_match!( + element $name $namespace { + $( + $fil: $vil $extra, + )* + } + ); )* + None } + } + + $( + impl_element!( + $(#[$attr])* + $name $namespace { + $( + $(#[$attr_method])* + $fil: $vil $extra, + )* + } + ); )* }; } @@ -75,63 +272,63 @@ builder_constructors! { /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) /// element. /// - base { - href: Uri, - target: Target, + base None { + href: Uri DEFAULT, + target: Target DEFAULT, }; /// Build a /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) /// element. - head {}; + head None {}; /// Build a /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) /// element. - link { + link None { // as: Mime, - crossorigin: CrossOrigin, - href: Uri, - hreflang: LanguageTag, - media: String, // FIXME media query - rel: LinkType, - sizes: String, // FIXME - title: String, // FIXME - r#type: Mime, - integrity: String, + crossorigin: CrossOrigin DEFAULT, + href: Uri DEFAULT, + hreflang: LanguageTag DEFAULT, + media: String DEFAULT, // FIXME media query + rel: LinkType DEFAULT, + sizes: String DEFAULT, // FIXME + title: String DEFAULT, // FIXME + r#type: Mime "type", + integrity: String DEFAULT, }; /// Build a /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) /// element. - meta { - charset: String, // FIXME IANA standard names - content: String, - http_equiv: HTTPEquiv, - name: Metadata, + meta None { + charset: String DEFAULT, // FIXME IANA standard names + content: String DEFAULT, + http_equiv: HTTPEquiv DEFAULT, + name: Metadata DEFAULT, }; /// Build a /// [` + + + +
+
+
+

Hello World

+

This is a test

+
+
+
+ + + + \ No newline at end of file diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index facdbedc0..2c91d8008 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" diff --git a/packages/router/README.md b/packages/router/README.md index d292e225b..0db79c6d6 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -1,52 +1,32 @@ -
-

Dioxus Router

-
+# Dioxus Router -
- - - Crates.io version - - - - Download - - - - docs.rs docs - - - - CI status - +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] - - - Awesome Page - - - - Discord Link - -
+[crates-badge]: https://img.shields.io/crates/v/dioxus-router.svg +[crates-url]: https://crates.io/crates/dioxus-router - +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE -Dioxus Router is a first-party Router for all your Dioxus Apps. It provides a React-Router style interface that works anywhere: across the browser, SSR, and natively. +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) | +[API Docs](https://docs.rs/dioxus-router/latest/dioxus_router) | +[Chat](https://discord.gg/XgGxMSkvUM) + +## Overview + +Dioxus Router is a first-party Router for all your Dioxus Apps. It provides an +interface similar to React Router, but takes advantage of types for more +expressiveness. ```rust ,no_run use dioxus::prelude::*; @@ -107,3 +87,19 @@ fn BlogPost(cx: Scope) -> Element { } } ``` + +You need to enable the right features for the platform you're targeting since these are not determined automatically! + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT without any additional +terms or conditions. diff --git a/packages/rsx-rosetta/Cargo.toml b/packages/rsx-rosetta/Cargo.toml new file mode 100644 index 000000000..d160d2bf9 --- /dev/null +++ b/packages/rsx-rosetta/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "rsx-rosetta" +version = "0.3.0" +edition = "2021" +authors = ["Jonathan Kelley"] +description = "Autofomatter for Dioxus RSX" +license = "MIT/Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-autofmt = { path = "../autofmt", version = "0.3.0" } +dioxus-rsx = { path = "../rsx" , version = "^0.0.3" } +html_parser = "0.6.3" +proc-macro2 = "1.0.49" +quote = "1.0.23" +syn = { version = "1.0.107", features = ["full"] } +convert_case = "0.5.0" + +# [features] +# default = ["html"] + +# eventually more output options diff --git a/packages/rsx-rosetta/README.md b/packages/rsx-rosetta/README.md new file mode 100644 index 000000000..da3049d3c --- /dev/null +++ b/packages/rsx-rosetta/README.md @@ -0,0 +1,59 @@ + + +# `rsx-rosetta` + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/rsx-rosetta.svg +[crates-url]: https://crates.io/crates/rsx-rosetta + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) | +[API Docs](https://docs.rs/rsx-rosetta/latest/rsx-rosetta) | +[Chat](https://discord.gg/XgGxMSkvUM) + +## Overview + +Dioxus sports its own templating language inspired by C#/Kotlin/RTMP, etc. It's pretty straightforward. + +However, it's NOT HTML. This is done since HTML is verbose and you'd need a dedicated LSP or IDE integration to get a good DX in .rs files. + +RSX is simple... It's similar enough to regular Rust code to trick most IDEs into automatically providing support for things like block selections, folding, highlighting, etc. + +To accomodate the transition from HTML to RSX, you might need to translate some existing code. + +This library provids a central AST that can accept a number of inputs: + +- HTML +- Syn (todo) +- Akama (todo) +- Jinja (todo) + +From there, you can convert directly to a string or into some other AST. + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT, without any additional +terms or conditions. + diff --git a/packages/rsx-rosetta/examples/html.rs b/packages/rsx-rosetta/examples/html.rs new file mode 100644 index 000000000..ff9af0ed7 --- /dev/null +++ b/packages/rsx-rosetta/examples/html.rs @@ -0,0 +1,24 @@ +use html_parser::Dom; + +fn main() { + let html = r#" +
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+ hello world! +
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::rsx_from_html(&dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + println!("{out}"); +} diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs new file mode 100644 index 000000000..64fed1e18 --- /dev/null +++ b/packages/rsx-rosetta/src/lib.rs @@ -0,0 +1,132 @@ +use convert_case::{Case, Casing}; +use dioxus_rsx::{ + BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, IfmtInput, +}; +pub use html_parser::{Dom, Node}; +use proc_macro2::{Ident, Span}; +use syn::{punctuated::Punctuated, LitStr}; + +/// Convert an HTML DOM tree into an RSX CallBody +pub fn rsx_from_html(dom: &Dom) -> CallBody { + CallBody { + roots: dom.children.iter().filter_map(rsx_node_from_html).collect(), + } +} + +/// Convert an HTML Node into an RSX BodyNode +/// +/// If the node is a comment, it will be ignored since RSX doesn't support comments +pub fn rsx_node_from_html(node: &Node) -> Option { + match node { + Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))), + Node::Element(el) => { + let el_name = el.name.to_case(Case::Snake); + let el_name = Ident::new(el_name.as_str(), Span::call_site()); + + let mut attributes: Vec<_> = el + .attributes + .iter() + .map(|(name, value)| { + let ident = if matches!(name.as_str(), "for" | "async" | "type" | "as") { + Ident::new_raw(name.as_str(), Span::call_site()) + } else { + let new_name = name.to_case(Case::Snake); + Ident::new(new_name.as_str(), Span::call_site()) + }; + + ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::AttrText { + value: ifmt_from_text(value.as_deref().unwrap_or("false")), + name: ident, + }, + } + }) + .collect(); + + let class = el.classes.join(" "); + if !class.is_empty() { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::AttrText { + name: Ident::new("class", Span::call_site()), + value: ifmt_from_text(&class), + }, + }); + } + + if let Some(id) = &el.id { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::AttrText { + name: Ident::new("id", Span::call_site()), + value: ifmt_from_text(id), + }, + }); + } + + let children = el.children.iter().filter_map(rsx_node_from_html).collect(); + + Some(BodyNode::Element(Element { + name: el_name, + children, + attributes, + _is_static: false, + key: None, + brace: Default::default(), + })) + } + + // We ignore comments + Node::Comment(_) => None, + } +} + +/// Pull out all the svgs from the body and replace them with components of the same name +pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec) { + for child in children { + match child { + BodyNode::Component(comp) => collect_svgs(&mut comp.children, out), + + BodyNode::Element(el) if el.name == "svg" => { + // we want to replace this instance with a component + let mut segments = Punctuated::new(); + + segments.push(Ident::new("icons", Span::call_site()).into()); + + let new_name: Ident = Ident::new(&format!("icon_{}", out.len()), Span::call_site()); + + segments.push(new_name.clone().into()); + + // Replace this instance with a component + let mut new_comp = BodyNode::Component(Component { + name: syn::Path { + leading_colon: None, + segments, + }, + prop_gen_args: None, + fields: vec![], + children: vec![], + manual_props: None, + brace: Default::default(), + }); + + std::mem::swap(child, &mut new_comp); + + // And push the original svg into the svg list + out.push(new_comp); + } + + BodyNode::Element(el) => collect_svgs(&mut el.children, out), + + _ => {} + } + } +} + +fn ifmt_from_text(text: &str) -> IfmtInput { + IfmtInput { + source: Some(LitStr::new(text, Span::call_site())), + segments: vec![], + } +} diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml index 40637eff8..055a97132 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -1,18 +1,21 @@ [package] name = "dioxus-rsx" -version = "0.0.1" +version = "0.0.3" edition = "2018" license = "MIT/Apache-2.0" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" -documentation = "https://docs.rs/dioxus-rsx" -keywords = ["dom", "ui", "gui", "react", "wasm"] +documentation = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] proc-macro2 = { version = "1.0", features = ["span-locations"] } +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" +krates = "0.12.6" \ No newline at end of file diff --git a/packages/rsx/README.md b/packages/rsx/README.md index 920fcd88c..c20737167 100644 --- a/packages/rsx/README.md +++ b/packages/rsx/README.md @@ -1 +1,43 @@ -# The actual RSX language implemented using syn parsers. +# Dioxus-RSX + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-rsx.svg +[crates-url]: https://crates.io/crates/dioxus-rsx + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) | +[API Docs](https://docs.rs/dioxus-rsx/latest/dioxus_rsx) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +This crate provides the actual DSL that Dioxus uses in the `rsx!` macro. This crate is separate from the macro crate to enable tooling like autoformat, translation, and AST manipulation (extract to component). + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT, without any additional +terms or conditions. + diff --git a/packages/rsx/src/attributes.rs b/packages/rsx/src/attributes.rs deleted file mode 100644 index 8920d69af..000000000 --- a/packages/rsx/src/attributes.rs +++ /dev/null @@ -1,892 +0,0 @@ -use crate::elements::*; - -// map the rsx name of the attribute to the html name of the attribute, the namespace that contains it, and if the attribute is volitile -pub fn attrbute_to_static_str( - attr: &str, - element: &'static str, - namespace: Option<&'static str>, -) -> Option<(&'static str, Option<&'static str>, bool)> { - if namespace == Some("http://www.w3.org/2000/svg") { - svg::MAPPED_ATTRIBUTES - .iter() - .find(|(a, _)| *a == attr) - .map(|(_, b)| (*b, None, false)) - } else { - NO_NAMESPACE_ATTRIBUTES - .iter() - .find(|&a| *a == attr) - .map(|a| (*a, None, false)) - .or_else(|| { - STYLE_ATTRIBUTES - .iter() - .find(|(a, _)| *a == attr) - .map(|(_, b)| (*b, Some("style"), false)) - }) - .or_else(|| { - MAPPED_ATTRIBUTES - .iter() - .find(|(a, _)| *a == attr) - .map(|(_, b)| (*b, None, false)) - }) - } - .or_else(|| { - ELEMENTS_WITH_MAPPED_ATTRIBUTES - .iter() - .find_map(|(el, attrs)| { - (element == *el) - .then(|| { - attrs - .iter() - .find(|(a, _, _)| *a == attr) - .map(|(_, b, volitile)| (*b, None, *volitile)) - }) - .flatten() - }) - }) - .or_else(|| { - ELEMENTS_WITH_NAMESPACE.iter().find_map(|(el, ns, attrs)| { - (element == *el && namespace == Some(*ns)) - .then(|| { - attrs - .iter() - .find(|a| **a == attr) - .map(|a| (*a, None, false)) - }) - .flatten() - }) - }) - .or_else(|| { - ELEMENTS_WITHOUT_NAMESPACE.iter().find_map(|(el, attrs)| { - (element == *el) - .then(|| { - attrs - .iter() - .find(|a| **a == attr) - .map(|a| (*a, None, false)) - }) - .flatten() - }) - }) -} - -macro_rules! no_namespace_trait_methods { - ( - $( - $(#[$attr:meta])* - $name:ident; - )* - ) => { - pub const NO_NAMESPACE_ATTRIBUTES: &'static [&'static str] = &[ - $( - stringify!($name), - )* - ]; - }; -} - -macro_rules! style_trait_methods { - ( - $( - $(#[$attr:meta])* - $name:ident: $lit:literal, - )* - ) => { - pub const STYLE_ATTRIBUTES: &'static [(&'static str, &'static str)] = &[ - $( - (stringify!($name), $lit), - )* - ]; - }; -} - -macro_rules! mapped_trait_methods { - ( - $( - $(#[$attr:meta])* - $name:ident: $lit:literal, - )* - ) => { - pub const MAPPED_ATTRIBUTES: &'static [(&'static str, &'static str)] = &[ - $( - (stringify!($name), $lit), - )* - ("prevent_default", "dioxus-prevent-default"), - ]; - }; -} - -no_namespace_trait_methods! { - accesskey; - - /// The HTML class attribute is used to specify a class for an HTML element. - /// - /// ## Details - /// Multiple HTML elements can share the same class. - /// - /// The class global attribute is a space-separated list of the case-sensitive classes of the element. - /// Classes allow CSS and Javascript to select and access specific elements via the class selectors or - /// functions like the DOM method document.getElementsByClassName. - /// - /// ## Example - /// - /// ### HTML: - /// ```html - ///

Above point sounds a bit obvious. Remove/rewrite?

- /// ``` - /// - /// ### CSS: - /// ```css - /// .note { - /// font-style: italic; - /// font-weight: bold; - /// } - /// - /// .editorial { - /// background: rgb(255, 0, 0, .25); - /// padding: 10px; - /// } - /// ``` - class; - contenteditable; - data; - dir; - draggable; - hidden; - id; - lang; - spellcheck; - style; - tabindex; - title; - translate; - - role; - - /// dangerous_inner_html is Dioxus's replacement for using innerHTML in the browser DOM. In general, setting - /// HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS) - /// attack. So, you can set HTML directly from Dioxus, but you have to type out dangerous_inner_html to remind - /// yourself that it’s dangerous - dangerous_inner_html; -} - -// This macro creates an explicit method call for each of the style attributes. -// -// The left token specifies the name of the attribute in the rsx! macro, and the right string literal specifies the -// actual name of the attribute generated. -// -// This roughly follows the html spec -style_trait_methods! { - align_content: "align-content", - align_items: "align-items", - align_self: "align-self", - alignment_adjust: "alignment-adjust", - alignment_baseline: "alignment-baseline", - all: "all", - alt: "alt", - animation: "animation", - animation_delay: "animation-delay", - animation_direction: "animation-direction", - animation_duration: "animation-duration", - animation_fill_mode: "animation-fill-mode", - animation_iteration_count: "animation-iteration-count", - animation_name: "animation-name", - animation_play_state: "animation-play-state", - animation_timing_function: "animation-timing-function", - azimuth: "azimuth", - backface_visibility: "backface-visibility", - background: "background", - background_attachment: "background-attachment", - background_clip: "background-clip", - background_color: "background-color", - background_image: "background-image", - background_origin: "background-origin", - background_position: "background-position", - background_repeat: "background-repeat", - background_size: "background-size", - background_blend_mode: "background-blend-mode", - baseline_shift: "baseline-shift", - bleed: "bleed", - bookmark_label: "bookmark-label", - bookmark_level: "bookmark-level", - bookmark_state: "bookmark-state", - border: "border", - border_color: "border-color", - border_style: "border-style", - border_width: "border-width", - border_bottom: "border-bottom", - border_bottom_color: "border-bottom-color", - border_bottom_style: "border-bottom-style", - border_bottom_width: "border-bottom-width", - border_left: "border-left", - border_left_color: "border-left-color", - border_left_style: "border-left-style", - border_left_width: "border-left-width", - border_right: "border-right", - border_right_color: "border-right-color", - border_right_style: "border-right-style", - border_right_width: "border-right-width", - border_top: "border-top", - border_top_color: "border-top-color", - border_top_style: "border-top-style", - border_top_width: "border-top-width", - border_collapse: "border-collapse", - border_image: "border-image", - border_image_outset: "border-image-outset", - border_image_repeat: "border-image-repeat", - border_image_slice: "border-image-slice", - border_image_source: "border-image-source", - border_image_width: "border-image-width", - border_radius: "border-radius", - border_bottom_left_radius: "border-bottom-left-radius", - border_bottom_right_radius: "border-bottom-right-radius", - border_top_left_radius: "border-top-left-radius", - border_top_right_radius: "border-top-right-radius", - border_spacing: "border-spacing", - bottom: "bottom", - box_decoration_break: "box-decoration-break", - box_shadow: "box-shadow", - box_sizing: "box-sizing", - box_snap: "box-snap", - break_after: "break-after", - break_before: "break-before", - break_inside: "break-inside", - buffered_rendering: "buffered-rendering", - caption_side: "caption-side", - clear: "clear", - clear_side: "clear-side", - clip: "clip", - clip_path: "clip-path", - clip_rule: "clip-rule", - color: "color", - color_adjust: "color-adjust", - color_correction: "color-correction", - color_interpolation: "color-interpolation", - color_interpolation_filters: "color-interpolation-filters", - color_profile: "color-profile", - color_rendering: "color-rendering", - column_fill: "column-fill", - column_gap: "column-gap", - column_rule: "column-rule", - column_rule_color: "column-rule-color", - column_rule_style: "column-rule-style", - column_rule_width: "column-rule-width", - column_span: "column-span", - columns: "columns", - column_count: "column-count", - column_width: "column-width", - contain: "contain", - content: "content", - counter_increment: "counter-increment", - counter_reset: "counter-reset", - counter_set: "counter-set", - cue: "cue", - cue_after: "cue-after", - cue_before: "cue-before", - cursor: "cursor", - direction: "direction", - display: "display", - display_inside: "display-inside", - display_outside: "display-outside", - display_extras: "display-extras", - display_box: "display-box", - dominant_baseline: "dominant-baseline", - elevation: "elevation", - empty_cells: "empty-cells", - enable_background: "enable-background", - fill: "fill", - fill_opacity: "fill-opacity", - fill_rule: "fill-rule", - filter: "filter", - float: "float", - float_defer_column: "float-defer-column", - float_defer_page: "float-defer-page", - float_offset: "float-offset", - float_wrap: "float-wrap", - flow_into: "flow-into", - flow_from: "flow-from", - flex: "flex", - flex_basis: "flex-basis", - flex_grow: "flex-grow", - flex_shrink: "flex-shrink", - flex_flow: "flex-flow", - flex_direction: "flex-direction", - flex_wrap: "flex-wrap", - flood_color: "flood-color", - flood_opacity: "flood-opacity", - font: "font", - font_family: "font-family", - font_size: "font-size", - font_stretch: "font-stretch", - font_style: "font-style", - font_weight: "font-weight", - font_feature_settings: "font-feature-settings", - font_kerning: "font-kerning", - font_language_override: "font-language-override", - font_size_adjust: "font-size-adjust", - font_synthesis: "font-synthesis", - font_variant: "font-variant", - font_variant_alternates: "font-variant-alternates", - font_variant_caps: "font-variant-caps", - font_variant_east_asian: "font-variant-east-asian", - font_variant_ligatures: "font-variant-ligatures", - font_variant_numeric: "font-variant-numeric", - font_variant_position: "font-variant-position", - footnote_policy: "footnote-policy", - glyph_orientation_horizontal: "glyph-orientation-horizontal", - glyph_orientation_vertical: "glyph-orientation-vertical", - grid: "grid", - grid_auto_flow: "grid-auto-flow", - grid_auto_columns: "grid-auto-columns", - grid_auto_rows: "grid-auto-rows", - grid_template: "grid-template", - grid_template_areas: "grid-template-areas", - grid_template_columns: "grid-template-columns", - grid_template_rows: "grid-template-rows", - grid_area: "grid-area", - grid_column: "grid-column", - grid_column_start: "grid-column-start", - grid_column_end: "grid-column-end", - grid_row: "grid-row", - grid_row_start: "grid-row-start", - grid_row_end: "grid-row-end", - hanging_punctuation: "hanging-punctuation", - height: "height", - hyphenate_character: "hyphenate-character", - hyphenate_limit_chars: "hyphenate-limit-chars", - hyphenate_limit_last: "hyphenate-limit-last", - hyphenate_limit_lines: "hyphenate-limit-lines", - hyphenate_limit_zone: "hyphenate-limit-zone", - hyphens: "hyphens", - icon: "icon", - image_orientation: "image-orientation", - image_resolution: "image-resolution", - image_rendering: "image-rendering", - ime: "ime", - ime_align: "ime-align", - ime_mode: "ime-mode", - ime_offset: "ime-offset", - ime_width: "ime-width", - initial_letters: "initial-letters", - inline_box_align: "inline-box-align", - isolation: "isolation", - justify_content: "justify-content", - justify_items: "justify-items", - justify_self: "justify-self", - kerning: "kerning", - left: "left", - letter_spacing: "letter-spacing", - lighting_color: "lighting-color", - line_box_contain: "line-box-contain", - line_break: "line-break", - line_grid: "line-grid", - line_height: "line-height", - line_slack: "line-slack", - line_snap: "line-snap", - list_style: "list-style", - list_style_image: "list-style-image", - list_style_position: "list-style-position", - list_style_type: "list-style-type", - margin: "margin", - margin_bottom: "margin-bottom", - margin_left: "margin-left", - margin_right: "margin-right", - margin_top: "margin-top", - marker: "marker", - marker_end: "marker-end", - marker_mid: "marker-mid", - marker_pattern: "marker-pattern", - marker_segment: "marker-segment", - marker_start: "marker-start", - marker_knockout_left: "marker-knockout-left", - marker_knockout_right: "marker-knockout-right", - marker_side: "marker-side", - marks: "marks", - marquee_direction: "marquee-direction", - marquee_play_count: "marquee-play-count", - marquee_speed: "marquee-speed", - marquee_style: "marquee-style", - mask: "mask", - mask_image: "mask-image", - mask_repeat: "mask-repeat", - mask_position: "mask-position", - mask_clip: "mask-clip", - mask_origin: "mask-origin", - mask_size: "mask-size", - mask_box: "mask-box", - mask_box_outset: "mask-box-outset", - mask_box_repeat: "mask-box-repeat", - mask_box_slice: "mask-box-slice", - mask_box_source: "mask-box-source", - mask_box_width: "mask-box-width", - mask_type: "mask-type", - max_height: "max-height", - max_lines: "max-lines", - max_width: "max-width", - min_height: "min-height", - min_width: "min-width", - mix_blend_mode: "mix-blend-mode", - nav_down: "nav-down", - nav_index: "nav-index", - nav_left: "nav-left", - nav_right: "nav-right", - nav_up: "nav-up", - object_fit: "object-fit", - object_position: "object-position", - offset_after: "offset-after", - offset_before: "offset-before", - offset_end: "offset-end", - offset_start: "offset-start", - opacity: "opacity", - order: "order", - orphans: "orphans", - outline: "outline", - outline_color: "outline-color", - outline_style: "outline-style", - outline_width: "outline-width", - outline_offset: "outline-offset", - overflow: "overflow", - overflow_x: "overflow-x", - overflow_y: "overflow-y", - overflow_style: "overflow-style", - overflow_wrap: "overflow-wrap", - padding: "padding", - padding_bottom: "padding-bottom", - padding_left: "padding-left", - padding_right: "padding-right", - padding_top: "padding-top", - page: "page", - page_break_after: "page-break-after", - page_break_before: "page-break-before", - page_break_inside: "page-break-inside", - paint_order: "paint-order", - pause: "pause", - pause_after: "pause-after", - pause_before: "pause-before", - perspective: "perspective", - perspective_origin: "perspective-origin", - pitch: "pitch", - pitch_range: "pitch-range", - play_during: "play-during", - pointer_events: "pointer-events", - position: "position", - quotes: "quotes", - region_fragment: "region-fragment", - resize: "resize", - rest: "rest", - rest_after: "rest-after", - rest_before: "rest-before", - richness: "richness", - right: "right", - ruby_align: "ruby-align", - ruby_merge: "ruby-merge", - ruby_position: "ruby-position", - scroll_behavior: "scroll-behavior", - scroll_snap_coordinate: "scroll-snap-coordinate", - scroll_snap_destination: "scroll-snap-destination", - scroll_snap_points_x: "scroll-snap-points-x", - scroll_snap_points_y: "scroll-snap-points-y", - scroll_snap_type: "scroll-snap-type", - shape_image_threshold: "shape-image-threshold", - shape_inside: "shape-inside", - shape_margin: "shape-margin", - shape_outside: "shape-outside", - shape_padding: "shape-padding", - shape_rendering: "shape-rendering", - size: "size", - speak: "speak", - speak_as: "speak-as", - speak_header: "speak-header", - speak_numeral: "speak-numeral", - speak_punctuation: "speak-punctuation", - speech_rate: "speech-rate", - stop_color: "stop-color", - stop_opacity: "stop-opacity", - stress: "stress", - string_set: "string-set", - stroke: "stroke", - stroke_dasharray: "stroke-dasharray", - stroke_dashoffset: "stroke-dashoffset", - stroke_linecap: "stroke-linecap", - stroke_linejoin: "stroke-linejoin", - stroke_miterlimit: "stroke-miterlimit", - stroke_opacity: "stroke-opacity", - stroke_width: "stroke-width", - tab_size: "tab-size", - table_layout: "table-layout", - text_align: "text-align", - text_align_all: "text-align-all", - text_align_last: "text-align-last", - text_anchor: "text-anchor", - text_combine_upright: "text-combine-upright", - text_decoration: "text-decoration", - text_decoration_color: "text-decoration-color", - text_decoration_line: "text-decoration-line", - text_decoration_style: "text-decoration-style", - text_decoration_skip: "text-decoration-skip", - text_emphasis: "text-emphasis", - text_emphasis_color: "text-emphasis-color", - text_emphasis_style: "text-emphasis-style", - text_emphasis_position: "text-emphasis-position", - text_emphasis_skip: "text-emphasis-skip", - text_height: "text-height", - text_indent: "text-indent", - text_justify: "text-justify", - text_orientation: "text-orientation", - text_overflow: "text-overflow", - text_rendering: "text-rendering", - text_shadow: "text-shadow", - text_size_adjust: "text-size-adjust", - text_space_collapse: "text-space-collapse", - text_spacing: "text-spacing", - text_transform: "text-transform", - text_underline_position: "text-underline-position", - text_wrap: "text-wrap", - top: "top", - touch_action: "touch-action", - transform: "transform", - transform_box: "transform-box", - transform_origin: "transform-origin", - transform_style: "transform-style", - transition: "transition", - transition_delay: "transition-delay", - transition_duration: "transition-duration", - transition_property: "transition-property", - unicode_bidi: "unicode-bidi", - vector_effect: "vector-effect", - vertical_align: "vertical-align", - visibility: "visibility", - voice_balance: "voice-balance", - voice_duration: "voice-duration", - voice_family: "voice-family", - voice_pitch: "voice-pitch", - voice_range: "voice-range", - voice_rate: "voice-rate", - voice_stress: "voice-stress", - voice_volumn: "voice-volumn", - volume: "volume", - white_space: "white-space", - widows: "widows", - width: "width", - will_change: "will-change", - word_break: "word-break", - word_spacing: "word-spacing", - word_wrap: "word-wrap", - wrap_flow: "wrap-flow", - wrap_through: "wrap-through", - writing_mode: "writing-mode", - gap: "gap", - list_styler_type: "list-style-type", - row_gap: "row-gap", - transition_timing_function: "transition-timing-function", - user_select: "user-select", - webkit_user_select: "-webkit-user-select", - z_index : "z-index", -} -mapped_trait_methods! { - aria_current: "aria-current", - aria_details: "aria-details", - aria_disabled: "aria-disabled", - aria_hidden: "aria-hidden", - aria_invalid: "aria-invalid", - aria_keyshortcuts: "aria-keyshortcuts", - aria_label: "aria-label", - aria_roledescription: "aria-roledescription", - // Widget Attributes - aria_autocomplete: "aria-autocomplete", - aria_checked: "aria-checked", - aria_expanded: "aria-expanded", - aria_haspopup: "aria-haspopup", - aria_level: "aria-level", - aria_modal: "aria-modal", - aria_multiline: "aria-multiline", - aria_multiselectable: "aria-multiselectable", - aria_orientation: "aria-orientation", - aria_placeholder: "aria-placeholder", - aria_pressed: "aria-pressed", - aria_readonly: "aria-readonly", - aria_required: "aria-required", - aria_selected: "aria-selected", - aria_sort: "aria-sort", - aria_valuemax: "aria-valuemax", - aria_valuemin: "aria-valuemin", - aria_valuenow: "aria-valuenow", - aria_valuetext: "aria-valuetext", - // Live Region Attributes - aria_atomic: "aria-atomic", - aria_busy: "aria-busy", - aria_live: "aria-live", - aria_relevant: "aria-relevant", - - aria_dropeffect: "aria-dropeffect", - aria_grabbed: "aria-grabbed", - // Relationship Attributes - aria_activedescendant: "aria-activedescendant", - aria_colcount: "aria-colcount", - aria_colindex: "aria-colindex", - aria_colspan: "aria-colspan", - aria_controls: "aria-controls", - aria_describedby: "aria-describedby", - aria_errormessage: "aria-errormessage", - aria_flowto: "aria-flowto", - aria_labelledby: "aria-labelledby", - aria_owns: "aria-owns", - aria_posinset: "aria-posinset", - aria_rowcount: "aria-rowcount", - aria_rowindex: "aria-rowindex", - aria_rowspan: "aria-rowspan", - aria_setsize: "aria-setsize", -} - -pub mod svg { - mapped_trait_methods! { - accent_height: "accent-height", - accumulate: "accumulate", - additive: "additive", - alignment_baseline: "alignment-baseline", - alphabetic: "alphabetic", - amplitude: "amplitude", - arabic_form: "arabic-form", - ascent: "ascent", - attributeName: "attributeName", - attributeType: "attributeType", - azimuth: "azimuth", - baseFrequency: "baseFrequency", - baseline_shift: "baseline-shift", - baseProfile: "baseProfile", - bbox: "bbox", - begin: "begin", - bias: "bias", - by: "by", - calcMode: "calcMode", - cap_height: "cap-height", - class: "class", - clip: "clip", - clipPathUnits: "clipPathUnits", - clip_path: "clip-path", - clip_rule: "clip-rule", - color: "color", - color_interpolation: "color-interpolation", - color_interpolation_filters: "color-interpolation-filters", - color_profile: "color-profile", - color_rendering: "color-rendering", - contentScriptType: "contentScriptType", - contentStyleType: "contentStyleType", - crossorigin: "crossorigin", - cursor: "cursor", - cx: "cx", - cy: "cy", - d: "d", - decelerate: "decelerate", - descent: "descent", - diffuseConstant: "diffuseConstant", - direction: "direction", - display: "display", - divisor: "divisor", - dominant_baseline: "dominant-baseline", - dur: "dur", - dx: "dx", - dy: "dy", - edgeMode: "edgeMode", - elevation: "elevation", - enable_background: "enable-background", - end: "end", - exponent: "exponent", - fill: "fill", - fill_opacity: "fill-opacity", - fill_rule: "fill-rule", - filter: "filter", - filterRes: "filterRes", - filterUnits: "filterUnits", - flood_color: "flood-color", - flood_opacity: "flood-opacity", - font_family: "font-family", - font_size: "font-size", - font_size_adjust: "font-size-adjust", - font_stretch: "font-stretch", - font_style: "font-style", - font_variant: "font-variant", - font_weight: "font-weight", - format: "format", - from: "from", - fr: "fr", - fx: "fx", - fy: "fy", - g1: "g1", - g2: "g2", - glyph_name: "glyph-name", - glyph_orientation_horizontal: "glyph-orientation-horizontal", - glyph_orientation_vertical: "glyph-orientation-vertical", - glyphRef: "glyphRef", - gradientTransform: "gradientTransform", - gradientUnits: "gradientUnits", - hanging: "hanging", - height: "height", - href: "href", - hreflang: "hreflang", - horiz_adv_x: "horiz-adv-x", - horiz_origin_x: "horiz-origin-x", - id: "id", - ideographic: "ideographic", - image_rendering: "image-rendering", - _in: "_in", - in2: "in2", - intercept: "intercept", - k: "k", - k1: "k1", - k2: "k2", - k3: "k3", - k4: "k4", - kernelMatrix: "kernelMatrix", - kernelUnitLength: "kernelUnitLength", - kerning: "kerning", - keyPoints: "keyPoints", - keySplines: "keySplines", - keyTimes: "keyTimes", - lang: "lang", - lengthAdjust: "lengthAdjust", - letter_spacing: "letter-spacing", - lighting_color: "lighting-color", - limitingConeAngle: "limitingConeAngle", - local: "local", - marker_end: "marker-end", - marker_mid: "marker-mid", - marker_start: "marker_start", - markerHeight: "markerHeight", - markerUnits: "markerUnits", - markerWidth: "markerWidth", - mask: "mask", - maskContentUnits: "maskContentUnits", - maskUnits: "maskUnits", - mathematical: "mathematical", - max: "max", - media: "media", - method: "method", - min: "min", - mode: "mode", - name: "name", - numOctaves: "numOctaves", - offset: "offset", - opacity: "opacity", - operator: "operator", - order: "order", - orient: "orient", - orientation: "orientation", - origin: "origin", - overflow: "overflow", - overline_position: "overline-position", - overline_thickness: "overline-thickness", - panose_1: "panose-1", - paint_order: "paint-order", - path: "path", - pathLength: "pathLength", - patternContentUnits: "patternContentUnits", - patternTransform: "patternTransform", - patternUnits: "patternUnits", - ping: "ping", - pointer_events: "pointer-events", - points: "points", - pointsAtX: "pointsAtX", - pointsAtY: "pointsAtY", - pointsAtZ: "pointsAtZ", - preserveAlpha: "preserveAlpha", - preserveAspectRatio: "preserveAspectRatio", - primitiveUnits: "primitiveUnits", - r: "r", - radius: "radius", - referrerPolicy: "referrerPolicy", - refX: "refX", - refY: "refY", - rel: "rel", - rendering_intent: "rendering-intent", - repeatCount: "repeatCount", - repeatDur: "repeatDur", - requiredExtensions: "requiredExtensions", - requiredFeatures: "requiredFeatures", - restart: "restart", - result: "result", - role: "role", - rotate: "rotate", - rx: "rx", - ry: "ry", - scale: "scale", - seed: "seed", - shape_rendering: "shape-rendering", - slope: "slope", - spacing: "spacing", - specularConstant: "specularConstant", - specularExponent: "specularExponent", - speed: "speed", - spreadMethod: "spreadMethod", - startOffset: "startOffset", - stdDeviation: "stdDeviation", - stemh: "stemh", - stemv: "stemv", - stitchTiles: "stitchTiles", - stop_color: "stop_color", - stop_opacity: "stop_opacity", - strikethrough_position: "strikethrough-position", - strikethrough_thickness: "strikethrough-thickness", - string: "string", - stroke: "stroke", - stroke_dasharray: "stroke-dasharray", - stroke_dashoffset: "stroke-dashoffset", - stroke_linecap: "stroke-linecap", - stroke_linejoin: "stroke-linejoin", - stroke_miterlimit: "stroke-miterlimit", - stroke_opacity: "stroke-opacity", - stroke_width: "stroke-width", - style: "style", - surfaceScale: "surfaceScale", - systemLanguage: "systemLanguage", - tabindex: "tabindex", - tableValues: "tableValues", - target: "target", - targetX: "targetX", - targetY: "targetY", - text_anchor: "text-anchor", - text_decoration: "text-decoration", - text_rendering: "text-rendering", - textLength: "textLength", - to: "to", - transform: "transform", - transform_origin: "transform-origin", - r#type: "_type", - u1: "u1", - u2: "u2", - underline_position: "underline-position", - underline_thickness: "underline-thickness", - unicode: "unicode", - unicode_bidi: "unicode-bidi", - unicode_range: "unicode-range", - units_per_em: "units-per-em", - v_alphabetic: "v-alphabetic", - v_hanging: "v-hanging", - v_ideographic: "v-ideographic", - v_mathematical: "v-mathematical", - values: "values", - vector_effect: "vector-effect", - version: "version", - vert_adv_y: "vert-adv-y", - vert_origin_x: "vert-origin-x", - vert_origin_y: "vert-origin-y", - view_box: "viewBox", - view_target: "viewTarget", - visibility: "visibility", - width: "width", - widths: "widths", - word_spacing: "word-spacing", - writing_mode: "writing-mode", - x: "x", - x_height: "x-height", - x1: "x1", - x2: "x2", - xmlns: "xmlns", - x_channel_selector: "xChannelSelector", - y: "y", - y1: "y1", - y2: "y2", - y_channel_selector: "yChannelSelector", - z: "z", - zoomAndPan: "zoomAndPan", - } -} diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index d3783c292..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, }) } } @@ -165,7 +162,7 @@ impl ToTokens for Component { } if !self.children.is_empty() { - let renderer = TemplateRenderer { + let renderer: TemplateRenderer = TemplateRenderer { roots: &self.children, }; diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index ae4bfabe9..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, }) } @@ -210,7 +212,7 @@ pub enum ElementAttr { } impl ElementAttr { - pub fn flart(&self) -> Span { + pub fn start(&self) -> Span { match self { ElementAttr::AttrText { name, .. } => name.span(), ElementAttr::AttrExpression { name, .. } => name.span(), @@ -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,15 +258,15 @@ 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 ) } } ElementAttr::CustomAttrText { name, value } => { quote! { __cx.attr( - dioxus_elements::#el_name::#name.0, + #name, #value, None, false @@ -274,7 +276,7 @@ impl ToTokens for ElementAttrNamed { ElementAttr::CustomAttrExpression { name, value } => { quote! { __cx.attr( - dioxus_elements::#el_name::#name.0, + #name, #value, None, false diff --git a/packages/rsx/src/hot_reload/hot_reload_diff.rs b/packages/rsx/src/hot_reload/hot_reload_diff.rs new file mode 100644 index 000000000..ea9918f99 --- /dev/null +++ b/packages/rsx/src/hot_reload/hot_reload_diff.rs @@ -0,0 +1,637 @@ +use proc_macro2::TokenStream; +use syn::{File, Macro}; + +pub enum DiffResult { + CodeChanged, + RsxChanged(Vec<(Macro, TokenStream)>), +} + +/// Find any rsx calls in the given file and return a list of all the rsx calls that have changed. +pub fn find_rsx(new: &File, old: &File) -> DiffResult { + let mut rsx_calls = Vec::new(); + if new.items.len() != old.items.len() { + return DiffResult::CodeChanged; + } + for (new, old) in new.items.iter().zip(old.items.iter()) { + if find_rsx_item(new, old, &mut rsx_calls) { + return DiffResult::CodeChanged; + } + } + DiffResult::RsxChanged(rsx_calls) +} + +fn find_rsx_item( + new: &syn::Item, + old: &syn::Item, + rsx_calls: &mut Vec<(Macro, TokenStream)>, +) -> bool { + match (new, old) { + (syn::Item::Const(new_item), syn::Item::Const(old_item)) => { + find_rsx_expr(&new_item.expr, &old_item.expr, rsx_calls) + || new_item.attrs != old_item.attrs + || new_item.vis != old_item.vis + || new_item.const_token != old_item.const_token + || new_item.ident != old_item.ident + || new_item.colon_token != old_item.colon_token + || new_item.ty != old_item.ty + || new_item.eq_token != old_item.eq_token + || new_item.semi_token != old_item.semi_token + } + (syn::Item::Enum(new_item), syn::Item::Enum(old_item)) => { + if new_item.variants.len() != old_item.variants.len() { + return true; + } + for (new_varient, old_varient) in new_item.variants.iter().zip(old_item.variants.iter()) + { + match (&new_varient.discriminant, &old_varient.discriminant) { + (Some((new_eq, new_expr)), Some((old_eq, old_expr))) => { + if find_rsx_expr(new_expr, old_expr, rsx_calls) || new_eq != old_eq { + return true; + } + } + (None, None) => (), + _ => return true, + } + if new_varient.attrs != old_varient.attrs + || new_varient.ident != old_varient.ident + || new_varient.fields != old_varient.fields + { + return true; + } + } + new_item.attrs != old_item.attrs + || new_item.vis != old_item.vis + || new_item.enum_token != old_item.enum_token + || new_item.ident != old_item.ident + || new_item.generics != old_item.generics + || new_item.brace_token != old_item.brace_token + } + (syn::Item::ExternCrate(new_item), syn::Item::ExternCrate(old_item)) => { + old_item != new_item + } + (syn::Item::Fn(new_item), syn::Item::Fn(old_item)) => { + find_rsx_block(&new_item.block, &old_item.block, rsx_calls) + || new_item.attrs != old_item.attrs + || new_item.vis != old_item.vis + || new_item.sig != old_item.sig + } + (syn::Item::ForeignMod(new_item), syn::Item::ForeignMod(old_item)) => old_item != new_item, + (syn::Item::Impl(new_item), syn::Item::Impl(old_item)) => { + if new_item.items.len() != old_item.items.len() { + return true; + } + for (new_item, old_item) in new_item.items.iter().zip(old_item.items.iter()) { + if match (new_item, old_item) { + (syn::ImplItem::Const(new_item), syn::ImplItem::Const(old_item)) => { + find_rsx_expr(&new_item.expr, &old_item.expr, rsx_calls) + } + (syn::ImplItem::Method(new_item), syn::ImplItem::Method(old_item)) => { + find_rsx_block(&new_item.block, &old_item.block, rsx_calls) + } + (syn::ImplItem::Type(new_item), syn::ImplItem::Type(old_item)) => { + old_item != new_item + } + (syn::ImplItem::Macro(new_item), syn::ImplItem::Macro(old_item)) => { + old_item != new_item + } + _ => true, + } { + return true; + } + } + new_item.attrs != old_item.attrs + || new_item.defaultness != old_item.defaultness + || new_item.unsafety != old_item.unsafety + || new_item.impl_token != old_item.impl_token + || new_item.generics != old_item.generics + || new_item.trait_ != old_item.trait_ + || new_item.self_ty != old_item.self_ty + || new_item.brace_token != old_item.brace_token + } + (syn::Item::Macro(new_item), syn::Item::Macro(old_item)) => { + find_rsx_macro(&new_item.mac, &old_item.mac, rsx_calls) + || new_item.attrs != old_item.attrs + || new_item.semi_token != old_item.semi_token + || new_item.ident != old_item.ident + } + (syn::Item::Macro2(new_item), syn::Item::Macro2(old_item)) => old_item != new_item, + (syn::Item::Mod(new_item), syn::Item::Mod(old_item)) => { + match (&new_item.content, &old_item.content) { + (Some((_, new_items)), Some((_, old_items))) => { + if new_items.len() != old_items.len() { + return true; + } + for (new_item, old_item) in new_items.iter().zip(old_items.iter()) { + if find_rsx_item(new_item, old_item, rsx_calls) { + return true; + } + } + new_item.attrs != old_item.attrs + || new_item.vis != old_item.vis + || new_item.mod_token != old_item.mod_token + || new_item.ident != old_item.ident + || new_item.semi != old_item.semi + } + (None, None) => { + new_item.attrs != old_item.attrs + || new_item.vis != old_item.vis + || new_item.mod_token != old_item.mod_token + || new_item.ident != old_item.ident + || new_item.semi != old_item.semi + } + _ => true, + } + } + (syn::Item::Static(new_item), syn::Item::Static(old_item)) => { + find_rsx_expr(&new_item.expr, &old_item.expr, rsx_calls) + || new_item.attrs != old_item.attrs + || new_item.vis != old_item.vis + || new_item.static_token != old_item.static_token + || new_item.mutability != old_item.mutability + || new_item.ident != old_item.ident + || new_item.colon_token != old_item.colon_token + || new_item.ty != old_item.ty + || new_item.eq_token != old_item.eq_token + || new_item.semi_token != old_item.semi_token + } + (syn::Item::Struct(new_item), syn::Item::Struct(old_item)) => old_item != new_item, + (syn::Item::Trait(new_item), syn::Item::Trait(old_item)) => { + find_rsx_trait(new_item, old_item, rsx_calls) + } + (syn::Item::TraitAlias(new_item), syn::Item::TraitAlias(old_item)) => old_item != new_item, + (syn::Item::Type(new_item), syn::Item::Type(old_item)) => old_item != new_item, + (syn::Item::Union(new_item), syn::Item::Union(old_item)) => old_item != new_item, + (syn::Item::Use(new_item), syn::Item::Use(old_item)) => old_item != new_item, + (syn::Item::Verbatim(_), syn::Item::Verbatim(_)) => false, + _ => true, + } +} + +fn find_rsx_trait( + new_item: &syn::ItemTrait, + old_item: &syn::ItemTrait, + rsx_calls: &mut Vec<(Macro, TokenStream)>, +) -> bool { + if new_item.items.len() != old_item.items.len() { + return true; + } + for (new_item, old_item) in new_item.items.iter().zip(old_item.items.iter()) { + if match (new_item, old_item) { + (syn::TraitItem::Const(new_item), syn::TraitItem::Const(old_item)) => { + if let (Some((_, new_expr)), Some((_, old_expr))) = + (&new_item.default, &old_item.default) + { + find_rsx_expr(new_expr, old_expr, rsx_calls) + } else { + true + } + } + (syn::TraitItem::Method(new_item), syn::TraitItem::Method(old_item)) => { + if let (Some(new_block), Some(old_block)) = (&new_item.default, &old_item.default) { + find_rsx_block(new_block, old_block, rsx_calls) + } else { + true + } + } + (syn::TraitItem::Type(new_item), syn::TraitItem::Type(old_item)) => { + old_item != new_item + } + (syn::TraitItem::Macro(new_item), syn::TraitItem::Macro(old_item)) => { + old_item != new_item + } + _ => true, + } { + return true; + } + } + new_item.attrs != old_item.attrs + || new_item.vis != old_item.vis + || new_item.unsafety != old_item.unsafety + || new_item.auto_token != old_item.auto_token + || new_item.ident != old_item.ident + || new_item.generics != old_item.generics + || new_item.colon_token != old_item.colon_token + || new_item.supertraits != old_item.supertraits + || new_item.brace_token != old_item.brace_token +} + +fn find_rsx_block( + new_block: &syn::Block, + old_block: &syn::Block, + rsx_calls: &mut Vec<(Macro, TokenStream)>, +) -> bool { + if new_block.stmts.len() != old_block.stmts.len() { + return true; + } + for (new_stmt, old_stmt) in new_block.stmts.iter().zip(old_block.stmts.iter()) { + if find_rsx_stmt(new_stmt, old_stmt, rsx_calls) { + return true; + } + } + new_block.brace_token != old_block.brace_token +} + +fn find_rsx_stmt( + new_stmt: &syn::Stmt, + old_stmt: &syn::Stmt, + rsx_calls: &mut Vec<(Macro, TokenStream)>, +) -> bool { + match (new_stmt, old_stmt) { + (syn::Stmt::Local(new_local), syn::Stmt::Local(old_local)) => { + (match (&new_local.init, &old_local.init) { + (Some((new_eq, new_expr)), Some((old_eq, old_expr))) => { + find_rsx_expr(new_expr, old_expr, rsx_calls) || new_eq != old_eq + } + (None, None) => false, + _ => true, + } || new_local.attrs != old_local.attrs + || new_local.let_token != old_local.let_token + || new_local.pat != old_local.pat + || new_local.semi_token != old_local.semi_token) + } + (syn::Stmt::Item(new_item), syn::Stmt::Item(old_item)) => { + find_rsx_item(new_item, old_item, rsx_calls) + } + (syn::Stmt::Expr(new_expr), syn::Stmt::Expr(old_expr)) => { + find_rsx_expr(new_expr, old_expr, rsx_calls) + } + (syn::Stmt::Semi(new_expr, new_semi), syn::Stmt::Semi(old_expr, old_semi)) => { + find_rsx_expr(new_expr, old_expr, rsx_calls) || new_semi != old_semi + } + _ => true, + } +} + +fn find_rsx_expr( + new_expr: &syn::Expr, + old_expr: &syn::Expr, + rsx_calls: &mut Vec<(Macro, TokenStream)>, +) -> bool { + match (new_expr, old_expr) { + (syn::Expr::Array(new_expr), syn::Expr::Array(old_expr)) => { + if new_expr.elems.len() != old_expr.elems.len() { + return true; + } + for (new_el, old_el) in new_expr.elems.iter().zip(old_expr.elems.iter()) { + if find_rsx_expr(new_el, old_el, rsx_calls) { + return true; + } + } + new_expr.attrs != old_expr.attrs || new_expr.bracket_token != old_expr.bracket_token + } + (syn::Expr::Assign(new_expr), syn::Expr::Assign(old_expr)) => { + find_rsx_expr(&new_expr.left, &old_expr.left, rsx_calls) + || find_rsx_expr(&new_expr.right, &old_expr.right, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.eq_token != old_expr.eq_token + } + (syn::Expr::AssignOp(new_expr), syn::Expr::AssignOp(old_expr)) => { + find_rsx_expr(&new_expr.left, &old_expr.left, rsx_calls) + || find_rsx_expr(&new_expr.right, &old_expr.right, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.op != old_expr.op + } + (syn::Expr::Async(new_expr), syn::Expr::Async(old_expr)) => { + find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.async_token != old_expr.async_token + || new_expr.capture != old_expr.capture + } + (syn::Expr::Await(new_expr), syn::Expr::Await(old_expr)) => { + find_rsx_expr(&new_expr.base, &old_expr.base, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.dot_token != old_expr.dot_token + || new_expr.await_token != old_expr.await_token + } + (syn::Expr::Binary(new_expr), syn::Expr::Binary(old_expr)) => { + find_rsx_expr(&new_expr.left, &old_expr.left, rsx_calls) + || find_rsx_expr(&new_expr.right, &old_expr.right, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.op != old_expr.op + } + (syn::Expr::Block(new_expr), syn::Expr::Block(old_expr)) => { + find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.label != old_expr.label + } + (syn::Expr::Box(new_expr), syn::Expr::Box(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.box_token != old_expr.box_token + } + (syn::Expr::Break(new_expr), syn::Expr::Break(old_expr)) => { + match (&new_expr.expr, &old_expr.expr) { + (Some(new_inner), Some(old_inner)) => { + find_rsx_expr(new_inner, old_inner, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.break_token != old_expr.break_token + || new_expr.label != old_expr.label + } + (None, None) => { + new_expr.attrs != old_expr.attrs + || new_expr.break_token != old_expr.break_token + || new_expr.label != old_expr.label + } + _ => true, + } + } + (syn::Expr::Call(new_expr), syn::Expr::Call(old_expr)) => { + find_rsx_expr(&new_expr.func, &old_expr.func, rsx_calls); + if new_expr.args.len() != old_expr.args.len() { + return true; + } + for (new_arg, old_arg) in new_expr.args.iter().zip(old_expr.args.iter()) { + if find_rsx_expr(new_arg, old_arg, rsx_calls) { + return true; + } + } + new_expr.attrs != old_expr.attrs || new_expr.paren_token != old_expr.paren_token + } + (syn::Expr::Cast(new_expr), syn::Expr::Cast(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.as_token != old_expr.as_token + || new_expr.ty != old_expr.ty + } + (syn::Expr::Closure(new_expr), syn::Expr::Closure(old_expr)) => { + find_rsx_expr(&new_expr.body, &old_expr.body, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.movability != old_expr.movability + || new_expr.asyncness != old_expr.asyncness + || new_expr.capture != old_expr.capture + || new_expr.or1_token != old_expr.or1_token + || new_expr.inputs != old_expr.inputs + || new_expr.or2_token != old_expr.or2_token + || new_expr.output != old_expr.output + } + (syn::Expr::Continue(new_expr), syn::Expr::Continue(old_expr)) => old_expr != new_expr, + (syn::Expr::Field(new_expr), syn::Expr::Field(old_expr)) => { + find_rsx_expr(&new_expr.base, &old_expr.base, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.dot_token != old_expr.dot_token + || new_expr.member != old_expr.member + } + (syn::Expr::ForLoop(new_expr), syn::Expr::ForLoop(old_expr)) => { + find_rsx_block(&new_expr.body, &old_expr.body, rsx_calls) + || find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.label != old_expr.label + || new_expr.for_token != old_expr.for_token + || new_expr.pat != old_expr.pat + || new_expr.in_token != old_expr.in_token + } + (syn::Expr::Group(new_expr), syn::Expr::Group(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + } + (syn::Expr::If(new_expr), syn::Expr::If(old_expr)) => { + if find_rsx_expr(&new_expr.cond, &old_expr.cond, rsx_calls) + || find_rsx_block(&new_expr.then_branch, &old_expr.then_branch, rsx_calls) + { + return true; + } + match (&new_expr.else_branch, &old_expr.else_branch) { + (Some((new_tok, new_else)), Some((old_tok, old_else))) => { + find_rsx_expr(new_else, old_else, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.if_token != old_expr.if_token + || new_expr.cond != old_expr.cond + || new_tok != old_tok + } + (None, None) => { + new_expr.attrs != old_expr.attrs + || new_expr.if_token != old_expr.if_token + || new_expr.cond != old_expr.cond + } + _ => true, + } + } + (syn::Expr::Index(new_expr), syn::Expr::Index(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || find_rsx_expr(&new_expr.index, &old_expr.index, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.bracket_token != old_expr.bracket_token + } + (syn::Expr::Let(new_expr), syn::Expr::Let(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.let_token != old_expr.let_token + || new_expr.pat != old_expr.pat + || new_expr.eq_token != old_expr.eq_token + } + (syn::Expr::Lit(new_expr), syn::Expr::Lit(old_expr)) => old_expr != new_expr, + (syn::Expr::Loop(new_expr), syn::Expr::Loop(old_expr)) => { + find_rsx_block(&new_expr.body, &old_expr.body, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.label != old_expr.label + || new_expr.loop_token != old_expr.loop_token + } + (syn::Expr::Macro(new_expr), syn::Expr::Macro(old_expr)) => { + find_rsx_macro(&new_expr.mac, &old_expr.mac, rsx_calls) + || new_expr.attrs != old_expr.attrs + } + (syn::Expr::Match(new_expr), syn::Expr::Match(old_expr)) => { + if find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) { + return true; + } + for (new_arm, old_arm) in new_expr.arms.iter().zip(old_expr.arms.iter()) { + match (&new_arm.guard, &old_arm.guard) { + (Some((new_tok, new_expr)), Some((old_tok, old_expr))) => { + if find_rsx_expr(new_expr, old_expr, rsx_calls) || new_tok != old_tok { + return true; + } + } + (None, None) => (), + _ => return true, + } + if find_rsx_expr(&new_arm.body, &old_arm.body, rsx_calls) + || new_arm.attrs != old_arm.attrs + || new_arm.pat != old_arm.pat + || new_arm.fat_arrow_token != old_arm.fat_arrow_token + || new_arm.comma != old_arm.comma + { + return true; + } + } + new_expr.attrs != old_expr.attrs + || new_expr.match_token != old_expr.match_token + || new_expr.brace_token != old_expr.brace_token + } + (syn::Expr::MethodCall(new_expr), syn::Expr::MethodCall(old_expr)) => { + if find_rsx_expr(&new_expr.receiver, &old_expr.receiver, rsx_calls) { + return true; + } + for (new_arg, old_arg) in new_expr.args.iter().zip(old_expr.args.iter()) { + if find_rsx_expr(new_arg, old_arg, rsx_calls) { + return true; + } + } + new_expr.attrs != old_expr.attrs + || new_expr.dot_token != old_expr.dot_token + || new_expr.method != old_expr.method + || new_expr.turbofish != old_expr.turbofish + || new_expr.paren_token != old_expr.paren_token + } + (syn::Expr::Paren(new_expr), syn::Expr::Paren(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.paren_token != old_expr.paren_token + } + (syn::Expr::Path(new_expr), syn::Expr::Path(old_expr)) => old_expr != new_expr, + (syn::Expr::Range(new_expr), syn::Expr::Range(old_expr)) => { + match (&new_expr.from, &old_expr.from) { + (Some(new_expr), Some(old_expr)) => { + if find_rsx_expr(new_expr, old_expr, rsx_calls) { + return true; + } + } + (None, None) => (), + _ => return true, + } + match (&new_expr.to, &old_expr.to) { + (Some(new_inner), Some(old_inner)) => { + find_rsx_expr(new_inner, old_inner, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.limits != old_expr.limits + } + (None, None) => { + new_expr.attrs != old_expr.attrs || new_expr.limits != old_expr.limits + } + _ => true, + } + } + (syn::Expr::Reference(new_expr), syn::Expr::Reference(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.and_token != old_expr.and_token + || new_expr.mutability != old_expr.mutability + } + (syn::Expr::Repeat(new_expr), syn::Expr::Repeat(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || find_rsx_expr(&new_expr.len, &old_expr.len, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.bracket_token != old_expr.bracket_token + || new_expr.semi_token != old_expr.semi_token + } + (syn::Expr::Return(new_expr), syn::Expr::Return(old_expr)) => { + match (&new_expr.expr, &old_expr.expr) { + (Some(new_inner), Some(old_inner)) => { + find_rsx_expr(new_inner, old_inner, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.return_token != old_expr.return_token + } + (None, None) => { + new_expr.attrs != old_expr.attrs + || new_expr.return_token != old_expr.return_token + } + _ => true, + } + } + (syn::Expr::Struct(new_expr), syn::Expr::Struct(old_expr)) => { + match (&new_expr.rest, &old_expr.rest) { + (Some(new_expr), Some(old_expr)) => { + if find_rsx_expr(new_expr, old_expr, rsx_calls) { + return true; + } + } + (None, None) => (), + _ => return true, + } + for (new_field, old_field) in new_expr.fields.iter().zip(old_expr.fields.iter()) { + if find_rsx_expr(&new_field.expr, &old_field.expr, rsx_calls) + || new_field.attrs != old_field.attrs + || new_field.member != old_field.member + || new_field.colon_token != old_field.colon_token + { + return true; + } + } + new_expr.attrs != old_expr.attrs + || new_expr.path != old_expr.path + || new_expr.brace_token != old_expr.brace_token + || new_expr.dot2_token != old_expr.dot2_token + } + (syn::Expr::Try(new_expr), syn::Expr::Try(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.question_token != old_expr.question_token + } + (syn::Expr::TryBlock(new_expr), syn::Expr::TryBlock(old_expr)) => { + find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.try_token != old_expr.try_token + } + (syn::Expr::Tuple(new_expr), syn::Expr::Tuple(old_expr)) => { + for (new_el, old_el) in new_expr.elems.iter().zip(old_expr.elems.iter()) { + if find_rsx_expr(new_el, old_el, rsx_calls) { + return true; + } + } + new_expr.attrs != old_expr.attrs || new_expr.paren_token != old_expr.paren_token + } + (syn::Expr::Type(new_expr), syn::Expr::Type(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.colon_token != old_expr.colon_token + || new_expr.ty != old_expr.ty + } + (syn::Expr::Unary(new_expr), syn::Expr::Unary(old_expr)) => { + find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.op != old_expr.op + } + (syn::Expr::Unsafe(new_expr), syn::Expr::Unsafe(old_expr)) => { + find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.unsafe_token != old_expr.unsafe_token + } + (syn::Expr::While(new_expr), syn::Expr::While(old_expr)) => { + find_rsx_expr(&new_expr.cond, &old_expr.cond, rsx_calls) + || find_rsx_block(&new_expr.body, &old_expr.body, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.label != old_expr.label + || new_expr.while_token != old_expr.while_token + } + (syn::Expr::Yield(new_expr), syn::Expr::Yield(old_expr)) => { + match (&new_expr.expr, &old_expr.expr) { + (Some(new_inner), Some(old_inner)) => { + find_rsx_expr(new_inner, old_inner, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.yield_token != old_expr.yield_token + } + (None, None) => { + new_expr.attrs != old_expr.attrs || new_expr.yield_token != old_expr.yield_token + } + _ => true, + } + } + (syn::Expr::Verbatim(_), syn::Expr::Verbatim(_)) => false, + _ => true, + } +} + +fn find_rsx_macro( + new_mac: &syn::Macro, + old_mac: &syn::Macro, + rsx_calls: &mut Vec<(Macro, TokenStream)>, +) -> bool { + if matches!( + new_mac + .path + .get_ident() + .map(|ident| ident.to_string()) + .as_deref(), + Some("rsx" | "render") + ) && matches!( + old_mac + .path + .get_ident() + .map(|ident| ident.to_string()) + .as_deref(), + Some("rsx" | "render") + ) { + rsx_calls.push((old_mac.clone(), new_mac.tokens.clone())); + false + } else { + new_mac != old_mac + } +} diff --git a/packages/rsx/src/hot_reload/hot_reloading_context.rs b/packages/rsx/src/hot_reload/hot_reloading_context.rs new file mode 100644 index 000000000..c38101093 --- /dev/null +++ b/packages/rsx/src/hot_reload/hot_reloading_context.rs @@ -0,0 +1,19 @@ +pub trait HotReloadingContext { + fn map_attribute( + element_name_rust: &str, + attribute_name_rust: &str, + ) -> Option<(&'static str, Option<&'static str>)>; + fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)>; +} + +pub struct Empty; + +impl HotReloadingContext for Empty { + fn map_attribute(_: &str, _: &str) -> Option<(&'static str, Option<&'static str>)> { + None + } + + fn map_element(_: &str) -> Option<(&'static str, Option<&'static str>)> { + None + } +} diff --git a/packages/rsx/src/hot_reload/hot_reloading_file_map.rs b/packages/rsx/src/hot_reload/hot_reloading_file_map.rs new file mode 100644 index 000000000..0393142b4 --- /dev/null +++ b/packages/rsx/src/hot_reload/hot_reloading_file_map.rs @@ -0,0 +1,192 @@ +use crate::{CallBody, HotReloadingContext}; +use dioxus_core::Template; +use krates::cm::MetadataCommand; +use krates::Cmd; +pub use proc_macro2::TokenStream; +pub use std::collections::HashMap; +use std::path::PathBuf; +pub use std::sync::Mutex; +pub use std::time::SystemTime; +pub use std::{fs, io, path::Path}; +pub use std::{fs::File, io::Read}; +pub use syn::__private::ToTokens; +use syn::spanned::Spanned; + +use super::hot_reload_diff::{find_rsx, DiffResult}; + +pub enum UpdateResult { + UpdatedRsx(Vec>), + NeedsRebuild, +} + +/// The result of building a FileMap +pub struct FileMapBuildResult { + /// The FileMap that was built + pub map: FileMap, + /// Any errors that occurred while building the FileMap that were not fatal + pub errors: Vec, +} + +pub struct FileMap { + pub map: HashMap>)>, + in_workspace: HashMap>, + phantom: std::marker::PhantomData, +} + +impl FileMap { + /// Create a new FileMap from a crate directory + pub fn create(path: PathBuf) -> io::Result> { + Self::create_with_filter(path, |_| false) + } + + /// Create a new FileMap from a crate directory + pub fn create_with_filter( + path: PathBuf, + mut filter: impl FnMut(&Path) -> bool, + ) -> io::Result> { + struct FileMapSearchResult { + map: HashMap>)>, + errors: Vec, + } + fn find_rs_files( + root: PathBuf, + filter: &mut impl FnMut(&Path) -> bool, + ) -> io::Result { + let mut files = HashMap::new(); + let mut errors = Vec::new(); + if root.is_dir() { + for entry in (fs::read_dir(root)?).flatten() { + let path = entry.path(); + if !filter(&path) { + let FileMapSearchResult { + map, + errors: child_errors, + } = find_rs_files(path, filter)?; + errors.extend(child_errors); + files.extend(map); + } + } + } else if root.extension().and_then(|s| s.to_str()) == Some("rs") { + if let Ok(mut file) = File::open(root.clone()) { + let mut src = String::new(); + file.read_to_string(&mut src)?; + files.insert(root, (src, None)); + } + } + Ok(FileMapSearchResult { map: files, errors }) + } + + let FileMapSearchResult { map, errors } = find_rs_files(path, &mut filter)?; + let result = Self { + map, + in_workspace: HashMap::new(), + phantom: std::marker::PhantomData, + }; + Ok(FileMapBuildResult { + map: result, + errors, + }) + } + + /// Try to update the rsx in a file + pub fn update_rsx(&mut self, file_path: &Path, crate_dir: &Path) -> io::Result { + let mut file = File::open(file_path)?; + let mut src = String::new(); + file.read_to_string(&mut src)?; + if let Ok(syntax) = syn::parse_file(&src) { + let in_workspace = self.child_in_workspace(crate_dir)?; + if let Some((old_src, template_slot)) = self.map.get_mut(file_path) { + if let Ok(old) = syn::parse_file(old_src) { + match find_rsx(&syntax, &old) { + DiffResult::CodeChanged => { + self.map.insert(file_path.to_path_buf(), (src, None)); + } + DiffResult::RsxChanged(changed) => { + let mut messages: Vec> = Vec::new(); + for (old, new) in changed.into_iter() { + let old_start = old.span().start(); + + if let (Ok(old_call_body), Ok(new_call_body)) = ( + syn::parse2::(old.tokens), + syn::parse2::(new), + ) { + // if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root + // we need to check if the file is in a workspace or not and strip the prefix accordingly + let prefix = if let Some(workspace) = &in_workspace { + workspace + } else { + crate_dir + }; + if let Ok(file) = file_path.strip_prefix(prefix) { + let line = old_start.line; + let column = old_start.column + 1; + let location = file.display().to_string() + + ":" + + &line.to_string() + + ":" + + &column.to_string() + // the byte index doesn't matter, but dioxus needs it + + ":0"; + + if let Some(template) = new_call_body + .update_template::( + Some(old_call_body), + Box::leak(location.into_boxed_str()), + ) + { + // dioxus cannot handle empty templates + if template.roots.is_empty() { + return Ok(UpdateResult::NeedsRebuild); + } else { + // if the template is the same, don't send it + if let Some(old_template) = template_slot { + if old_template == &template { + continue; + } + } + *template_slot = Some(template); + messages.push(template); + } + } else { + return Ok(UpdateResult::NeedsRebuild); + } + } + } + } + return Ok(UpdateResult::UpdatedRsx(messages)); + } + } + } + } else { + // if this is a new file, rebuild the project + let FileMapBuildResult { map, mut errors } = + FileMap::create(crate_dir.to_path_buf())?; + if let Some(err) = errors.pop() { + return Err(err); + } + *self = map; + } + } + Ok(UpdateResult::NeedsRebuild) + } + + fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result> { + if let Some(in_workspace) = self.in_workspace.get(crate_dir) { + Ok(in_workspace.clone()) + } else { + let mut cmd = Cmd::new(); + let manafest_path = crate_dir.join("Cargo.toml"); + cmd.manifest_path(&manafest_path); + let cmd: MetadataCommand = cmd.into(); + let metadata = cmd + .exec() + .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + + let in_workspace = metadata.workspace_root != crate_dir; + let workspace_path = in_workspace.then(|| metadata.workspace_root.into()); + self.in_workspace + .insert(crate_dir.to_path_buf(), workspace_path.clone()); + Ok(workspace_path) + } + } +} diff --git a/packages/rsx/src/hot_reload/mod.rs b/packages/rsx/src/hot_reload/mod.rs new file mode 100644 index 000000000..b42ed454d --- /dev/null +++ b/packages/rsx/src/hot_reload/mod.rs @@ -0,0 +1,6 @@ +mod hot_reload_diff; +pub use hot_reload_diff::*; +mod hot_reloading_context; +pub use hot_reloading_context::*; +mod hot_reloading_file_map; +pub use hot_reloading_file_map::*; diff --git a/packages/rsx/src/ifmt.rs b/packages/rsx/src/ifmt.rs index e94884907..8146cea42 100644 --- a/packages/rsx/src/ifmt.rs +++ b/packages/rsx/src/ifmt.rs @@ -21,7 +21,7 @@ pub struct IfmtInput { impl IfmtInput { pub fn is_static(&self) -> bool { - matches!(self.segments.as_slice(), &[Segment::Literal(_)]) + matches!(self.segments.as_slice(), &[Segment::Literal(_)] | &[]) } } @@ -59,6 +59,11 @@ impl FromStr for IfmtInput { let mut current_captured = String::new(); while let Some(c) = chars.next() { if c == ':' { + // two :s in a row is a path, not a format arg + if chars.next_if(|c| *c == ':').is_some() { + current_captured.push_str("::"); + continue; + } let mut current_format_args = String::new(); for c in chars.by_ref() { if c == '}' { @@ -193,7 +198,7 @@ pub struct FormattedSegment { impl ToTokens for FormattedSegment { fn to_tokens(&self, tokens: &mut TokenStream) { let (fmt, seg) = (&self.format_args, &self.segment); - let fmt = format!("{{0:{}}}", fmt); + let fmt = format!("{{0:{fmt}}}"); tokens.append_all(quote! { format_args!(#fmt, #seg) }); diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index a795c2081..d22b122e9 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -15,14 +15,19 @@ mod errors; mod component; mod element; +pub mod hot_reload; mod ifmt; mod node; -mod template; + +use std::{collections::HashMap, fmt::Debug, hash::Hash}; // Re-export the namespaces into each other pub use component::*; +use dioxus_core::{Template, TemplateAttribute, TemplateNode}; pub use element::*; +pub use hot_reload::HotReloadingContext; pub use ifmt::*; +use internment::Intern; pub use node::*; // imports @@ -33,13 +38,30 @@ use syn::{ Result, Token, }; +// interns a object into a static object, resusing the value if it already exists +fn intern(s: impl Into>) -> &'static T { + s.into().as_ref() +} + /// Fundametnally, every CallBody is a template #[derive(Default, Debug)] pub struct CallBody { pub roots: Vec, +} - // set this after - pub inline_cx: bool, +impl CallBody { + /// This will try to create a new template from the current body and the previous body. This will return None if the rsx has some dynamic part that has changed. + /// This function intentionally leaks memory to create a static template. + /// Keeping the template static allows us to simplify the core of dioxus and leaking memory in dev mode is less of an issue. + /// the previous_location is the location of the previous template at the time the template was originally compiled. + pub fn update_template( + &self, + template: Option, + location: &'static str, + ) -> Option> { + let mut renderer: TemplateRenderer = TemplateRenderer { roots: &self.roots }; + renderer.update_template::(template, location) + } } impl Parse for CallBody { @@ -56,10 +78,7 @@ impl Parse for CallBody { roots.push(node); } - Ok(Self { - roots, - inline_cx: false, - }) + Ok(Self { roots }) } } @@ -68,20 +87,29 @@ impl ToTokens for CallBody { fn to_tokens(&self, out_tokens: &mut TokenStream2) { let body = TemplateRenderer { roots: &self.roots }; - if self.inline_cx { - out_tokens.append_all(quote! { - Some({ - let __cx = cx; - #body - }) + out_tokens.append_all(quote! { + ::dioxus::core::LazyNodes::new( move | __cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { + #body }) - } else { - out_tokens.append_all(quote! { - ::dioxus::core::LazyNodes::new( move | __cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { - #body - }) + }) + } +} + +#[derive(Default, Debug)] +pub struct RenderCallBody(pub CallBody); + +impl ToTokens for RenderCallBody { + fn to_tokens(&self, out_tokens: &mut TokenStream2) { + let body: TemplateRenderer = TemplateRenderer { + roots: &self.0.roots, + }; + + out_tokens.append_all(quote! { + Some({ + let __cx = cx; + #body }) - } + }) } } @@ -89,15 +117,49 @@ pub struct TemplateRenderer<'a> { pub roots: &'a [BodyNode], } +impl<'a> TemplateRenderer<'a> { + fn update_template( + &mut self, + previous_call: Option, + location: &'static str, + ) -> Option> { + let mut mapping = previous_call.map(|call| DynamicMapping::from(call.roots)); + + let mut context = DynamicContext::default(); + + let mut roots = Vec::new(); + for (idx, root) in self.roots.iter().enumerate() { + context.current_path.push(idx as u8); + roots.push(context.update_node::(root, &mut mapping)?); + context.current_path.pop(); + } + + Some(Template { + name: location, + roots: intern(roots.as_slice()), + node_paths: intern( + context + .node_paths + .into_iter() + .map(|path| intern(path.as_slice())) + .collect::>() + .as_slice(), + ), + attr_paths: intern( + context + .attr_paths + .into_iter() + .map(|path| intern(path.as_slice())) + .collect::>() + .as_slice(), + ), + }) + } +} + impl<'a> ToTokens for TemplateRenderer<'a> { fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let mut context = DynamicContext { - dynamic_nodes: vec![], - dynamic_attributes: vec![], - current_path: vec![], - attr_paths: vec![], - node_paths: vec![], - }; + let mut context = DynamicContext::default(); let key = match self.roots.get(0) { Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key.clone(), @@ -111,7 +173,10 @@ impl<'a> ToTokens for TemplateRenderer<'a> { }; let spndbg = format!("{:?}", self.roots[0].span()); - let root_col = spndbg[9..].split("..").next().unwrap(); + let root_col = spndbg + .rsplit_once("..") + .and_then(|(_, after)| after.split_once(')').map(|(before, _)| before)) + .unwrap_or_default(); let root_printer = self.roots.iter().enumerate().map(|(idx, root)| { context.current_path.push(idx as u8); @@ -121,7 +186,6 @@ impl<'a> ToTokens for TemplateRenderer<'a> { }); // Render and release the mutable borrow on context - let num_roots = self.roots.len(); let roots = quote! { #( #root_printer ),* }; let node_printer = &context.dynamic_nodes; let dyn_attr_printer = &context.dynamic_attributes; @@ -146,17 +210,106 @@ impl<'a> ToTokens for TemplateRenderer<'a> { ::dioxus::core::VNode { parent: None, key: #key_tokens, - template: TEMPLATE, - root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([None; #num_roots]) as &mut _).as_slice_of_cells(), - // root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([None; #num_roots]) as &mut [::dioxus::core::ElementId]).as_slice_of_cells(), + template: std::cell::Cell::new(TEMPLATE), + root_ids: Default::default(), dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]), dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]), } }); } } -// As we print out the dynamic nodes, we want to keep track of them in a linear fashion -// We'll use the size of the vecs to determine the index of the dynamic node in the final + +#[derive(Default, Debug)] +struct DynamicMapping { + attribute_to_idx: HashMap>, + last_attribute_idx: usize, + node_to_idx: HashMap>, + last_element_idx: usize, +} + +impl DynamicMapping { + fn from(nodes: Vec) -> Self { + let mut new = Self::default(); + for node in nodes { + new.add_node(node); + } + new + } + + fn get_attribute_idx(&mut self, attr: &ElementAttr) -> Option { + self.attribute_to_idx + .get_mut(attr) + .and_then(|idxs| idxs.pop()) + } + + fn get_node_idx(&mut self, node: &BodyNode) -> Option { + self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop()) + } + + fn insert_attribute(&mut self, attr: ElementAttr) -> usize { + let idx = self.last_attribute_idx; + self.last_attribute_idx += 1; + + self.attribute_to_idx + .entry(attr) + .or_insert_with(Vec::new) + .push(idx); + + idx + } + + fn insert_node(&mut self, node: BodyNode) -> usize { + let idx = self.last_element_idx; + self.last_element_idx += 1; + + self.node_to_idx + .entry(node) + .or_insert_with(Vec::new) + .push(idx); + + idx + } + + fn add_node(&mut self, node: BodyNode) { + match node { + BodyNode::Element(el) => { + for attr in el.attributes { + match &attr.attr { + ElementAttr::CustomAttrText { value, .. } + | ElementAttr::AttrText { value, .. } + if value.is_static() => {} + + ElementAttr::AttrExpression { .. } + | ElementAttr::AttrText { .. } + | ElementAttr::CustomAttrText { .. } + | ElementAttr::CustomAttrExpression { .. } + | ElementAttr::EventTokens { .. } => { + self.insert_attribute(attr.attr); + } + } + } + + for child in el.children { + self.add_node(child); + } + } + + BodyNode::Text(text) if text.is_static() => {} + + BodyNode::RawExpr(_) + | BodyNode::Text(_) + | BodyNode::ForLoop(_) + | BodyNode::IfChain(_) + | BodyNode::Component(_) => { + self.insert_node(node); + } + } + } +} + +// As we create the dynamic nodes, we want to keep track of them in a linear fashion +// We'll use the size of the vecs to determine the index of the dynamic node in the final output +#[derive(Default, Debug)] pub struct DynamicContext<'a> { dynamic_nodes: Vec<&'a BodyNode>, dynamic_attributes: Vec<&'a ElementAttrNamed>, @@ -167,20 +320,112 @@ pub struct DynamicContext<'a> { } impl<'a> DynamicContext<'a> { + fn update_node( + &mut self, + root: &'a BodyNode, + mapping: &mut Option, + ) -> Option> { + match root { + BodyNode::Element(el) => { + let element_name_rust = el.name.to_string(); + + let mut static_attrs = Vec::new(); + for attr in &el.attributes { + match &attr.attr { + ElementAttr::AttrText { name, value } if value.is_static() => { + let value = value.source.as_ref().unwrap(); + let attribute_name_rust = name.to_string(); + let (name, namespace) = + Ctx::map_attribute(&element_name_rust, &attribute_name_rust) + .unwrap_or((intern(attribute_name_rust.as_str()), None)); + static_attrs.push(TemplateAttribute::Static { + name, + namespace, + value: intern(value.value().as_str()), + }) + } + + ElementAttr::CustomAttrText { name, value } if value.is_static() => { + let value = value.source.as_ref().unwrap(); + static_attrs.push(TemplateAttribute::Static { + name: intern(name.value().as_str()), + namespace: None, + value: intern(value.value().as_str()), + }) + } + + ElementAttr::AttrExpression { .. } + | ElementAttr::AttrText { .. } + | ElementAttr::CustomAttrText { .. } + | ElementAttr::CustomAttrExpression { .. } + | ElementAttr::EventTokens { .. } => { + let idx = match mapping { + Some(mapping) => mapping.get_attribute_idx(&attr.attr)?, + None => self.dynamic_attributes.len(), + }; + self.dynamic_attributes.push(attr); + + if self.attr_paths.len() <= idx { + self.attr_paths.resize_with(idx + 1, Vec::new); + } + self.attr_paths[idx] = self.current_path.clone(); + static_attrs.push(TemplateAttribute::Dynamic { id: idx }) + } + } + } + + let mut children = Vec::new(); + for (idx, root) in el.children.iter().enumerate() { + self.current_path.push(idx as u8); + children.push(self.update_node::(root, mapping)?); + self.current_path.pop(); + } + + let (tag, namespace) = Ctx::map_element(&element_name_rust) + .unwrap_or((intern(element_name_rust.as_str()), None)); + Some(TemplateNode::Element { + tag, + namespace, + attrs: intern(static_attrs.into_boxed_slice()), + children: intern(children.as_slice()), + }) + } + + BodyNode::Text(text) if text.is_static() => { + let text = text.source.as_ref().unwrap(); + Some(TemplateNode::Text { + text: intern(text.value().as_str()), + }) + } + + BodyNode::RawExpr(_) + | BodyNode::Text(_) + | BodyNode::ForLoop(_) + | BodyNode::IfChain(_) + | BodyNode::Component(_) => { + let idx = match mapping { + Some(mapping) => mapping.get_node_idx(root)?, + None => self.dynamic_nodes.len(), + }; + self.dynamic_nodes.push(root); + + if self.node_paths.len() <= idx { + self.node_paths.resize_with(idx + 1, Vec::new); + } + self.node_paths[idx] = self.current_path.clone(); + + Some(match root { + BodyNode::Text(_) => TemplateNode::DynamicText { id: idx }, + _ => TemplateNode::Dynamic { id: idx }, + }) + } + } + } + fn render_static_node(&mut self, root: &'a BodyNode) -> TokenStream2 { match root { BodyNode::Element(el) => { let el_name = &el.name; - - // dynamic attributes - // [0] - // [0, 1] - // [0, 1] - // [0, 1] - // [0, 1, 2] - // [0, 2] - // [0, 2, 1] - let static_attrs = el.attributes.iter().map(|attr| match &attr.attr { ElementAttr::AttrText { name, value } if value.is_static() => { let value = value.to_static().unwrap(); @@ -268,3 +513,209 @@ impl<'a> DynamicContext<'a> { } } } + +#[test] +fn create_template() { + let input = quote! { + svg { + width: 100, + height: "100px", + "width2": 100, + "height2": "100px", + p { + "hello world" + } + (0..10).map(|i| rsx!{"{i}"}) + } + }; + + struct Mock; + + impl HotReloadingContext for Mock { + fn map_attribute( + element_name_rust: &str, + attribute_name_rust: &str, + ) -> Option<(&'static str, Option<&'static str>)> { + match element_name_rust { + "svg" => match attribute_name_rust { + "width" => Some(("width", Some("style"))), + "height" => Some(("height", Some("style"))), + _ => None, + }, + _ => None, + } + } + + fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)> { + match element_name_rust { + "svg" => Some(("svg", Some("svg"))), + _ => None, + } + } + } + + let call_body: CallBody = syn::parse2(input).unwrap(); + + let template = call_body.update_template::(None, "testing").unwrap(); + + dbg!(template); + + assert_eq!( + template, + Template { + name: "testing", + roots: &[TemplateNode::Element { + tag: "svg", + namespace: Some("svg"), + attrs: &[ + TemplateAttribute::Dynamic { id: 0 }, + TemplateAttribute::Static { + name: "height", + namespace: Some("style"), + value: "100px", + }, + TemplateAttribute::Dynamic { id: 1 }, + TemplateAttribute::Static { + name: "height2", + namespace: None, + value: "100px", + }, + ], + children: &[ + TemplateNode::Element { + tag: "p", + namespace: None, + attrs: &[], + children: &[TemplateNode::Text { + text: "hello world", + }], + }, + TemplateNode::Dynamic { id: 0 } + ], + }], + node_paths: &[&[0, 1,],], + attr_paths: &[&[0,], &[0,],], + }, + ) +} + +#[test] +fn diff_template() { + use dioxus_core::Scope; + #[allow(unused, non_snake_case)] + fn Comp(_: Scope) -> dioxus_core::Element { + None + } + + let input = quote! { + svg { + width: 100, + height: "100px", + "width2": 100, + "height2": "100px", + p { + "hello world" + } + (0..10).map(|i| rsx!{"{i}"}), + (0..10).map(|i| rsx!{"{i}"}), + (0..11).map(|i| rsx!{"{i}"}), + Comp{} + } + }; + + #[derive(Debug)] + struct Mock; + + impl HotReloadingContext for Mock { + fn map_attribute( + element_name_rust: &str, + attribute_name_rust: &str, + ) -> Option<(&'static str, Option<&'static str>)> { + match element_name_rust { + "svg" => match attribute_name_rust { + "width" => Some(("width", Some("style"))), + "height" => Some(("height", Some("style"))), + _ => None, + }, + _ => None, + } + } + + fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)> { + match element_name_rust { + "svg" => Some(("svg", Some("svg"))), + _ => None, + } + } + } + + let call_body1: CallBody = syn::parse2(input).unwrap(); + + let template = call_body1.update_template::(None, "testing").unwrap(); + dbg!(template); + + // scrambling the attributes should not cause a full rebuild + let input = quote! { + div { + "width2": 100, + height: "100px", + "height2": "100px", + width: 100, + Comp{} + (0..11).map(|i| rsx!{"{i}"}), + (0..10).map(|i| rsx!{"{i}"}), + (0..10).map(|i| rsx!{"{i}"}), + p { + "hello world" + } + } + }; + + let call_body2: CallBody = syn::parse2(input).unwrap(); + + let template = call_body2 + .update_template::(Some(call_body1), "testing") + .unwrap(); + dbg!(template); + + assert_eq!( + template, + Template { + name: "testing", + roots: &[TemplateNode::Element { + tag: "div", + namespace: None, + attrs: &[ + TemplateAttribute::Dynamic { id: 1 }, + TemplateAttribute::Static { + name: "height", + namespace: None, + value: "100px", + }, + TemplateAttribute::Static { + name: "height2", + namespace: None, + value: "100px", + }, + TemplateAttribute::Dynamic { id: 0 }, + ], + children: &[ + TemplateNode::Dynamic { id: 3 }, + TemplateNode::Dynamic { id: 2 }, + TemplateNode::Dynamic { id: 1 }, + TemplateNode::Dynamic { id: 0 }, + TemplateNode::Element { + tag: "p", + namespace: None, + attrs: &[], + children: &[TemplateNode::Text { + text: "hello world", + }], + }, + ], + }], + node_paths: &[&[0, 3], &[0, 2], &[0, 1], &[0, 0]], + attr_paths: &[&[0], &[0]] + }, + ) +} diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index 29f18fa31..0d5556552 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -1,5 +1,3 @@ -// use crate::{raw_expr::RawExprNode, text::TextNode}; - use super::*; use proc_macro2::{Span, TokenStream as TokenStream2}; @@ -97,25 +95,7 @@ impl Parse for BodyNode { // Transform for loops into into_iter calls if stream.peek(Token![for]) { - let _f = stream.parse::()?; - let pat = stream.parse::()?; - let _i = stream.parse::()?; - let expr = stream.parse::>()?; - - let body; - braced!(body in stream); - let mut children = vec![]; - while !body.is_empty() { - children.push(body.parse()?); - } - - return Ok(BodyNode::ForLoop(ForLoop { - for_token: _f, - pat, - in_token: _i, - expr, - body: children, - })); + return Ok(BodyNode::ForLoop(stream.parse()?)); } // Transform unterminated if statements into terminated optional if statements @@ -143,7 +123,7 @@ impl ToTokens for BodyNode { pat, expr, body, .. } = exp; - let renderer = TemplateRenderer { roots: body }; + let renderer: TemplateRenderer = TemplateRenderer { roots: body }; tokens.append_all(quote! { __cx.make_node( @@ -223,6 +203,36 @@ pub struct ForLoop { pub in_token: Token![in], pub expr: Box, pub body: Vec, + pub brace_token: token::Brace, +} + +impl Parse for ForLoop { + fn parse(input: ParseStream) -> Result { + let for_token: Token![for] = input.parse()?; + + let pat = input.parse()?; + + let in_token: Token![in] = input.parse()?; + let expr: Expr = input.call(Expr::parse_without_eager_brace)?; + + let content; + let brace_token = braced!(content in input); + + let mut children = vec![]; + + while !content.is_empty() { + children.push(content.parse()?); + } + + Ok(Self { + for_token, + pat, + in_token, + body: children, + expr: Box::new(expr), + brace_token, + }) + } } fn is_if_chain_terminated(chain: &ExprIf) -> bool { diff --git a/packages/rsx/src/template.rs b/packages/rsx/src/template.rs deleted file mode 100644 index 22c73ae33..000000000 --- a/packages/rsx/src/template.rs +++ /dev/null @@ -1,1064 +0,0 @@ -// use proc_macro2::TokenStream; -// use quote::TokenStreamExt; -// use quote::{quote, ToTokens}; -// use syn::{Expr, Ident, LitStr}; - -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// pub fn try_parse_template( -// rsx: &str, -// location: OwnedCodeLocation, -// previous_template: Option, -// ) -> Result<(OwnedTemplate, DynamicTemplateContextBuilder), Error> { -// use crate::CallBody; - -// let call_body: CallBody = -// syn::parse_str(rsx).map_err(|e| Error::ParseError(ParseError::new(e, location.clone())))?; -// let mut template_builder = TemplateBuilder::from_roots_always(call_body.roots); -// if let Some(prev) = previous_template { -// template_builder = template_builder -// .try_switch_dynamic_context(prev) -// .ok_or_else(|| { -// Error::RecompileRequiredError(RecompileReason::Variable( -// "dynamic context updated".to_string(), -// )) -// })?; -// } -// let dyn_ctx = template_builder.dynamic_context.clone(); -// Ok((template_builder.try_into_owned(&location)?, dyn_ctx)) -// } - -// use crate::{BodyNode, ElementAttr, FormattedSegment, Segment}; - -// struct TemplateElementBuilder { -// tag: Ident, -// attributes: Vec, -// children: Vec, -// listeners: Vec, -// locally_static: bool, -// } - -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// type OwndedTemplateElement = TemplateElement< -// Vec>, -// OwnedAttributeValue, -// Vec, -// Vec, -// >; - -// impl TemplateElementBuilder { -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// fn try_into_owned(self, location: &OwnedCodeLocation) -> Result { -// let Self { -// tag, -// attributes, -// children, -// listeners, -// .. -// } = self; -// let (element_tag, element_ns) = -// element_to_static_str(&tag.to_string()).ok_or_else(|| { -// Error::ParseError(ParseError::new( -// syn::Error::new(tag.span(), format!("unknown element: {}", tag)), -// location.clone(), -// )) -// })?; - -// let mut owned_attributes = Vec::new(); -// for a in attributes { -// owned_attributes.push(a.try_into_owned(location, element_tag, element_ns)?); -// } - -// Ok(TemplateElement::new( -// element_tag, -// element_ns, -// owned_attributes, -// children, -// listeners, -// )) -// } -// } - -// impl ToTokens for TemplateElementBuilder { -// fn to_tokens(&self, tokens: &mut TokenStream) { -// let Self { -// tag, -// attributes, -// children, -// listeners, -// .. -// } = self; -// let children = children.iter().map(|id| { -// let raw = id.0; -// quote! {TemplateNodeId(#raw)} -// }); -// tokens.append_all(quote! { -// TemplateElement::new( -// dioxus_elements::#tag::TAG_NAME, -// dioxus_elements::#tag::NAME_SPACE, -// &[#(#attributes),*], -// &[#(#children),*], -// &[#(#listeners),*], -// ) -// }) -// } -// } - -// enum AttributeName { -// Ident(Ident), -// Str(LitStr), -// } - -// struct TemplateAttributeBuilder { -// element_tag: Ident, -// name: AttributeName, -// value: TemplateAttributeValue, -// } - -// impl TemplateAttributeBuilder { -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// fn try_into_owned( -// self, -// location: &OwnedCodeLocation, -// element_tag: &'static str, -// element_ns: Option<&'static str>, -// ) -> Result, Error> { -// let Self { name, value, .. } = self; -// let (name, span, literal) = match name { -// AttributeName::Ident(name) => (name.to_string(), name.span(), false), -// AttributeName::Str(name) => (name.value(), name.span(), true), -// }; -// let (name, namespace, volatile) = attrbute_to_static_str(&name, element_tag, element_ns) -// .ok_or_else(|| { -// if literal { -// // literals will be captured when a full recompile is triggered -// Error::RecompileRequiredError(RecompileReason::Attribute(name.to_string())) -// } else { -// Error::ParseError(ParseError::new( -// syn::Error::new(span, format!("unknown attribute: {}", name)), -// location.clone(), -// )) -// } -// })?; -// let attribute = AttributeDiscription { -// name, -// namespace, -// volatile, -// }; -// Ok(TemplateAttribute { value, attribute }) -// } -// } - -// impl ToTokens for TemplateAttributeBuilder { -// fn to_tokens(&self, tokens: &mut TokenStream) { -// let Self { -// element_tag, -// name, -// value, -// } = self; -// let value = match value { -// TemplateAttributeValue::Static(val) => { -// let val = match val { -// OwnedAttributeValue::Text(txt) => quote! {StaticAttributeValue::Text(#txt)}, -// OwnedAttributeValue::Float32(f) => quote! {StaticAttributeValue::Float32(#f)}, -// OwnedAttributeValue::Float64(f) => quote! {StaticAttributeValue::Float64(#f)}, -// OwnedAttributeValue::Int32(i) => quote! {StaticAttributeValue::Int32(#i)}, -// OwnedAttributeValue::Int64(i) => quote! {StaticAttributeValue::Int64(#i)}, -// OwnedAttributeValue::Uint32(u) => quote! {StaticAttributeValue::Uint32(#u)}, -// OwnedAttributeValue::Uint64(u) => quote! {StaticAttributeValue::Uint64(#u)}, -// OwnedAttributeValue::Bool(b) => quote! {StaticAttributeValue::Bool(#b)}, -// OwnedAttributeValue::Vec3Float(f1, f2, f3) => { -// quote! {StaticAttributeValue::Vec3Float(#f1, #f2, #f3)} -// } -// OwnedAttributeValue::Vec3Int(f1, f2, f3) => { -// quote! {StaticAttributeValue::Vec3Int(#f1, #f2, #f3)} -// } -// OwnedAttributeValue::Vec3Uint(f1, f2, f3) => { -// quote! {StaticAttributeValue::Vec3Uint(#f1, #f2, #f3)} -// } -// OwnedAttributeValue::Vec4Float(f1, f2, f3, f4) => { -// quote! {StaticAttributeValue::Vec4Float(#f1, #f2, #f3, #f4)} -// } -// OwnedAttributeValue::Vec4Int(f1, f2, f3, f4) => { -// quote! {StaticAttributeValue::Vec4Int(#f1, #f2, #f3, #f4)} -// } -// OwnedAttributeValue::Vec4Uint(f1, f2, f3, f4) => { -// quote! {StaticAttributeValue::Vec4Uint(#f1, #f2, #f3, #f4)} -// } -// OwnedAttributeValue::Bytes(b) => { -// quote! {StaticAttributeValue::Bytes(&[#(#b),*])} -// } -// OwnedAttributeValue::Any(_) => todo!(), -// }; -// quote! {TemplateAttributeValue::Static(#val)} -// } -// TemplateAttributeValue::Dynamic(idx) => quote! {TemplateAttributeValue::Dynamic(#idx)}, -// }; -// match name { -// AttributeName::Ident(name) => tokens.append_all(quote! { -// TemplateAttribute{ -// attribute: dioxus_elements::#element_tag::#name, -// value: #value, -// } -// }), -// AttributeName::Str(lit) => tokens.append_all(quote! { -// TemplateAttribute{ -// attribute: dioxus::prelude::AttributeDiscription{ -// name: #lit, -// namespace: None, -// volatile: false -// }, -// value: #value, -// } -// }), -// } -// } -// } - -// enum TemplateNodeTypeBuilder { -// Element(TemplateElementBuilder), -// Text(TextTemplate>, String>), -// DynamicNode(usize), -// } - -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// type OwnedTemplateNodeType = TemplateNodeType< -// Vec>, -// OwnedAttributeValue, -// Vec, -// Vec, -// Vec>, -// String, -// >; - -// impl TemplateNodeTypeBuilder { -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// fn try_into_owned(self, location: &OwnedCodeLocation) -> Result { -// match self { -// TemplateNodeTypeBuilder::Element(el) => { -// Ok(TemplateNodeType::Element(el.try_into_owned(location)?)) -// } -// TemplateNodeTypeBuilder::Text(txt) => Ok(TemplateNodeType::Text(txt)), -// TemplateNodeTypeBuilder::DynamicNode(idx) => Ok(TemplateNodeType::DynamicNode(idx)), -// } -// } -// } - -// impl ToTokens for TemplateNodeTypeBuilder { -// fn to_tokens(&self, tokens: &mut TokenStream) { -// match self { -// TemplateNodeTypeBuilder::Element(el) => tokens.append_all(quote! { -// TemplateNodeType::Element(#el) -// }), -// TemplateNodeTypeBuilder::Text(txt) => { -// let mut length = 0; - -// let segments = txt.segments.iter().map(|seg| match seg { -// TextTemplateSegment::Static(s) => { -// length += s.len(); -// quote!(TextTemplateSegment::Static(#s)) -// } -// TextTemplateSegment::Dynamic(idx) => quote!(TextTemplateSegment::Dynamic(#idx)), -// }); - -// tokens.append_all(quote! { -// TemplateNodeType::Text(TextTemplate::new(&[#(#segments),*], #length)) -// }); -// } -// TemplateNodeTypeBuilder::DynamicNode(idx) => tokens.append_all(quote! { -// TemplateNodeType::DynamicNode(#idx) -// }), -// } -// } -// } - -// struct TemplateNodeBuilder { -// id: TemplateNodeId, -// depth: usize, -// parent: Option, -// node_type: TemplateNodeTypeBuilder, -// fully_static: bool, -// } - -// impl TemplateNodeBuilder { -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// fn try_into_owned(self, location: &OwnedCodeLocation) -> Result { -// let TemplateNodeBuilder { -// id, -// node_type, -// parent, -// depth, -// .. -// } = self; -// let node_type = node_type.try_into_owned(location)?; -// Ok(OwnedTemplateNode { -// id, -// node_type, -// parent, -// depth, -// }) -// } - -// fn to_tokens(&self, tokens: &mut TokenStream) { -// let Self { -// id, -// node_type, -// parent, -// depth, -// .. -// } = self; -// let raw_id = id.0; -// let parent = match parent { -// Some(id) => { -// let id = id.0; -// quote! {Some(TemplateNodeId(#id))} -// } -// None => quote! {None}, -// }; - -// tokens.append_all(quote! { -// TemplateNode { -// id: TemplateNodeId(#raw_id), -// node_type: #node_type, -// parent: #parent, -// depth: #depth, -// } -// }) -// } -// } - -// #[derive(Default)] -// pub struct TemplateBuilder { -// nodes: Vec, -// root_nodes: Vec, -// dynamic_context: DynamicTemplateContextBuilder, -// } - -// impl TemplateBuilder { -// /// Create a template builder from nodes if it would improve performance to do so. -// pub fn from_roots(roots: Vec) -> Option { -// let mut builder = Self::default(); - -// for (i, root) in roots.into_iter().enumerate() { -// let id = builder.build_node(root, None, vec![i], 0); -// builder.root_nodes.push(id); -// } - -// // only build a template if there is at least one static node -// if builder -// .nodes -// .iter() -// .all(|r| matches!(&r.node_type, TemplateNodeTypeBuilder::DynamicNode(_))) -// { -// None -// } else { -// Some(builder) -// } -// } - -// /// Create a template builder from nodes regardless of performance. -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// fn from_roots_always(roots: Vec) -> Self { -// let mut builder = Self::default(); - -// for (i, root) in roots.into_iter().enumerate() { -// let id = builder.build_node(root, None, vec![i], 0); -// builder.root_nodes.push(id); -// } - -// builder -// } - -// fn build_node( -// &mut self, -// node: BodyNode, -// parent: Option, -// path: Vec, -// depth: usize, -// ) -> TemplateNodeId { -// let id = TemplateNodeId(self.nodes.len()); -// match node { -// BodyNode::Element(el) => { -// let mut locally_static = true; -// let mut attributes = Vec::new(); -// let mut listeners = Vec::new(); -// for attr in el.attributes { -// match attr.attr { -// ElementAttr::AttrText { name, value } => { -// if let Some(static_value) = value.to_static() { -// attributes.push(TemplateAttributeBuilder { -// element_tag: el.name.clone(), -// name: AttributeName::Ident(name), -// value: TemplateAttributeValue::Static( -// OwnedAttributeValue::Text(static_value), -// ), -// }) -// } else { -// locally_static = false; -// attributes.push(TemplateAttributeBuilder { -// element_tag: el.name.clone(), -// name: AttributeName::Ident(name), -// value: TemplateAttributeValue::Dynamic( -// self.dynamic_context.add_attr(quote!(#value)), -// ), -// }) -// } -// } -// ElementAttr::CustomAttrText { name, value } => { -// if let Some(static_value) = value.to_static() { -// attributes.push(TemplateAttributeBuilder { -// element_tag: el.name.clone(), -// name: AttributeName::Str(name), -// value: TemplateAttributeValue::Static( -// OwnedAttributeValue::Text(static_value), -// ), -// }) -// } else { -// locally_static = false; -// attributes.push(TemplateAttributeBuilder { -// element_tag: el.name.clone(), -// name: AttributeName::Str(name), -// value: TemplateAttributeValue::Dynamic( -// self.dynamic_context.add_attr(quote!(#value)), -// ), -// }) -// } -// } -// ElementAttr::AttrExpression { name, value } => { -// locally_static = false; -// attributes.push(TemplateAttributeBuilder { -// element_tag: el.name.clone(), -// name: AttributeName::Ident(name), -// value: TemplateAttributeValue::Dynamic( -// self.dynamic_context.add_attr(quote!(#value)), -// ), -// }) -// } -// ElementAttr::CustomAttrExpression { name, value } => { -// locally_static = false; -// attributes.push(TemplateAttributeBuilder { -// element_tag: el.name.clone(), -// name: AttributeName::Str(name), -// value: TemplateAttributeValue::Dynamic( -// self.dynamic_context.add_attr(quote!(#value)), -// ), -// }) -// } -// ElementAttr::EventTokens { name, tokens } => { -// locally_static = false; -// listeners.push(self.dynamic_context.add_listener(name, tokens)) -// } -// } -// } -// if let Some(key) = el.key { -// self.dynamic_context.add_key(quote!( -// dioxus::core::exports::bumpalo::format!(in __bump, "{}", #key) -// .into_bump_str() -// )); -// } -// self.nodes.push(TemplateNodeBuilder { -// id, -// node_type: TemplateNodeTypeBuilder::Element(TemplateElementBuilder { -// tag: el.name, -// attributes, -// children: Vec::new(), -// listeners, -// locally_static, -// }), -// parent, -// depth, -// fully_static: false, -// }); -// let children: Vec<_> = el -// .children -// .into_iter() -// .enumerate() -// .map(|(i, child)| { -// let mut new_path = path.clone(); -// new_path.push(i); -// self.build_node(child, Some(id), new_path, depth + 1) -// }) -// .collect(); -// let children_fully_static = children.iter().all(|c| self.nodes[c.0].fully_static); -// let parent = &mut self.nodes[id.0]; -// parent.fully_static = locally_static && children_fully_static; -// if let TemplateNodeTypeBuilder::Element(element) = &mut parent.node_type { -// element.children = children; -// } -// } - -// BodyNode::Component(comp) => { -// self.nodes.push(TemplateNodeBuilder { -// id, -// node_type: TemplateNodeTypeBuilder::DynamicNode( -// self.dynamic_context.add_node(BodyNode::Component(comp)), -// ), -// parent, -// depth, -// fully_static: false, -// }); -// } - -// BodyNode::Text(txt) => { -// let mut segments = Vec::new(); -// let mut length = 0; -// let mut fully_static = true; - -// for segment in txt.segments { -// segments.push(match segment { -// Segment::Literal(lit) => { -// length += lit.len(); -// TextTemplateSegment::Static(lit) -// } -// Segment::Formatted(fmted) => { -// fully_static = false; -// TextTemplateSegment::Dynamic(self.dynamic_context.add_text(fmted)) -// } -// }) -// } - -// self.nodes.push(TemplateNodeBuilder { -// id, -// node_type: TemplateNodeTypeBuilder::Text(TextTemplate::new(segments, length)), -// parent, -// depth, -// fully_static, -// }); -// } - -// BodyNode::RawExpr(expr) => { -// self.nodes.push(TemplateNodeBuilder { -// id, -// node_type: TemplateNodeTypeBuilder::DynamicNode( -// self.dynamic_context.add_node(BodyNode::RawExpr(expr)), -// ), -// parent, -// depth, -// fully_static: false, -// }); -// } -// } -// id -// } - -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// pub fn try_switch_dynamic_context( -// mut self, -// dynamic_context: DynamicTemplateContextBuilder, -// ) -> Option { -// let attribute_mapping: HashMap = dynamic_context -// .attributes -// .iter() -// .enumerate() -// .map(|(i, ts)| (ts.to_string(), i)) -// .collect(); -// let text_mapping: HashMap = dynamic_context -// .text -// .iter() -// .enumerate() -// .map(|(i, ts)| (ts.to_token_stream().to_string(), i)) -// .collect(); -// let listener_mapping: HashMap<(String, Expr), usize> = dynamic_context -// .listeners -// .iter() -// .enumerate() -// .map(|(i, ts)| (ts.clone(), i)) -// .collect(); -// let node_mapping: HashMap = dynamic_context -// .nodes -// .iter() -// .enumerate() -// .map(|(i, ts)| (ts.to_token_stream().to_string(), i)) -// .collect(); - -// for node in &mut self.nodes { -// match &mut node.node_type { -// TemplateNodeTypeBuilder::Element(element) => { -// for listener in &mut element.listeners { -// *listener = -// *listener_mapping.get(&self.dynamic_context.listeners[*listener])?; -// } -// for attribute in &mut element.attributes { -// if let TemplateAttributeValue::Dynamic(idx) = &mut attribute.value { -// *idx = *attribute_mapping -// .get(&self.dynamic_context.attributes[*idx].to_string())?; -// } -// } -// } -// TemplateNodeTypeBuilder::Text(txt) => { -// for seg in &mut txt.segments { -// if let TextTemplateSegment::Dynamic(idx) = seg { -// *idx = *text_mapping.get( -// &self.dynamic_context.text[*idx] -// .to_token_stream() -// .to_string(), -// )?; -// } -// } -// } -// TemplateNodeTypeBuilder::DynamicNode(idx) => { -// *idx = *node_mapping.get( -// &self.dynamic_context.nodes[*idx] -// .to_token_stream() -// .to_string(), -// )?; -// } -// } -// } -// self.dynamic_context = dynamic_context; - -// Some(self) -// } - -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// pub fn try_into_owned(self, location: &OwnedCodeLocation) -> Result { -// let mut nodes = Vec::new(); -// let dynamic_mapping = self.dynamic_mapping(&nodes); -// let dynamic_path = self.dynamic_path(); -// for node in self.nodes { -// nodes.push(node.try_into_owned(location)?); -// } - -// Ok(OwnedTemplate { -// nodes, -// root_nodes: self.root_nodes, -// dynamic_mapping, -// dynamic_path, -// }) -// } - -// #[cfg(any(feature = "hot-reload", debug_assertions))] -// pub fn dynamic_mapping( -// &self, -// resolved_nodes: &Vec, -// ) -> OwnedDynamicNodeMapping { -// let dynamic_context = &self.dynamic_context; -// let mut node_mapping = vec![None; dynamic_context.nodes.len()]; -// let nodes = &self.nodes; -// for n in nodes { -// if let TemplateNodeTypeBuilder::DynamicNode(idx) = &n.node_type { -// node_mapping[*idx] = Some(n.id) -// } -// } -// let mut text_mapping = vec![Vec::new(); dynamic_context.text.len()]; -// for n in nodes { -// if let TemplateNodeTypeBuilder::Text(txt) = &n.node_type { -// for seg in &txt.segments { -// match seg { -// TextTemplateSegment::Static(_) => (), -// TextTemplateSegment::Dynamic(idx) => text_mapping[*idx].push(n.id), -// } -// } -// } -// } -// let mut attribute_mapping = vec![Vec::new(); dynamic_context.attributes.len()]; -// for n in nodes { -// if let TemplateNodeTypeBuilder::Element(el) = &n.node_type { -// for (i, attr) in el.attributes.iter().enumerate() { -// match attr.value { -// TemplateAttributeValue::Static(_) => (), -// TemplateAttributeValue::Dynamic(idx) => { -// attribute_mapping[idx].push((n.id, i)); -// } -// } -// } -// } -// } -// let mut listener_mapping = Vec::new(); -// for n in nodes { -// if let TemplateNodeTypeBuilder::Element(el) = &n.node_type { -// if !el.listeners.is_empty() { -// listener_mapping.push(n.id); -// } -// } -// } - -// let mut volatile_mapping = Vec::new(); -// for n in resolved_nodes { -// if let TemplateNodeType::Element(el) = &n.node_type { -// for (i, attr) in el.attributes.iter().enumerate() { -// if attr.attribute.volatile { -// volatile_mapping.push((n.id, i)); -// } -// } -// } -// } - -// OwnedDynamicNodeMapping::new( -// node_mapping, -// text_mapping, -// attribute_mapping, -// volatile_mapping, -// listener_mapping, -// ) -// } - -// fn dynamic_path(&self) -> Option { -// let mut last_seg: Option = None; -// let mut nodes_to_insert_after = Vec::new(); -// // iterating from the last root to the first -// for root in self.root_nodes.iter().rev() { -// let root_node = &self.nodes[root.0]; -// if let TemplateNodeTypeBuilder::DynamicNode(_) = root_node.node_type { -// match &mut last_seg { -// // if there has been no static nodes, we can append the child to the parent node -// None => nodes_to_insert_after.push(*root), -// // otherwise we insert the child before the last static node -// Some(seg) => { -// seg.ops.push(UpdateOp::InsertBefore(*root)); -// } -// } -// } else if let Some(mut new) = self.construct_path_segment(*root) { -// if let Some(last) = last_seg.take() { -// match new.traverse { -// OwnedTraverse::Halt => { -// new.traverse = OwnedTraverse::NextSibling(Box::new(last)); -// } -// OwnedTraverse::FirstChild(b) => { -// new.traverse = OwnedTraverse::Both(Box::new((*b, last))); -// } -// _ => unreachable!(), -// } -// } else { -// for node in nodes_to_insert_after.drain(..) { -// new.ops.push(UpdateOp::InsertAfter(node)); -// } -// } -// last_seg = Some(new); -// } else if let Some(last) = last_seg.take() { -// last_seg = Some(OwnedPathSeg { -// ops: Vec::new(), -// traverse: OwnedTraverse::NextSibling(Box::new(last)), -// }); -// } -// } -// last_seg -// } - -// fn construct_path_segment(&self, node_id: TemplateNodeId) -> Option { -// let n = &self.nodes[node_id.0]; -// if n.fully_static { -// return None; -// } -// match &n.node_type { -// TemplateNodeTypeBuilder::Element(el) => { -// let mut last_seg: Option = None; -// let mut children_to_append = Vec::new(); -// // iterating from the last child to the first -// for child in el.children.iter().rev() { -// let child_node = &self.nodes[child.0]; -// if let TemplateNodeTypeBuilder::DynamicNode(_) = child_node.node_type { -// match &mut last_seg { -// // if there has been no static nodes, we can append the child to the parent node -// None => children_to_append.push(*child), -// // otherwise we insert the child before the last static node -// Some(seg) => { -// seg.ops.push(UpdateOp::InsertBefore(*child)); -// } -// } -// } else if let Some(mut new) = self.construct_path_segment(*child) { -// if let Some(last) = last_seg.take() { -// match new.traverse { -// OwnedTraverse::Halt => { -// new.traverse = OwnedTraverse::NextSibling(Box::new(last)); -// } -// OwnedTraverse::FirstChild(b) => { -// new.traverse = OwnedTraverse::Both(Box::new((*b, last))); -// } -// _ => unreachable!(), -// } -// } -// last_seg = Some(new); -// } else if let Some(last) = last_seg.take() { -// last_seg = Some(OwnedPathSeg { -// ops: Vec::new(), -// traverse: OwnedTraverse::NextSibling(Box::new(last)), -// }); -// } -// } -// let mut ops = Vec::new(); -// if !el.locally_static || n.parent.is_none() { -// ops.push(UpdateOp::StoreNode(node_id)); -// } -// for child in children_to_append.into_iter().rev() { -// ops.push(UpdateOp::AppendChild(child)); -// } -// Some(OwnedPathSeg { -// ops, -// traverse: match last_seg { -// Some(last) => OwnedTraverse::FirstChild(Box::new(last)), -// None => OwnedTraverse::Halt, -// }, -// }) -// } -// TemplateNodeTypeBuilder::Text(_) => Some(OwnedPathSeg { -// ops: vec![UpdateOp::StoreNode(n.id)], -// traverse: dioxus_core::OwnedTraverse::Halt, -// }), -// TemplateNodeTypeBuilder::DynamicNode(_) => unreachable!( -// "constructing path segment for dynamic nodes is handled in the parent node" -// ), -// } -// } -// } - -// impl ToTokens for TemplateBuilder { -// fn to_tokens(&self, tokens: &mut TokenStream) { -// let Self { -// nodes, -// root_nodes, -// dynamic_context, -// } = self; - -// let mut node_mapping = vec![None; dynamic_context.nodes.len()]; -// for n in nodes { -// if let TemplateNodeTypeBuilder::DynamicNode(idx) = &n.node_type { -// node_mapping[*idx] = Some(n.id); -// } -// } -// let mut text_mapping = vec![Vec::new(); dynamic_context.text.len()]; -// for n in nodes { -// if let TemplateNodeTypeBuilder::Text(txt) = &n.node_type { -// for seg in &txt.segments { -// match seg { -// TextTemplateSegment::Static(_) => (), -// TextTemplateSegment::Dynamic(idx) => text_mapping[*idx].push(n.id), -// } -// } -// } -// } -// let mut attribute_mapping = vec![Vec::new(); dynamic_context.attributes.len()]; -// for n in nodes { -// if let TemplateNodeTypeBuilder::Element(el) = &n.node_type { -// for (i, attr) in el.attributes.iter().enumerate() { -// match attr.value { -// TemplateAttributeValue::Static(_) => (), -// TemplateAttributeValue::Dynamic(idx) => { -// attribute_mapping[idx].push((n.id, i)); -// } -// } -// } -// } -// } -// let mut listener_mapping = Vec::new(); -// for n in nodes { -// if let TemplateNodeTypeBuilder::Element(el) = &n.node_type { -// if !el.listeners.is_empty() { -// listener_mapping.push(n.id); -// } -// } -// } - -// let root_nodes = root_nodes.iter().map(|id| { -// let raw = id.0; -// quote! { TemplateNodeId(#raw) } -// }); -// let node_mapping_quoted = node_mapping.iter().map(|op| match op { -// Some(id) => { -// let raw_id = id.0; -// quote! {Some(TemplateNodeId(#raw_id))} -// } -// None => quote! {None}, -// }); -// let text_mapping_quoted = text_mapping.iter().map(|inner| { -// let raw = inner.iter().map(|id| id.0); -// quote! {&[#(TemplateNodeId(#raw)),*]} -// }); -// let attribute_mapping_quoted = attribute_mapping.iter().map(|inner| { -// let raw = inner.iter().map(|(id, _)| id.0); -// let indecies = inner.iter().map(|(_, idx)| idx); -// quote! {&[#((TemplateNodeId(#raw), #indecies)),*]} -// }); -// let listener_mapping_quoted = listener_mapping.iter().map(|id| { -// let raw = id.0; -// quote! {TemplateNodeId(#raw)} -// }); -// let mut nodes_quoted = TokenStream::new(); -// for n in nodes { -// n.to_tokens(&mut nodes_quoted); -// quote! {,}.to_tokens(&mut nodes_quoted); -// } - -// let dynamic_path = match self.dynamic_path() { -// Some(seg) => { -// let seg = quote_owned_segment(seg); -// quote! {Some(#seg)} -// } -// None => quote! {None}, -// }; - -// let quoted = quote! { -// { -// const __NODES: dioxus::prelude::StaticTemplateNodes = &[#nodes_quoted]; -// const __TEXT_MAPPING: &'static [&'static [dioxus::prelude::TemplateNodeId]] = &[#(#text_mapping_quoted),*]; -// const __ATTRIBUTE_MAPPING: &'static [&'static [(dioxus::prelude::TemplateNodeId, usize)]] = &[#(#attribute_mapping_quoted),*]; -// const __ROOT_NODES: &'static [dioxus::prelude::TemplateNodeId] = &[#(#root_nodes),*]; -// const __NODE_MAPPING: &'static [Option] = &[#(#node_mapping_quoted),*]; -// const __NODES_WITH_LISTENERS: &'static [dioxus::prelude::TemplateNodeId] = &[#(#listener_mapping_quoted),*]; -// static __VOLITALE_MAPPING_INNER: dioxus::core::exports::once_cell::sync::Lazy> = dioxus::core::exports::once_cell::sync::Lazy::new(||{ -// // check each property to see if it is volatile -// let mut volatile = Vec::new(); -// for n in __NODES { -// if let TemplateNodeType::Element(el) = &n.node_type { -// for (i, attr) in el.attributes.iter().enumerate() { -// if attr.attribute.volatile { -// volatile.push((n.id, i)); -// } -// } -// } -// } -// volatile -// }); -// static __VOLITALE_MAPPING: &'static dioxus::core::exports::once_cell::sync::Lazy> = &__VOLITALE_MAPPING_INNER; -// static __STATIC_VOLITALE_MAPPING: dioxus::prelude::LazyStaticVec<(dioxus::prelude::TemplateNodeId, usize)> = LazyStaticVec(__VOLITALE_MAPPING); -// static __TEMPLATE: dioxus::prelude::Template = Template::Static(&StaticTemplate { -// nodes: __NODES, -// root_nodes: __ROOT_NODES, -// dynamic_mapping: StaticDynamicNodeMapping::new(__NODE_MAPPING, __TEXT_MAPPING, __ATTRIBUTE_MAPPING, __STATIC_VOLITALE_MAPPING, __NODES_WITH_LISTENERS), -// dynamic_path: #dynamic_path, -// }); - -// let __bump = __cx.bump(); -// __cx.template_ref(dioxus::prelude::TemplateId(get_line_num!()), __TEMPLATE.clone(), #dynamic_context) -// } -// }; - -// tokens.append_all(quoted) -// } -// } - -// #[derive(Default, Clone, Debug)] -// pub struct DynamicTemplateContextBuilder { -// nodes: Vec, -// text: Vec, -// attributes: Vec, -// listeners: Vec<(String, Expr)>, -// key: Option, -// } - -// impl DynamicTemplateContextBuilder { -// fn add_node(&mut self, node: BodyNode) -> usize { -// let node_id = self.nodes.len(); - -// self.nodes.push(node); - -// node_id -// } - -// fn add_text(&mut self, text: FormattedSegment) -> usize { -// let text_id = self.text.len(); - -// self.text.push(text); - -// text_id -// } - -// fn add_attr(&mut self, attr: TokenStream) -> usize { -// let attr_id = self.attributes.len(); - -// self.attributes.push(attr); - -// attr_id -// } - -// fn add_listener(&mut self, name: Ident, listener: Expr) -> usize { -// let listener_id = self.listeners.len(); - -// self.listeners.push((name.to_string(), listener)); - -// listener_id -// } - -// fn add_key(&mut self, key: TokenStream) { -// self.key = Some(key); -// } -// } - -// impl ToTokens for DynamicTemplateContextBuilder { -// fn to_tokens(&self, tokens: &mut TokenStream) { -// let nodes = &self.nodes; -// let text = &self.text; -// let attributes = &self.attributes; -// let listeners_names = self -// .listeners -// .iter() -// .map(|(n, _)| syn::parse_str::(n).expect(n)); -// let listeners_exprs = self.listeners.iter().map(|(_, e)| e); -// let key = match &self.key { -// Some(k) => quote!(Some(#k)), -// None => quote!(None), -// }; -// tokens.append_all(quote! { -// TemplateContext { -// nodes: __cx.bump().alloc([#(#nodes),*]), -// text_segments: __cx.bump().alloc([#(&*dioxus::core::exports::bumpalo::format!(in __bump, "{}", #text).into_bump_str()),*]), -// attributes: __cx.bump().alloc([#({#attributes}.into_value(__cx.bump())),*]), -// listeners: __cx.bump().alloc([#(dioxus_elements::on::#listeners_names(__cx, #listeners_exprs)),*]), -// key: #key, -// } -// }) -// } -// } - -// fn quote_owned_segment(seg: OwnedPathSeg) -> proc_macro2::TokenStream { -// let OwnedPathSeg { ops, traverse } = seg; - -// let ops = ops -// .into_iter() -// .map(|op| match op { -// UpdateOp::StoreNode(id) => { -// let id = quote_template_node_id(id); -// quote!(UpdateOp::StoreNode(#id)) -// } -// UpdateOp::InsertBefore(id) => { -// let id = quote_template_node_id(id); -// quote!(UpdateOp::InsertBefore(#id)) -// } -// UpdateOp::InsertAfter(id) => { -// let id = quote_template_node_id(id); -// quote!(UpdateOp::InsertAfter(#id)) -// } -// UpdateOp::AppendChild(id) => { -// let id = quote_template_node_id(id); -// quote!(UpdateOp::AppendChild(#id)) -// } -// }) -// .collect::>(); - -// let traverse = quote_owned_traverse(traverse); - -// quote! { -// StaticPathSeg { -// ops: &[#(#ops),*], -// traverse: #traverse, -// } -// } -// } - -// fn quote_owned_traverse(traverse: OwnedTraverse) -> proc_macro2::TokenStream { -// match traverse { -// OwnedTraverse::Halt => { -// quote! {StaticTraverse::Halt} -// } -// OwnedTraverse::FirstChild(seg) => { -// let seg = quote_owned_segment(*seg); -// quote! {StaticTraverse::FirstChild(&#seg)} -// } -// OwnedTraverse::NextSibling(seg) => { -// let seg = quote_owned_segment(*seg); -// quote! {StaticTraverse::NextSibling(&#seg)} -// } -// OwnedTraverse::Both(b) => { -// let (child, sibling) = *b; -// let child = quote_owned_segment(child); -// let sibling = quote_owned_segment(sibling); -// quote! {StaticTraverse::Both(&(#child, #sibling))} -// } -// } -// } - -// fn quote_template_node_id(id: TemplateNodeId) -> proc_macro2::TokenStream { -// let raw = id.0; -// quote! { -// TemplateNodeId(#raw) -// } -// } 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..02900ca0e 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" @@ -8,15 +8,16 @@ license = "MIT/Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" -keywords = ["dom", "ui", "gui", "react", "wasm"] +keywords = ["dom", "ui", "gui", "react", "ssr"] # 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"] } +askama_escape = "0.10.3" [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/ssr/README.md b/packages/ssr/README.md index bcaea3044..c5b91b7b8 100644 --- a/packages/ssr/README.md +++ b/packages/ssr/README.md @@ -8,14 +8,13 @@ ## Resources This crate is a part of the broader Dioxus ecosystem. For more resources about Dioxus, check out: -- [Getting Started](https://dioxuslabs.com/getting-started) -- [Book](https://dioxuslabs.com/book) -- [Reference](https://dioxuslabs.com/reference) -- [Community Examples](https://github.com/DioxusLabs/community-examples) +- [Getting Started](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/index.html) +- [Book](https://dioxuslabs.com/docs/0.3/guide/en/) +- [Examples](https://github.com/DioxusLabs/example-projects) ## Overview -Dioxus SSR provides utilities to render Dioxus components to valid HTML. Once rendered, the HTML can be rehydrated client side or served from your web-server of choice. +Dioxus SSR provides utilities to render Dioxus components to valid HTML. Once rendered, the HTML can be rehydrated client-side or served from your web server of choice. ```rust, ignore let app: Component = |cx| cx.render(rsx!(div {"hello world!"})); @@ -23,17 +22,17 @@ let app: Component = |cx| cx.render(rsx!(div {"hello world!"})); let mut vdom = VirtualDom::new(app); let _ = vdom.rebuild(); -let text = dioxus_ssr::render_vdom(&vdom); +let text = dioxus_ssr::render(&vdom); assert_eq!(text, "
hello world!
") ``` ## Basic Usage -The simplest example is to simply render some `rsx!` nodes to html. This can be done with the [`render_lazy`] api. +The simplest example is to simply render some `rsx!` nodes to HTML. This can be done with the [`render_lazy`] API. ```rust, ignore -let content = dioxus_ssr::render(rsx!{ +let content = dioxus_ssr::render_lazy(rsx!{ div { (0..5).map(|i| rsx!( "Number: {i}" @@ -45,76 +44,54 @@ let content = dioxus_ssr::render(rsx!{ ## Rendering a VirtualDom ```rust, ignore -let mut dom = VirtualDom::new(app); -let _ = dom.rebuild(); +let mut vdom = VirtualDom::new(app); +let _ = vdom.rebuild(); -let content = dioxus_ssr::render_vdom(&dom); +let content = dioxus_ssr::render(&vdom); ``` -## Configuring output -It's possible to configure the output of the generated HTML. - -```rust, ignore -let content = dioxus_ssr::render_vdom(&dom, |config| config.pretty(true).prerender(true)); -``` - -## Usage as a writer - -We provide the basic `SsrFormatter` object that implements `Display`, so you can integrate SSR into an existing string, or write directly to a file. - -```rust, ignore -use std::fmt::{Error, Write}; - -let mut buf = String::new(); - -let dom = VirtualDom::new(app); -let _ = dom.rebuild(); - -let args = dioxus_ssr::formatter(dom, |config| config); -buf.write_fmt!(format_args!("{}", args)); -``` - -## Configuration - - - ## Usage in pre-rendering -This crate is particularly useful in pre-generating pages server-side and then selectively loading dioxus client-side to pick up the reactive elements. +This crate is particularly useful in pre-generating pages server-side and then selectively loading Dioxus client-side to pick up the reactive elements. -In fact, this crate supports hydration out of the box. However, it is extremely important that both the client and server will generate the exact same VirtualDOMs - the client picks up its VirtualDOM assuming that the pre-rendered page output is the same. To do this, you need to make sure that your VirtualDOM implementation is deterministic! This could involve either serializing our app state and sending it to the client, hydrating only parts of the page, or building tests to ensure what's rendered on the server is the same as the client. +This crate supports hydration out of the box. However, both the client and server must generate the *exact* same VirtualDOMs - the client picks up its VirtualDOM assuming that the pre-rendered page output is the same. To do this, you need to make sure that your VirtualDOM implementation is deterministic! This could involve either serializing our app state and sending it to the client, hydrating only parts of the page, or building tests to ensure what's rendered on the server is the same as the client. With pre-rendering enabled, this crate will generate element nodes with Element IDs pre-associated. During hydration, the Dioxus-WebSys renderer will attach the Virtual nodes to these real nodes after a page query. -To enable pre-rendering, simply configure the `SsrConfig` with pre-rendering enabled. +To enable pre-rendering, simply set the pre-rendering flag to true. ```rust, ignore -let dom = VirtualDom::new(App); +let mut vdom = VirtualDom::new(App); -let text = dioxus_ssr::render_vdom(App, |cfg| cfg.pre_render(true)); +let _ = vdom.rebuild(); + +let mut renderer = dioxus_ssr::Renderer::new(); +renderer.pre_render = true; + +let text = renderer.render(&vdom); ``` ## Usage in server-side rendering -Dioxus SSR can also be to render on the server. Obviously, you can just render the VirtualDOM to a string and send that down. +Dioxus SSR can also be used to render on the server. You can just render the VirtualDOM to a string and send that to the client. ```rust, ignore -let text = dioxus_ssr::render_vdom(&vdom); +let text = dioxus_ssr::render(&vdom); assert_eq!(text, "
hello world!
") ``` -The rest of the space - IE doing this more efficiently, caching the virtualdom, etc, will all need to be a custom implementation for now. +The rest of the space - IE doing this more efficiently, caching the VirtualDom, etc, will all need to be a custom implementation for now. ## Usage without a VirtualDom -Dioxus SSR needs an arena to allocate from - whether it be the VirtualDom or a dedicated Bump allocator. To render `rsx!` directly to a string, you'll want to create an `SsrRenderer` and call `render_lazy`. +Dioxus SSR needs an arena to allocate from - whether it be the VirtualDom or a dedicated Bump allocator. To render `rsx!` directly to a string, you'll want to create a `Renderer` and call `render_lazy`. ```rust, ignore -let text = dioxus_ssr::SsrRenderer::new().render_lazy(rsx!{ +let text = dioxus_ssr::Renderer::new().render_lazy(rsx!{ div { "hello world" } }); assert_eq!(text, "
hello world!
") @@ -131,4 +108,4 @@ let text = render_lazy!(rsx!( div { "hello world" } )); Dioxus SSR is a powerful tool to generate static sites. Using Dioxus for static site generation _is_ a bit overkill, however. The new documentation generation library, Doxie, is essentially Dioxus SSR on steroids designed for static site generation with client-side hydration. -Again, simply render the VirtualDOM to a string using `render_vdom` or any of the other render methods. +Again, simply render the VirtualDOM to a string using `render` or any of the other render methods. diff --git a/packages/ssr/src/cache.rs b/packages/ssr/src/cache.rs index 4b6f52178..a67fd5f22 100644 --- a/packages/ssr/src/cache.rs +++ b/packages/ssr/src/cache.rs @@ -17,6 +17,14 @@ pub enum Segment { Attr(usize), Node(usize), PreRendered(String), + /// A marker for where to insert a dynamic styles + StyleMarker { + // If the marker is inside a style tag or not + // This will be true if there are static styles + inside_style_tag: bool, + }, + /// A marker for where to insert a dynamic inner html + InnerHtmlMarker, } impl std::fmt::Write for StringChain { @@ -36,13 +44,13 @@ impl StringCache { let mut cur_path = vec![]; - for (root_idx, root) in template.template.roots.iter().enumerate() { + for (root_idx, root) in template.template.get().roots.iter().enumerate() { Self::recurse(root, &mut cur_path, root_idx, &mut chain)?; } Ok(Self { segments: chain.segments, - template: template.template, + template: template.template.get(), }) } @@ -60,29 +68,76 @@ impl StringCache { .. } => { cur_path.push(root_idx); - write!(chain, "<{}", tag)?; + write!(chain, "<{tag}")?; + // we need to collect the styles and write them at the end + let mut styles = Vec::new(); + // we need to collect the inner html and write it at the end + let mut inner_html = None; + // we need to keep track of if we have dynamic attrs to know if we need to insert a style and inner_html marker + let mut has_dynamic_attrs = false; for attr in *attrs { match attr { - TemplateAttribute::Static { name, value, .. } => { - write!(chain, " {}=\"{}\"", name, value)?; + TemplateAttribute::Static { + name, + value, + namespace, + } => { + if *name == "dangerous_inner_html" { + inner_html = Some(value); + } else if let Some("style") = namespace { + styles.push((name, value)); + } else { + write!(chain, " {name}=\"{value}\"")?; + } } TemplateAttribute::Dynamic { id: index } => { - chain.segments.push(Segment::Attr(*index)) + chain.segments.push(Segment::Attr(*index)); + has_dynamic_attrs = true; } } } + + // write the styles + if !styles.is_empty() { + write!(chain, " style=\"")?; + for (name, value) in styles { + write!(chain, "{name}:{value};")?; + } + chain.segments.push(Segment::StyleMarker { + inside_style_tag: true, + }); + write!(chain, "\"")?; + } else if has_dynamic_attrs { + chain.segments.push(Segment::StyleMarker { + inside_style_tag: false, + }); + } + if children.is_empty() && tag_is_self_closing(tag) { write!(chain, "/>")?; } else { write!(chain, ">")?; + // Write the static inner html, or insert a marker if dynamic inner html is possible + if let Some(inner_html) = inner_html { + chain.write_str(inner_html)?; + } else if has_dynamic_attrs { + chain.segments.push(Segment::InnerHtmlMarker); + } + for child in *children { Self::recurse(child, cur_path, root_idx, chain)?; } - write!(chain, "", tag)?; + write!(chain, "")?; } cur_path.pop(); } - TemplateNode::Text { text } => write!(chain, "{}", text)?, + TemplateNode::Text { text } => { + write!( + chain, + "{}", + askama_escape::escape(text, askama_escape::Html) + )?; + } TemplateNode::Dynamic { id: idx } | TemplateNode::DynamicText { id: idx } => { chain.segments.push(Segment::Node(*idx)) } diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 608c6a6d4..cfa08aee4 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -51,7 +51,7 @@ impl Renderer { ) -> std::fmt::Result { // We should never ever run into async or errored nodes in SSR // Error boundaries and suspense boundaries will convert these to sync - if let RenderReturn::Sync(Some(node)) = dom.get_scope(scope).unwrap().root_node() { + if let RenderReturn::Ready(node) = dom.get_scope(scope).unwrap().root_node() { self.render_template(buf, dom, node)? }; @@ -66,19 +66,32 @@ impl Renderer { ) -> std::fmt::Result { let entry = self .template_cache - .entry(template.template.name) + .entry(template.template.get().name) .or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap())) .clone(); + let mut inner_html = None; + + // We need to keep track of the dynamic styles so we can insert them into the right place + let mut accumulated_dynamic_styles = Vec::new(); + for segment in entry.segments.iter() { match segment { Segment::Attr(idx) => { let attr = &template.dynamic_attrs[*idx]; - match attr.value { - AttributeValue::Text(value) => write!(buf, " {}=\"{}\"", attr.name, value)?, - AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?, - _ => {} - }; + if attr.name == "dangerous_inner_html" { + inner_html = Some(attr); + } else if attr.namespace == Some("style") { + accumulated_dynamic_styles.push(attr); + } else { + match attr.value { + AttributeValue::Text(value) => { + write!(buf, " {}=\"{}\"", attr.name, value)? + } + AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?, + _ => {} + }; + } } Segment::Node(idx) => match &template.dynamic_nodes[*idx] { DynamicNode::Component(node) => { @@ -89,7 +102,7 @@ impl Renderer { let scope = dom.get_scope(id).unwrap(); let node = scope.root_node(); match node { - RenderReturn::Sync(Some(node)) => { + RenderReturn::Ready(node) => { self.render_template(buf, dom, node)? } _ => todo!( @@ -104,8 +117,11 @@ impl Renderer { write!(buf, "")?; } - // todo: escape the text - write!(buf, "{}", text.value)?; + write!( + buf, + "{}", + askama_escape::escape(text.value, askama_escape::Html) + )?; if self.pre_render { write!(buf, "")?; @@ -119,12 +135,53 @@ impl Renderer { DynamicNode::Placeholder(_el) => { if self.pre_render { - write!(buf, "
")?;
+                            write!(buf, "
")?;
                         }
                     }
                 },
 
-                Segment::PreRendered(contents) => write!(buf, "{}", contents)?,
+                Segment::PreRendered(contents) => write!(buf, "{contents}")?,
+
+                Segment::StyleMarker { inside_style_tag } => {
+                    if !accumulated_dynamic_styles.is_empty() {
+                        // if we are inside a style tag, we don't need to write the style attribute
+                        if !*inside_style_tag {
+                            write!(buf, " style=\"")?;
+                        }
+                        for attr in &accumulated_dynamic_styles {
+                            match attr.value {
+                                AttributeValue::Text(value) => {
+                                    write!(buf, "{}:{};", attr.name, value)?
+                                }
+                                AttributeValue::Bool(value) => {
+                                    write!(buf, "{}:{};", attr.name, value)?
+                                }
+                                AttributeValue::Float(f) => write!(buf, "{}:{};", attr.name, f)?,
+                                AttributeValue::Int(i) => write!(buf, "{}:{};", attr.name, i)?,
+                                _ => {}
+                            };
+                        }
+                        if !*inside_style_tag {
+                            write!(buf, "\"")?;
+                        }
+
+                        // clear the accumulated styles
+                        accumulated_dynamic_styles.clear();
+                    }
+                }
+
+                Segment::InnerHtmlMarker => {
+                    if let Some(inner_html) = inner_html.take() {
+                        let inner_html = &inner_html.value;
+                        match inner_html {
+                            AttributeValue::Text(value) => write!(buf, "{}", value)?,
+                            AttributeValue::Bool(value) => write!(buf, "{}", value)?,
+                            AttributeValue::Float(f) => write!(buf, "{}", f)?,
+                            AttributeValue::Int(i) => write!(buf, "{}", i)?,
+                            _ => {}
+                        }
+                    }
+                }
             }
         }
 
@@ -138,7 +195,7 @@ fn to_string_works() {
 
     fn app(cx: Scope) -> Element {
         let dynamic = 123;
-        let dyn2 = ""; // todo: escape this
+        let dyn2 = ""; // this should be escaped
 
         render! {
             div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
@@ -165,10 +222,15 @@ fn to_string_works() {
                 vec![
                     PreRendered("
Hello world 1 -->".into(),), + StyleMarker { + inside_style_tag: false, + }, + PreRendered(">".into()), + InnerHtmlMarker, + PreRendered("Hello world 1 -->".into(),), Node(0,), PreRendered( - "<-- Hello world 2
nest 1
nest 2
".into(), + "<-- Hello world 2
nest 1
nest 2
".into(), ), Node(1,), Node(2,), @@ -180,5 +242,5 @@ fn to_string_works() { use Segment::*; - assert_eq!(out, "
Hello world 1 -->123<-- Hello world 2
nest 1
nest 2
finalize 0
finalize 1
finalize 2
finalize 3
finalize 4
"); + assert_eq!(out, "
Hello world 1 -->123<-- Hello world 2
nest 1
nest 2
</diiiiiiiiv>
finalize 0
finalize 1
finalize 2
finalize 3
finalize 4
"); } diff --git a/packages/ssr/tests/inner_html.rs b/packages/ssr/tests/inner_html.rs new file mode 100644 index 000000000..11fb94746 --- /dev/null +++ b/packages/ssr/tests/inner_html.rs @@ -0,0 +1,26 @@ +use dioxus::prelude::*; + +#[test] +fn static_inner_html() { + fn app(cx: Scope) -> Element { + render! { div { dangerous_inner_html: "
1234
" } } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!(dioxus_ssr::render(&dom), r#"
1234
"#); +} + +#[test] +fn dynamic_inner_html() { + fn app(cx: Scope) -> Element { + let inner_html = "
1234
"; + render! { div { dangerous_inner_html: "{inner_html}" } } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!(dioxus_ssr::render(&dom), r#"
1234
"#); +} diff --git a/packages/ssr/tests/simple.rs b/packages/ssr/tests/simple.rs index 63137aeae..395f801e1 100644 --- a/packages/ssr/tests/simple.rs +++ b/packages/ssr/tests/simple.rs @@ -38,7 +38,7 @@ fn dynamic() { dioxus_ssr::render_lazy(rsx! { div { "Hello world 1 -->" "{dynamic}" "<-- Hello world 2" } }), - "
Hello world 1 -->123<-- Hello world 2
" + "
Hello world 1 -->123<-- Hello world 2
" ); } diff --git a/packages/ssr/tests/styles.rs b/packages/ssr/tests/styles.rs new file mode 100644 index 000000000..de77e715c --- /dev/null +++ b/packages/ssr/tests/styles.rs @@ -0,0 +1,40 @@ +use dioxus::prelude::*; + +#[test] +fn static_styles() { + fn app(cx: Scope) -> Element { + render! { div { width: "100px" } } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::render(&dom), + r#"
"# + ); +} + +#[test] +fn partially_dynamic_styles() { + let dynamic = 123; + + assert_eq!( + dioxus_ssr::render_lazy(rsx! { + div { width: "100px", height: "{dynamic}px" } + }), + r#"
"# + ); +} + +#[test] +fn dynamic_styles() { + let dynamic = 123; + + assert_eq!( + dioxus_ssr::render_lazy(rsx! { + div { width: "{dynamic}px" } + }), + r#"
"# + ); +} diff --git a/packages/tui/benches/update.rs b/packages/tui/benches/update.rs deleted file mode 100644 index b1fb622e0..000000000 --- a/packages/tui/benches/update.rs +++ /dev/null @@ -1,220 +0,0 @@ -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use dioxus::prelude::*; -use dioxus_tui::{Config, TuiContext}; - -criterion_group!(mbenches, tui_update); -criterion_main!(mbenches); - -/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. -fn tui_update(c: &mut Criterion) { - let mut group = c.benchmark_group("Update boxes"); - - // We can also use loops to define multiple benchmarks, even over multiple dimensions. - for size in 1..=8u32 { - let parameter_string = format!("{}", (3 * size).pow(2)); - group.bench_with_input( - BenchmarkId::new("size", parameter_string), - &size, - |b, size| { - b.iter(|| match size { - 1 => dioxus_tui::launch_cfg(app3, Config::default().with_headless()), - 2 => dioxus_tui::launch_cfg(app6, Config::default().with_headless()), - 3 => dioxus_tui::launch_cfg(app9, Config::default().with_headless()), - 4 => dioxus_tui::launch_cfg(app12, Config::default().with_headless()), - 5 => dioxus_tui::launch_cfg(app15, Config::default().with_headless()), - 6 => dioxus_tui::launch_cfg(app18, Config::default().with_headless()), - 7 => dioxus_tui::launch_cfg(app21, Config::default().with_headless()), - 8 => dioxus_tui::launch_cfg(app24, Config::default().with_headless()), - _ => (), - }) - }, - ); - } -} - -#[derive(Props, PartialEq)] -struct BoxProps { - x: usize, - y: usize, - hue: f32, - alpha: f32, -} -#[allow(non_snake_case)] -fn Box(cx: Scope) -> Element { - let count = use_state(cx, || 0); - - let x = cx.props.x * 2; - let y = cx.props.y * 2; - let hue = cx.props.hue; - let display_hue = cx.props.hue as u32 / 10; - let count = count.get(); - let alpha = cx.props.alpha + (count % 100) as f32; - - cx.render(rsx! { - div { - left: "{x}%", - top: "{y}%", - width: "100%", - height: "100%", - background_color: "hsl({hue}, 100%, 50%, {alpha}%)", - align_items: "center", - p{"{display_hue:03}"} - } - }) -} - -#[derive(Props, PartialEq)] -struct GridProps { - size: usize, -} -#[allow(non_snake_case)] -fn Grid(cx: Scope) -> Element { - let size = cx.props.size; - let count = use_state(cx, || 0); - let counts = use_ref(cx, || vec![0; size * size]); - - let ctx: &TuiContext = cx.consume_context().unwrap(); - if *count.get() + 1 >= (size * size) { - ctx.quit(); - } else { - counts.with_mut(|c| { - let i = *count.current(); - c[i] += 1; - c[i] %= 360; - }); - count.with_mut(|i| { - *i += 1; - *i %= size * size; - }); - } - - render! { - div{ - width: "100%", - height: "100%", - flex_direction: "column", - (0..size).map(|x| - { - rsx! { - div{ - width: "100%", - height: "100%", - flex_direction: "row", - (0..size).map(|y| - { - let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; - let key = format!("{}-{}", x, y); - rsx! { - Box{ - x: x, - y: y, - alpha: 100.0, - hue: alpha, - key: "{key}", - } - } - } - ) - } - } - } - ) - } - } -} - -fn app3(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 3, - } - } - }) -} - -fn app6(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 6, - } - } - }) -} - -fn app9(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 9, - } - } - }) -} - -fn app12(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 12, - } - } - }) -} - -fn app15(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 15, - } - } - }) -} - -fn app18(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 18, - } - } - }) -} - -fn app21(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 21, - } - } - }) -} - -fn app24(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 24, - } - } - }) -} diff --git a/packages/tui/src/node.rs b/packages/tui/src/node.rs deleted file mode 100644 index 26ea4412e..000000000 --- a/packages/tui/src/node.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::focus::Focus; -use crate::layout::TaffyLayout; -use crate::style_attributes::StyleModifier; -use dioxus_native_core::{real_dom::RealDom, state::*}; -use dioxus_native_core_macro::{sorted_str_slice, State}; - -pub(crate) type TuiDom = RealDom; -pub(crate) type TuiNode = dioxus_native_core::node::Node; - -#[derive(Debug, Clone, State, Default)] -pub(crate) struct NodeState { - #[child_dep_state(layout, Mutex)] - pub layout: TaffyLayout, - #[parent_dep_state(style)] - pub style: StyleModifier, - #[node_dep_state()] - pub prevent_default: PreventDefault, - #[node_dep_state()] - pub focus: Focus, - pub focused: bool, -} - -#[derive(PartialEq, Debug, Clone)] -pub(crate) enum PreventDefault { - Focus, - KeyPress, - KeyRelease, - KeyDown, - KeyUp, - MouseDown, - Click, - MouseEnter, - MouseLeave, - MouseOut, - Unknown, - MouseOver, - ContextMenu, - Wheel, - MouseUp, -} - -impl Default for PreventDefault { - fn default() -> Self { - PreventDefault::Unknown - } -} - -impl NodeDepState for PreventDefault { - type DepState = (); - type Ctx = (); - - const NODE_MASK: dioxus_native_core::node_ref::NodeMask = - dioxus_native_core::node_ref::NodeMask::new_with_attrs( - dioxus_native_core::node_ref::AttributeMask::Static(&sorted_str_slice!([ - "dioxus-prevent-default" - ])), - ) - .with_listeners(); - - fn reduce( - &mut self, - node: dioxus_native_core::node_ref::NodeView, - _sibling: (), - _ctx: &Self::Ctx, - ) -> bool { - let new = match node.attributes().and_then(|mut attrs| { - attrs - .find(|a| a.attribute.name == "dioxus-prevent-default") - .and_then(|a| a.value.as_text()) - }) { - Some("onfocus") => PreventDefault::Focus, - Some("onkeypress") => PreventDefault::KeyPress, - Some("onkeyrelease") => PreventDefault::KeyRelease, - Some("onkeydown") => PreventDefault::KeyDown, - Some("onkeyup") => PreventDefault::KeyUp, - Some("onclick") => PreventDefault::Click, - Some("onmousedown") => PreventDefault::MouseDown, - Some("onmouseup") => PreventDefault::MouseUp, - Some("onmouseenter") => PreventDefault::MouseEnter, - Some("onmouseover") => PreventDefault::MouseOver, - Some("onmouseleave") => PreventDefault::MouseLeave, - Some("onmouseout") => PreventDefault::MouseOut, - Some("onwheel") => PreventDefault::Wheel, - Some("oncontextmenu") => PreventDefault::ContextMenu, - _ => return false, - }; - if new == *self { - false - } else { - *self = new; - true - } - } -} diff --git a/packages/tui/src/widgets/mod.rs b/packages/tui/src/widgets/mod.rs deleted file mode 100644 index 36306b180..000000000 --- a/packages/tui/src/widgets/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -mod button; -mod checkbox; -mod input; -mod number; -mod password; -mod slider; -mod textbox; - -use dioxus_core::{ElementId, RenderReturn, Scope}; -pub use input::*; - -pub(crate) fn get_root_id(cx: Scope) -> Option { - if let RenderReturn::Sync(Some(sync)) = cx.root_node() { - sync.root_ids.get(0).and_then(|id| id.get()) - } else { - None - } -} diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 40056eaaa..7adfbf13a 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.1" 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 = "." } +dioxus-web = { path = ".", features = ["hydrate"] } diff --git a/packages/web/README.md b/packages/web/README.md index f2c8a9c75..87265c926 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -1,11 +1,46 @@ -# Dioxus-Web +# Dioxus-web -Build interactive user experiences directly in the web browser! +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] -Dioxus-web provides a `WebsysRenderer` for the Dioxus Virtual Dom that handles events, progresses components, and updates the actual DOM using web-sys methods. +[crates-badge]: https://img.shields.io/crates/v/dioxus-web.svg +[crates-url]: https://crates.io/crates/dioxus-web +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) | +[API Docs](https://docs.rs/dioxus-web/latest/dioxus_web) | +[Chat](https://discord.gg/XgGxMSkvUM) + +## Overview + +Run Dioxus in the browser using WebAssembly. + +- Relies on [sledgehammer-bindgen](https://github.com/Demonthos/sledgehammer_bindgen) and [web-sys](https://github.com/rustwasm/wasm-bindgen/tree/main/crates/web-sys) to modify the dom +- Supports instant hot reloading via the Dioxus CLI +- Around 60k gzipped + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT without any additional +terms or conditions. -## Web-specific Optimizations -- Uses string interning of all common node types -- Optimistically interns short strings -- Builds trees completely before mounting them diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 1e84b1d92..7483a7a8b 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -8,6 +8,7 @@ /// dioxus_web::launch(App, Config::new().hydrate(true).root_name("myroot")) /// ``` pub struct Config { + #[cfg(feature = "hydrate")] pub(crate) hydrate: bool, pub(crate) rootname: String, pub(crate) cached_strings: Vec, @@ -17,6 +18,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { + #[cfg(feature = "hydrate")] hydrate: false, rootname: "main".to_string(), cached_strings: Vec::new(), @@ -33,6 +35,7 @@ impl Config { Self::default() } + #[cfg(feature = "hydrate")] /// Enable SSR hydration /// /// This enables Dioxus to pick up work from a pre-renderd HTML file. Hydration will completely skip over any async @@ -46,15 +49,16 @@ impl Config { /// Set the name of the element that Dioxus will use as the root. /// - /// This is akint to calling React.render() on the element with the specified name. + /// This is akin to calling React.render() on the element with the specified name. pub fn rootname(mut self, name: impl Into) -> Self { self.rootname = name.into(); self } - /// Set the name of the element that Dioxus will use as the root. + /// Sets a string cache for wasm bindgen to [intern](https://docs.rs/wasm-bindgen/0.2.84/wasm_bindgen/fn.intern.html). This can help reduce the time it takes for wasm bindgen to pass + /// strings from rust to javascript. This can significantly improve pefromance when passing strings to javascript, but can have a negative impact on startup time. /// - /// This is akint to calling React.render() on the element with the specified name. + /// > Currently this cache is only used when creating static elements and attributes. pub fn with_string_cache(mut self, cache: Vec) -> Self { self.cached_strings = cache; self diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index fdae04a9a..4d17749d8 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; @@ -20,9 +22,11 @@ use crate::Config; pub struct WebsysDom { document: Document, + #[allow(dead_code)] + pub(crate) root: Element, templates: FxHashMap, max_template_id: u32, - interpreter: Channel, + pub(crate) interpreter: Channel, } pub struct UiEvent { @@ -70,10 +74,14 @@ impl WebsysDom { } })); - dioxus_interpreter_js::initilize(root.unchecked_into(), handler.as_ref().unchecked_ref()); + dioxus_interpreter_js::initilize( + root.clone().unchecked_into(), + handler.as_ref().unchecked_ref(), + ); handler.forget(); Self { document, + root, interpreter, templates: FxHashMap::default(), max_template_id: 0, @@ -121,12 +129,10 @@ impl WebsysDom { } = attr { match namespace { - Some(ns) if *ns == "style" => el - .dyn_ref::() - .unwrap() - .style() - .set_property(name, value) - .unwrap(), + Some(ns) if *ns == "style" => { + el.dyn_ref::() + .map(|f| f.style().set_property(name, value)); + } Some(ns) => el.set_attribute_ns(Some(ns), name, value).unwrap(), None => el.set_attribute(name, value).unwrap(), } @@ -177,16 +183,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), @@ -212,11 +235,16 @@ 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" => { + let mouse = MouseData::from(event); + Rc::new(DragData { mouse }) + } + "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { Rc::new(PointerData::from(event)) diff --git a/packages/web/src/hot_reload.rs b/packages/web/src/hot_reload.rs index ac474370a..cf73dc219 100644 --- a/packages/web/src/hot_reload.rs +++ b/packages/web/src/hot_reload.rs @@ -2,12 +2,13 @@ use futures_channel::mpsc::UnboundedReceiver; +use dioxus_core::Template; use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; use web_sys::{MessageEvent, WebSocket}; #[cfg(not(debug_assertions))] -pub(crate) fn init() -> UnboundedReceiver { +pub(crate) fn init() -> UnboundedReceiver> { let (tx, rx) = futures_channel::mpsc::unbounded(); std::mem::forget(tx); @@ -16,15 +17,16 @@ pub(crate) fn init() -> UnboundedReceiver { } #[cfg(debug_assertions)] -pub(crate) fn init() -> UnboundedReceiver { +pub(crate) fn init() -> UnboundedReceiver> { use std::convert::TryInto; + use serde::Deserialize; + let window = web_sys::window().unwrap(); - let protocol = if window.location().protocol().unwrap() == "https:" { - "wss:" - } else { - "ws:" + let protocol = match window.location().protocol().unwrap() { + prot if prot == "https:" => "wss:", + _ => "ws:", }; let url = format!( @@ -39,8 +41,13 @@ pub(crate) fn init() -> UnboundedReceiver { // change the rsx when new data is received let cl = Closure::wrap(Box::new(move |e: MessageEvent| { if let Ok(text) = e.data().dyn_into::() { - if let Ok(val) = text.try_into() { - _ = tx.unbounded_send(val); + let text: Result = text.try_into(); + if let Ok(string) = text { + let val = serde_json::from_str::(&string).unwrap(); + // leak the value + let val: &'static serde_json::Value = Box::leak(Box::new(val)); + let template: Template<'_> = Template::deserialize(val).unwrap(); + tx.unbounded_send(template).unwrap(); } } }) as Box); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index bfc6f6d63..e208b0591 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -62,6 +62,8 @@ mod cache; mod cfg; mod dom; mod hot_reload; +#[cfg(feature = "hydrate")] +mod rehydrate; mod util; // Currently disabled since it actually slows down immediate rendering @@ -179,20 +181,45 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop wasm_bindgen::intern(s); } - let _should_hydrate = cfg.hydrate; - let (tx, mut rx) = futures_channel::mpsc::unbounded(); + #[cfg(feature = "hydrate")] + let should_hydrate = cfg.hydrate; + #[cfg(not(feature = "hydrate"))] + let should_hydrate = false; + let mut websys_dom = dom::WebsysDom::new(cfg, tx); log::info!("rebuilding app"); - // if should_hydrate { - // } else { - let edits = dom.rebuild(); + if should_hydrate { + #[cfg(feature = "hydrate")] + { + // todo: we need to split rebuild and initialize into two phases + // it's a waste to produce edits just to get the vdom loaded - websys_dom.load_templates(&edits.templates); - websys_dom.apply_edits(edits.edits); + let templates = dom.rebuild().templates; + websys_dom.load_templates(&templates); + + if let Err(err) = websys_dom.rehydrate(&dom) { + log::error!( + "Rehydration failed {:?}. Rebuild DOM into element from scratch", + &err + ); + websys_dom.root.set_text_content(None); + + let edits = dom.rebuild(); + + websys_dom.load_templates(&edits.templates); + websys_dom.apply_edits(edits.edits); + } + } + } else { + let edits = dom.rebuild(); + + 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(); @@ -202,19 +229,23 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // if virtualdom has nothing, wait for it to have something before requesting idle time // if there is work then this future resolves immediately. - let mut res = { + let (mut res, template) = { let work = dom.wait_for_work().fuse(); pin_mut!(work); futures_util::select! { - _ = work => None, - _new_template = hotreload_rx.next() => { - todo!("Implement hot reload"); + _ = work => (None, None), + new_template = hotreload_rx.next() => { + (None, new_template) } - evt = rx.next() => evt + evt = rx.next() => (evt, None) } }; + if let Some(template) = template { + dom.replace_template(template); + } + // Dequeue all of the events from the channel in send order // todo: we should re-order these if possible while let Some(evt) = res { @@ -243,30 +274,3 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop websys_dom.apply_edits(edits.edits); } } - -// if should_hydrate { -// // todo: we need to split rebuild and initialize into two phases -// // it's a waste to produce edits just to get the vdom loaded -// let _ = dom.rebuild(); - -// #[cfg(feature = "hydrate")] -// #[allow(unused_variables)] -// if let Err(err) = websys_dom.rehydrate(&dom) { -// log::error!( -// "Rehydration failed {:?}. Rebuild DOM into element from scratch", -// &err -// ); - -// websys_dom.root.set_text_content(None); - -// // errrrr we should split rebuild into two phases -// // one that initializes things and one that produces edits -// let edits = dom.rebuild(); - -// websys_dom.apply_edits(edits.edits); -// } -// } else { -// let edits = dom.rebuild(); -// websys_dom.apply_edits(edits.template_mutations); -// websys_dom.apply_edits(edits.edits); -// } diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index bf327176d..9af23534b 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -1,9 +1,13 @@ use crate::dom::WebsysDom; -use dioxus_core::{VNode, VirtualDom}; +use dioxus_core::{ + AttributeValue, DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VPlaceholder, VText, + VirtualDom, +}; +use dioxus_html::event_bubbles; use wasm_bindgen::JsCast; -use web_sys::{Comment, Element, Node, Text}; +use web_sys::{Comment, Node}; -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub enum RehydrationError { NodeTypeMismatch, NodeNotFound, @@ -11,163 +15,264 @@ pub enum RehydrationError { } use RehydrationError::*; +fn set_node(hydrated: &mut Vec, id: ElementId, node: Node) { + let idx = id.0; + if idx >= hydrated.len() { + hydrated.resize(idx + 1, false); + } + if !hydrated[idx] { + dioxus_interpreter_js::set_node(idx as u32, node); + hydrated[idx] = true; + } +} + impl WebsysDom { // we're streaming in patches, but the nodes already exist // so we're just going to write the correct IDs to the node and load them in pub fn rehydrate(&mut self, dom: &VirtualDom) -> Result<(), RehydrationError> { - let root = self + let mut root = self .root .clone() .dyn_into::() - .map_err(|_| NodeTypeMismatch)?; + .map_err(|_| NodeTypeMismatch)? + .first_child() + .ok_or(NodeNotFound); let root_scope = dom.base_scope(); - let root_node = root_scope.root_node(); - let mut nodes = vec![root]; - let mut counter = vec![0]; + let mut hydrated = vec![true]; - let mut last_node_was_text = false; + let mut last_node_was_static_text = false; - todo!() - // // Recursively rehydrate the dom from the VirtualDom - // self.rehydrate_single( - // &mut nodes, - // &mut counter, - // dom, - // root_node, - // &mut last_node_was_text, - // ) + // Recursively rehydrate the dom from the VirtualDom + self.rehydrate_scope( + root_scope, + &mut root, + &mut hydrated, + dom, + &mut last_node_was_static_text, + )?; + + self.interpreter.flush(); + Ok(()) } - fn rehydrate_single( + fn rehydrate_scope( &mut self, - nodes: &mut Vec, - place: &mut Vec, + scope: &ScopeState, + current_child: &mut Result, + hydrated: &mut Vec, dom: &VirtualDom, - node: &VNode, - last_node_was_text: &mut bool, + last_node_was_static_text: &mut bool, + ) -> Result<(), RehydrationError> { + let vnode = match scope.root_node() { + dioxus_core::RenderReturn::Ready(ready) => ready, + _ => return Err(VNodeNotInitialized), + }; + self.rehydrate_vnode( + current_child, + hydrated, + dom, + vnode, + last_node_was_static_text, + ) + } + + fn rehydrate_vnode( + &mut self, + current_child: &mut Result, + hydrated: &mut Vec, + dom: &VirtualDom, + vnode: &VNode, + last_node_was_static_text: &mut bool, + ) -> Result<(), RehydrationError> { + for (i, root) in vnode.template.get().roots.iter().enumerate() { + // make sure we set the root node ids even if the node is not dynamic + set_node( + hydrated, + vnode.root_ids.get(i).ok_or(VNodeNotInitialized)?, + current_child.clone()?, + ); + + self.rehydrate_template_node( + current_child, + hydrated, + dom, + vnode, + root, + last_node_was_static_text, + )?; + } + Ok(()) + } + + fn rehydrate_template_node( + &mut self, + current_child: &mut Result, + hydrated: &mut Vec, + dom: &VirtualDom, + vnode: &VNode, + node: &TemplateNode, + last_node_was_static_text: &mut bool, ) -> Result<(), RehydrationError> { match node { - VNode::Text(t) => { - let node_id = t.id.get().ok_or(VNodeNotInitialized)?; - - let cur_place = place.last_mut().unwrap(); - - // skip over the comment element - if *last_node_was_text { - if cfg!(debug_assertions) { - let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - let node_text = node.dyn_into::().unwrap(); - assert_eq!(node_text.data(), "spacer"); + TemplateNode::Element { + children, attrs, .. + } => { + let mut mounted_id = None; + for attr in *attrs { + if let dioxus_core::TemplateAttribute::Dynamic { id } = attr { + let attribute = &vnode.dynamic_attrs[*id]; + let value = &attribute.value; + let id = attribute.mounted_element.get(); + mounted_id = Some(id); + let name = attribute.name; + if let AttributeValue::Listener(_) = value { + let event_name = &name[2..]; + self.interpreter.new_event_listener( + event_name, + id.0 as u32, + event_bubbles(event_name) as u8, + ); + } } - *cur_place += 1; } + if let Some(id) = mounted_id { + set_node(hydrated, id, current_child.clone()?); + } + if !children.is_empty() { + let mut children_current_child = current_child + .as_mut() + .map_err(|e| *e)? + .first_child() + .ok_or(NodeNotFound)? + .dyn_into::() + .map_err(|_| NodeTypeMismatch); + for child in *children { + self.rehydrate_template_node( + &mut children_current_child, + hydrated, + dom, + vnode, + child, + last_node_was_static_text, + )?; + } + } + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); + *last_node_was_static_text = false; + } + TemplateNode::Text { .. } => { + // if the last node was static text, it got merged with this one + if !*last_node_was_static_text { + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); + } + *last_node_was_static_text = true; + } + TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { + self.rehydrate_dynamic_node( + current_child, + hydrated, + dom, + &vnode.dynamic_nodes[*id], + last_node_was_static_text, + )?; + } + } + Ok(()) + } - let node = nodes - .last() - .unwrap() - .child_nodes() - .get(*cur_place) - .ok_or(NodeNotFound)?; - - let _text_el = node.dyn_ref::().ok_or(NodeTypeMismatch)?; - - // in debug we make sure the text is the same + fn rehydrate_dynamic_node( + &mut self, + current_child: &mut Result, + hydrated: &mut Vec, + dom: &VirtualDom, + dynamic: &DynamicNode, + last_node_was_static_text: &mut bool, + ) -> Result<(), RehydrationError> { + match dynamic { + dioxus_core::DynamicNode::Text(VText { id, .. }) => { + // skip comment separator before node if cfg!(debug_assertions) { - let contents = _text_el.node_value().unwrap(); - assert_eq!(t.text, contents); + assert!(current_child + .as_mut() + .map_err(|e| *e)? + .has_type::()); } + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); - *last_node_was_text = true; - - self.interpreter.SetNode(node_id.0, node); - - *cur_place += 1; - } - - VNode::Element(vel) => { - let node_id = vel.id.get().ok_or(VNodeNotInitialized)?; - - let cur_place = place.last_mut().unwrap(); - - let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - - self.interpreter.SetNode(node_id.0, node.clone()); - - *cur_place += 1; - - nodes.push(node.clone()); - - place.push(0); - - // we cant have the last node be text - let mut last_node_was_text = false; - for child in vel.children { - self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?; - } - - for listener in vel.listeners { - let id = listener.mounted_node.get().unwrap(); - self.interpreter.NewEventListener( - listener.event, - Some(id.as_u64()), - self.handler.as_ref().unchecked_ref(), - event_bubbles(listener.event), - ); - } - - if !vel.listeners.is_empty() { - use smallstr::SmallString; - use std::fmt::Write; - - // 8 digits is enough, yes? - // 12 million nodes in one page? - let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); - write!(s, "{}", node_id).unwrap(); - - node.dyn_ref::() - .unwrap() - .set_attribute("dioxus-id", s.as_str()) - .unwrap(); - } - - place.pop(); - nodes.pop(); + set_node( + hydrated, + id.get().ok_or(VNodeNotInitialized)?, + current_child.clone()?, + ); + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); + // skip comment separator after node if cfg!(debug_assertions) { - let el = node.dyn_ref::().unwrap(); - let name = el.tag_name().to_lowercase(); - assert_eq!(name, vel.tag); + assert!(current_child + .as_mut() + .map_err(|e| *e)? + .has_type::()); + } + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); + + *last_node_was_static_text = false; + } + dioxus_core::DynamicNode::Placeholder(VPlaceholder { id, .. }) => { + set_node( + hydrated, + id.get().ok_or(VNodeNotInitialized)?, + current_child.clone()?, + ); + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); + *last_node_was_static_text = false; + } + dioxus_core::DynamicNode::Component(comp) => { + let scope = comp.scope.get().ok_or(VNodeNotInitialized)?; + self.rehydrate_scope( + dom.get_scope(scope).unwrap(), + current_child, + hydrated, + dom, + last_node_was_static_text, + )?; + } + dioxus_core::DynamicNode::Fragment(fragment) => { + for vnode in *fragment { + self.rehydrate_vnode( + current_child, + hydrated, + dom, + vnode, + last_node_was_static_text, + )?; } } - - VNode::Placeholder(el) => { - let node_id = el.id.get().ok_or(VNodeNotInitialized)?; - - let cur_place = place.last_mut().unwrap(); - let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - - self.interpreter.SetNode(node_id.0, node); - - // self.nodes[node_id.0] = Some(node); - - *cur_place += 1; - } - - VNode::Fragment(el) => { - for el in el.children { - self.rehydrate_single(nodes, place, dom, el, last_node_was_text)?; - } - } - - VNode::Component(el) => { - let scope = dom.get_scope(el.scope.get().unwrap()).unwrap(); - let node = scope.root_node(); - todo!() - // self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?; - } - VNode::TemplateRef(_) => todo!(), } Ok(()) } diff --git a/packages/web/tests/hydrate.rs b/packages/web/tests/hydrate.rs index 936466a14..c7de26a8c 100644 --- a/packages/web/tests/hydrate.rs +++ b/packages/web/tests/hydrate.rs @@ -58,7 +58,7 @@ fn rehydrates() { .unwrap() .body() .unwrap() - .set_inner_html(&format!("
{}
", out)); + .set_inner_html(&format!("
{out}
")); dioxus_web::launch_cfg(app, Config::new().hydrate(true)); } diff --git a/translations/pt-br/README.md b/translations/pt-br/README.md index ef14a1af9..a50727662 100644 --- a/translations/pt-br/README.md +++ b/translations/pt-br/README.md @@ -40,7 +40,7 @@ | Exemplos | - Guia + Guia | 中文 @@ -83,7 +83,7 @@ Se você conhece React, então você já conhece o Dioxus. - + @@ -159,7 +159,7 @@ Dioxus é único no ecossistema do Rust por suportar: - SSR com `hydration` feito pelo Cliente - Suporte à aplicação Desktop -Para mais informações sobre quais funções estão atualmente disponíveis e para o progresso futuro, veja [O Guia](https://dioxuslabs.com/guide/). +Para mais informações sobre quais funções estão atualmente disponíveis e para o progresso futuro, veja [O Guia](https://dioxuslabs.com/guide/pt-br/). ## Projeto dentro do ecossistema Dioxus
TutorialTutorial Web Desktop SSR