diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..6fddca0d6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c3c1f681f..968d5f103 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,7 +13,6 @@ jobs: runs-on: ubuntu-latest environment: docs steps: - - uses: actions/checkout@v2 # NOTE: Comment out when https://github.com/rust-lang/mdBook/pull/1306 is merged and released # - name: Setup mdBook @@ -25,6 +24,7 @@ jobs: - name: Setup mdBook run: | cargo install mdbook --git https://github.com/Ruin0x11/mdBook.git --branch localization --rev e74fdb1 + - uses: actions/checkout@v3 - name: Build run: cd docs && @@ -32,7 +32,7 @@ jobs: cd router && mdbook build -d ../nightly/router && cd .. - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.2.3 + 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. diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 7d9415bca..c5616322e 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -30,13 +30,13 @@ jobs: name: Test Suite runs-on: macos-latest steps: - - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@v3 - uses: actions-rs/cargo@v1 with: command: test diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 40641e8a5..de6b34274 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,15 +32,15 @@ jobs: name: Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev + - uses: actions/checkout@v3 - uses: actions-rs/cargo@v1 with: command: check @@ -51,18 +51,18 @@ jobs: name: Test Suite runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev - uses: davidB/rust-cargo-make@v1 - uses: browser-actions/setup-firefox@latest - - uses: jetli/wasm-pack-action@v0.3.0 + - uses: jetli/wasm-pack-action@v0.4.0 + - uses: actions/checkout@v3 - uses: actions-rs/cargo@v1 with: command: make @@ -73,14 +73,14 @@ jobs: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 - run: rustup component add rustfmt + - uses: actions/checkout@v3 - uses: actions-rs/cargo@v1 with: command: fmt @@ -91,16 +91,16 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev - run: rustup component add clippy + - uses: actions/checkout@v3 - uses: actions-rs/cargo@v1 with: command: clippy @@ -115,7 +115,7 @@ jobs: # options: --security-opt seccomp=unconfined # steps: # - name: Checkout repository - # uses: actions/checkout@v2 + # uses: actions/checkout@v3 # - name: Generate code coverage # run: | # apt-get update &&\ diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 9b5204d1c..6ae99612f 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -47,8 +47,6 @@ jobs: # which causes failures for some of rustfmt's line-ending sensitive tests - name: disable git eol translation run: git config --global core.autocrlf false - - name: checkout - uses: actions/checkout@v2 # Run build - name: Install Rustup using win.rustup.rs @@ -66,6 +64,9 @@ jobs: if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly' shell: bash + - name: checkout + uses: actions/checkout@v3 + - name: test run: | rustc -Vv diff --git a/Cargo.toml b/Cargo.toml index 31a89c5bf..8f3a33d96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,3 @@ - [workspace] members = [ "packages/dioxus", @@ -13,17 +12,15 @@ members = [ "packages/mobile", "packages/interpreter", "packages/fermi", - "packages/tui", "packages/liveview", "packages/autofmt", "packages/rsx", - "packages/rsx_interpreter", + "docs/guide", + "packages/tui", "packages/native-core", "packages/native-core-macro", - "docs/guide", ] - # This is a "virtual package" # It is not meant to be published, but is used so "cargo run --example XYZ" works properly [package] @@ -38,7 +35,7 @@ homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] rust-version = "1.60.0" - +publish = false [dev-dependencies] dioxus = { path = "./packages/dioxus" } @@ -60,3 +57,9 @@ reqwest = { version = "0.11.9", features = ["json"] } fern = { version = "0.6.0", features = ["colored"] } thiserror = "1.0.30" env_logger = "0.9.0" +simple_logger = "4.0.0" + +[profile.release] +opt-level = 3 +lto = true +debug = true diff --git a/README.md b/README.md index f50889eab..4ef6acfee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -
-

Dioxus

-
+

+ +

@@ -54,7 +54,7 @@ Dioxus is a portable, performant, and ergonomic framework for building cross-pla ```rust fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let mut count = use_state(cx, || 0); cx.render(rsx! { h1 { "High-Five counter: {count}" } @@ -68,115 +68,108 @@ Dioxus can be used to deliver webapps, desktop apps, static sites, mobile apps, If you know React, then you already know Dioxus. -### Unique features: - +## Unique features: - Desktop apps running natively (no Electron!) in less than 10 lines of code. - Incredibly ergonomic and powerful state management. - 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. +- Blazingly fast 🔥🔥 and extremely memory efficient +- Integrated hot reloading for fast iteration +- First-class async support with coroutines and suspense - And more! Read the [full release post](https://dioxuslabs.com/blog/introducing-dioxus/). -## Get Started with... - - - - - - - - +## Supported Platforms +
+
TutorialWebDesktopSSRMobile
-
+ Web + + + + + + Desktop + + + + + + Mobile + + + + + + Liveview + + + + + + Terminal + + + + + +
-## Example Projects: +## Why Dioxus? +There's tons of options for building apps, so why would you choose Dioxus? -| File Navigator (Desktop) | WiFi scanner (Desktop) | TodoMVC (All platforms) | E-commerce w/ Tailwind (SSR/LiveView) | -|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![E-commerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) | +Well, first and foremost, Dioxus prioritizes developer experience. This is reflected in a variety of features unique to Dioxus: -See the [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) page for a curated list of content in the Dioxus Ecosystem. +- Autoformatting of our meta language (RSX) and accompanying VSCode extension +- Hotreloading using an interpreter of RSX for both desktop and web +- Emphasis on good docs - our guide is complete and our HTML elements are documented +- Significant research in simplifying -## Why Dioxus and why Rust? +Dioxus is also a very extensible platform. -TypeScript is a fantastic addition to JavaScript, but it's still fundamentally JavaScript. TS code runs slightly slower, has tons of configuration options, and not every package is properly typed. +- Easily build new renderers by implementing a very simple optimized stack-machine +- Build and share components and even custom elements -By using Rust, we gain: +So... Dioxus is great, but why won't it work for me? +- It's not fully mature yet. APIs are still shifting, things might break (though we try to avoid it) +- You need to run in a no-std environment. +- You don't like the React-hooks model of building UIs -- Static types for _every_ library -- Immutability by default -- A simple and intuitive module system -- Integrated documentation (`go to source` _actually goes to source_) -- Advanced pattern matching -- Clean, efficient, composable iterators -- Inline built-in unit/integration testing -- Best-in-class error handling -- Powerful and sane standard library -- Flexible macro system -- Access to `crates.io` -Specifically, Dioxus provides us many other assurances: +## Contributing +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! -- Proper use of immutable data structures -- Guaranteed error handling (so you can sleep easy at night not worrying about `cannot read property of undefined`) -- Native performance on mobile -- Direct access to system IO -And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time. - -## Why NOT Dioxus? - -You shouldn't use Dioxus if: - -- You don't like the React Hooks approach to frontend -- You need a no-std renderer -- You want to support browsers where Wasm or asm.js are not supported. -- You need a Send+Sync UI solution (Dioxus is not currently thread-safe) - -## Comparison with other Rust UI frameworks - -Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**. - -- [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead, no borrowed props, supports SSR (no hydration), no direct desktop/mobile support. -- [Percy](https://github.com/chinedufn/percy): Supports SSR but with less emphasis on state management and event handling. -- [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but no direct support for desktop/mobile. -- [Dominator](https://github.com/Pauan/rust-dominator): Signal-based zero-cost alternative, less emphasis on community and docs. -- [Azul](https://azul.rs): Fully native HTML/CSS renderer for desktop applications, no support for web/ssr - -## Parity with React & Roadmap - -Dioxus is heavily inspired by React, but we want your transition to feel like an upgrade. Dioxus is _most_ of the way there, but missing a few key features. These include: - -- Portals -- Suspense integration with SSR -- Server Components / Bundle Splitting / Lazy - -Dioxus is unique in the Rust ecosystem in that it supports: - -- Components with props that borrow from their parent -- Server-side-rendering with client-side hydration -- Support for desktop applications - -For more information on what features are currently available and the roadmap for the future, be sure to check out [the guide](https://dioxuslabs.com/guide/). - -## Projects in the ecosystem - -Want to jump in and help build the future of Rust frontend? There's plenty of places where your contributions can make a huge difference: - -- [TUI renderer](https://github.com/dioxusLabs/rink) -- [CLI Tooling](https://github.com/dioxusLabs/cli) -- [Documentation and Example Projects](https://github.com/dioxusLabs/docsite) -- LiveView and Web Server -- Asset System + + + ## License - This project is licensed under the [MIT license]. [mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT -### Contribution - 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/docs/guide/examples/component_children_inspect.rs b/docs/guide/examples/component_children_inspect.rs index d05a95a8b..880c64358 100644 --- a/docs/guide/examples/component_children_inspect.rs +++ b/docs/guide/examples/component_children_inspect.rs @@ -26,7 +26,7 @@ struct ClickableProps<'a> { // ANCHOR: Clickable fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { match cx.props.children { - Some(VNode::Text(_)) => { + Ok(VNode { dynamic_nodes, .. }) => { todo!("render some stuff") } _ => { diff --git a/docs/guide/examples/conditional_rendering.rs b/docs/guide/examples/conditional_rendering.rs index 5a89ef4ed..0b3ebfbfe 100644 --- a/docs/guide/examples/conditional_rendering.rs +++ b/docs/guide/examples/conditional_rendering.rs @@ -6,7 +6,7 @@ fn main() { } pub fn App(cx: Scope) -> Element { - let is_logged_in = use_state(&cx, || false); + let is_logged_in = use_state(cx, || false); cx.render(rsx!(LogIn { is_logged_in: **is_logged_in, @@ -48,7 +48,7 @@ fn LogIn<'a>( fn LogInWarning(cx: Scope, is_logged_in: bool) -> Element { // ANCHOR: conditional_none if *is_logged_in { - return None; + return cx.render(rsx!(())); } cx.render(rsx! { diff --git a/docs/guide/examples/event_handler_prop.rs b/docs/guide/examples/event_handler_prop.rs index a333f86fe..0ef70c841 100644 --- a/docs/guide/examples/event_handler_prop.rs +++ b/docs/guide/examples/event_handler_prop.rs @@ -1,6 +1,5 @@ #![allow(non_snake_case)] -use dioxus::events::MouseEvent; use dioxus::prelude::*; fn main() { diff --git a/docs/guide/examples/event_nested.rs b/docs/guide/examples/event_nested.rs index 512211707..9e85dfca4 100644 --- a/docs/guide/examples/event_nested.rs +++ b/docs/guide/examples/event_nested.rs @@ -14,7 +14,7 @@ fn App(cx: Scope) -> Element { button { onclick: move |event| { // now, outer won't be triggered - event.cancel_bubble(); + event.stop_propogation(); }, "inner" } diff --git a/docs/guide/examples/hello_world_tui_no_ctrl_c.rs b/docs/guide/examples/hello_world_tui_no_ctrl_c.rs index 652d17752..6afa21e6c 100644 --- a/docs/guide/examples/hello_world_tui_no_ctrl_c.rs +++ b/docs/guide/examples/hello_world_tui_no_ctrl_c.rs @@ -26,7 +26,7 @@ fn App(cx: Scope) -> Element { background_color: "red", justify_content: "center", align_items: "center", - onkeydown: move |k: KeyboardEvent| if let KeyCode::Q = k.data.key_code { + onkeydown: move |k: KeyboardEvent| if let KeyCode::Q = k.key_code { tui_ctx.quit(); }, diff --git a/docs/guide/examples/hooks_bad.rs b/docs/guide/examples/hooks_bad.rs index 4fb389bb7..ff9d3417a 100644 --- a/docs/guide/examples/hooks_bad.rs +++ b/docs/guide/examples/hooks_bad.rs @@ -17,13 +17,13 @@ fn App(cx: Scope) -> Element { // 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"); + let something = use_state(cx, || "hands"); println!("clap your {something}") } // ✅ instead, *always* call use_state // You can put other stuff in the conditional though - let something = use_state(&cx, || "hands"); + let something = use_state(cx, || "hands"); if you_are_happy && you_know_it { println!("clap your {something}") } @@ -33,12 +33,12 @@ fn App(cx: Scope) -> Element { // ❌ 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); + let b = use_state(cx, || 0); b.get() }; // ✅ instead, move hook `b` outside - let b = use_state(&cx, || 0); + let b = use_state(cx, || 0); let _a = || b.get(); // ANCHOR_END: closure @@ -50,12 +50,12 @@ fn App(cx: Scope) -> Element { // ❌ 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); + 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); + let selection_map = use_ref(cx, HashMap::<&str, bool>::new); for name in &names { let is_selected = selection_map.read()[name]; @@ -63,5 +63,5 @@ fn App(cx: Scope) -> Element { } // ANCHOR_END: loop - None + cx.render(rsx!(())) } diff --git a/docs/guide/examples/hooks_composed.rs b/docs/guide/examples/hooks_composed.rs index f14c1a323..5b1f5ebfe 100644 --- a/docs/guide/examples/hooks_composed.rs +++ b/docs/guide/examples/hooks_composed.rs @@ -8,6 +8,6 @@ struct AppSettings {} // ANCHOR: wrap_context fn use_settings(cx: &ScopeState) -> UseSharedState { - use_context::(cx).expect("App settings not provided") + 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 f717ba650..1fb2a0bd7 100644 --- a/docs/guide/examples/hooks_counter.rs +++ b/docs/guide/examples/hooks_counter.rs @@ -7,7 +7,7 @@ fn main() { // ANCHOR: component fn App(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let mut count = use_state(cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } diff --git a/docs/guide/examples/hooks_counter_two_state.rs b/docs/guide/examples/hooks_counter_two_state.rs index 75fcb9117..b2a8f6e77 100644 --- a/docs/guide/examples/hooks_counter_two_state.rs +++ b/docs/guide/examples/hooks_counter_two_state.rs @@ -8,8 +8,8 @@ fn main() { // ANCHOR: component fn App(cx: Scope) -> Element { // ANCHOR: use_state_calls - let mut count_a = use_state(&cx, || 0); - let mut count_b = use_state(&cx, || 0); + let mut count_a = use_state(cx, || 0); + let mut count_b = use_state(cx, || 0); // ANCHOR_END: use_state_calls cx.render(rsx!( diff --git a/docs/guide/examples/hooks_use_ref.rs b/docs/guide/examples/hooks_use_ref.rs index 862937aab..4910325de 100644 --- a/docs/guide/examples/hooks_use_ref.rs +++ b/docs/guide/examples/hooks_use_ref.rs @@ -7,7 +7,7 @@ fn main() { // ANCHOR: component fn App(cx: Scope) -> Element { - let list = use_ref(&cx, Vec::new); + let list = use_ref(cx, Vec::new); let list_formatted = format!("{:?}", *list.read()); cx.render(rsx!( diff --git a/docs/guide/examples/input_controlled.rs b/docs/guide/examples/input_controlled.rs index b58fe58a7..bf92072a3 100644 --- a/docs/guide/examples/input_controlled.rs +++ b/docs/guide/examples/input_controlled.rs @@ -7,7 +7,7 @@ fn main() { // ANCHOR: component fn App(cx: Scope) -> Element { - let name = use_state(&cx, || "bob".to_string()); + let name = use_state(cx, || "bob".to_string()); cx.render(rsx! { input { diff --git a/docs/guide/examples/meme_editor.rs b/docs/guide/examples/meme_editor.rs index 67bef123b..c5a473da9 100644 --- a/docs/guide/examples/meme_editor.rs +++ b/docs/guide/examples/meme_editor.rs @@ -1,7 +1,6 @@ // ANCHOR: all #![allow(non_snake_case)] -use dioxus::events::FormEvent; use dioxus::prelude::*; fn main() { @@ -18,7 +17,7 @@ fn MemeEditor(cx: Scope) -> Element { width: fit-content; "; - let caption = use_state(&cx, || "me waiting for my rust code to compile".to_string()); + let caption = use_state(cx, || "me waiting for my rust code to compile".to_string()); cx.render(rsx! { div { diff --git a/docs/guide/examples/meme_editor_dark_mode.rs b/docs/guide/examples/meme_editor_dark_mode.rs index 85fed5664..e394a4a7f 100644 --- a/docs/guide/examples/meme_editor_dark_mode.rs +++ b/docs/guide/examples/meme_editor_dark_mode.rs @@ -1,7 +1,6 @@ // ANCHOR: all #![allow(non_snake_case)] -use dioxus::events::FormEvent; use dioxus::prelude::*; fn main() { @@ -14,10 +13,10 @@ struct DarkMode(bool); pub fn App(cx: Scope) -> Element { // ANCHOR: context_provider - use_context_provider(&cx, || DarkMode(false)); + use_shared_state_provider(cx, || DarkMode(false)); // ANCHOR_END: context_provider - let is_dark_mode = use_is_dark_mode(&cx); + let is_dark_mode = use_is_dark_mode(cx); let wrapper_style = if is_dark_mode { r" @@ -37,7 +36,7 @@ pub fn App(cx: Scope) -> Element { pub fn use_is_dark_mode(cx: &ScopeState) -> bool { // ANCHOR: use_context - let dark_mode_context = use_context::(cx); + let dark_mode_context = use_shared_state::(cx); // ANCHOR_END: use_context dark_mode_context @@ -47,7 +46,7 @@ pub fn use_is_dark_mode(cx: &ScopeState) -> bool { // ANCHOR: toggle pub fn DarkModeToggle(cx: Scope) -> Element { - let dark_mode = use_context::(&cx)?; + let dark_mode = use_shared_state::(cx).unwrap(); let style = if dark_mode.read().0 { "color:white" @@ -71,7 +70,7 @@ pub fn DarkModeToggle(cx: Scope) -> Element { // ANCHOR: meme_editor fn MemeEditor(cx: Scope) -> Element { - let is_dark_mode = use_is_dark_mode(&cx); + let is_dark_mode = use_is_dark_mode(cx); let heading_style = if is_dark_mode { "color: white" } else { "" }; let container_style = r" @@ -82,7 +81,7 @@ fn MemeEditor(cx: Scope) -> Element { width: fit-content; "; - let caption = use_state(&cx, || "me waiting for my rust code to compile".to_string()); + let caption = use_state(cx, || "me waiting for my rust code to compile".to_string()); cx.render(rsx! { div { @@ -152,7 +151,7 @@ fn CaptionEditor<'a>( caption: &'a str, on_input: EventHandler<'a, FormEvent>, ) -> Element<'a> { - let is_dark_mode = use_is_dark_mode(&cx); + let is_dark_mode = use_is_dark_mode(cx); let colors = if is_dark_mode { r" diff --git a/docs/guide/examples/rendering_lists.rs b/docs/guide/examples/rendering_lists.rs index deb32f1fc..75dc69b58 100644 --- a/docs/guide/examples/rendering_lists.rs +++ b/docs/guide/examples/rendering_lists.rs @@ -14,9 +14,9 @@ struct Comment { 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| { diff --git a/docs/guide/examples/rsx_overview.rs b/docs/guide/examples/rsx_overview.rs index 8dc102e8a..64fa24c78 100644 --- a/docs/guide/examples/rsx_overview.rs +++ b/docs/guide/examples/rsx_overview.rs @@ -103,7 +103,7 @@ pub fn Expression(cx: Scope) -> Element { // ANCHOR: expression let text = "Dioxus"; cx.render(rsx!(span { - [text.to_uppercase()] + text.to_uppercase() })) // ANCHOR_END: expression } diff --git a/docs/guide/examples/spawn.rs b/docs/guide/examples/spawn.rs index 9ba8e18ff..90c311200 100644 --- a/docs/guide/examples/spawn.rs +++ b/docs/guide/examples/spawn.rs @@ -8,7 +8,7 @@ fn main() { fn App(cx: Scope) -> Element { // ANCHOR: spawn - let logged_in = use_state(&cx, || false); + let logged_in = use_state(cx, || false); let log_in = move |_| { cx.spawn({ @@ -58,18 +58,18 @@ pub fn Tokio(cx: Scope) -> Element { // ANCHOR_END: tokio }; - None + cx.render(rsx!(())) } pub fn ToOwnedMacro(cx: Scope) -> Element { - let count = use_state(&cx, || 0); - let age = use_state(&cx, || 0); - let name = use_state(&cx, || 0); - let description = use_state(&cx, || 0); + let count = use_state(cx, || 0); + let age = use_state(cx, || 0); + let name = use_state(cx, || 0); + let description = use_state(cx, || 0); let _ = || { // ANCHOR: to_owned_macro - use dioxus::core::to_owned; + use dioxus::hooks::to_owned; cx.spawn({ to_owned![count, age, name, description]; @@ -80,5 +80,5 @@ pub fn ToOwnedMacro(cx: Scope) -> Element { // ANCHOR_END: to_owned_macro }; - None + cx.render(rsx!(())) } diff --git a/docs/guide/examples/use_future.rs b/docs/guide/examples/use_future.rs index 445c8675a..c0d41e5af 100644 --- a/docs/guide/examples/use_future.rs +++ b/docs/guide/examples/use_future.rs @@ -14,7 +14,7 @@ struct ApiResponse { fn App(cx: Scope) -> Element { // ANCHOR: use_future - let future = use_future(&cx, (), |_| async move { + let future = use_future(cx, (), |_| async move { reqwest::get("https://dog.ceo/api/breeds/image/random") .await .unwrap() @@ -47,7 +47,7 @@ fn App(cx: Scope) -> Element { #[inline_props] fn RandomDog(cx: Scope, breed: String) -> Element { // ANCHOR: dependency - let future = use_future(&cx, (breed,), |(breed,)| async move { + let future = use_future(cx, (breed,), |(breed,)| async move { reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) .await .unwrap() @@ -56,5 +56,5 @@ fn RandomDog(cx: Scope, breed: String) -> Element { }); // ANCHOR_END: dependency - None + cx.render(rsx!(())) } diff --git a/docs/guide/src/en/__unused/advanced-guides/12-signals.md b/docs/guide/src/en/__unused/advanced-guides/12-signals.md index 6b0f3ffea..c7012a119 100644 --- a/docs/guide/src/en/__unused/advanced-guides/12-signals.md +++ b/docs/guide/src/en/__unused/advanced-guides/12-signals.md @@ -12,7 +12,7 @@ Your component today might look something like this: ```rust fn Comp(cx: Scope) -> DomTree { - let (title, set_title) = use_state(&cx, || "Title".to_string()); + let (title, set_title) = use_state(cx, || "Title".to_string()); cx.render(rsx!{ input { value: title, @@ -26,7 +26,7 @@ This component is fairly straightforward – the input updates its own value on ```rust fn Comp(cx: Scope) -> DomTree { - let (title, set_title) = use_state(&cx, || "Title".to_string()); + let (title, set_title) = use_state(cx, || "Title".to_string()); cx.render(rsx!{ div { input { @@ -49,7 +49,7 @@ We can use signals to generate a two-way binding between data and the input box. ```rust fn Comp(cx: Scope) -> DomTree { - let mut title = use_signal(&cx, || String::from("Title")); + let mut title = use_signal(cx, || String::from("Title")); cx.render(rsx!(input { value: title })) } ``` @@ -58,8 +58,8 @@ For a slightly more interesting example, this component calculates the sum betwe ```rust fn Calculator(cx: Scope) -> DomTree { - let mut a = use_signal(&cx, || 0); - let mut b = use_signal(&cx, || 0); + let mut a = use_signal(cx, || 0); + let mut b = use_signal(cx, || 0); let mut c = a + b; rsx! { input { value: a } @@ -72,8 +72,8 @@ fn Calculator(cx: Scope) -> DomTree { Do you notice how we can use built-in operations on signals? Under the hood, we actually create a new derived signal that depends on `a` and `b`. Whenever `a` or `b` update, then `c` will update. If we need to create a new derived signal that's more complex than a basic operation (`std::ops`) we can either chain signals together or combine them: ```rust -let mut a = use_signal(&cx, || 0); -let mut b = use_signal(&cx, || 0); +let mut a = use_signal(cx, || 0); +let mut b = use_signal(cx, || 0); // Chain signals together using the `with` method let c = a.with(b).map(|(a, b)| *a + *b); @@ -84,7 +84,7 @@ let c = a.with(b).map(|(a, b)| *a + *b); If we ever need to get the value out of a signal, we can simply `deref` it. ```rust -let mut a = use_signal(&cx, || 0); +let mut a = use_signal(cx, || 0); let c = *a + *b; ``` @@ -97,7 +97,7 @@ Sometimes you want a signal to propagate across your app, either through far-awa ```rust const TITLE: Atom = || "".to_string(); const Provider: Component = |cx|{ - let title = use_signal(&cx, &TITLE); + let title = use_signal(cx, &TITLE); render!(input { value: title }) }; ``` @@ -106,7 +106,7 @@ If we use the `TITLE` atom in another component, we can cause updates to flow be ```rust const Receiver: Component = |cx|{ - let title = use_signal(&cx, &TITLE); + let title = use_signal(cx, &TITLE); log::info!("This will only be called once!"); rsx!(cx, div { @@ -133,7 +133,7 @@ Dioxus automatically understands how to use your signals when mixed with iterato ```rust const DICT: AtomFamily = |_| {}; const List: Component = |cx|{ - let dict = use_signal(&cx, &DICT); + let dict = use_signal(cx, &DICT); cx.render(rsx!( ul { For { each: dict, map: |k, v| rsx!( li { "{v}" }) } diff --git a/docs/guide/src/en/__unused/composing.md b/docs/guide/src/en/__unused/composing.md index 5746d6477..06648a4c1 100644 --- a/docs/guide/src/en/__unused/composing.md +++ b/docs/guide/src/en/__unused/composing.md @@ -59,7 +59,7 @@ If we represented the reactive graph presented above in Dioxus, it would look ve // Declare a component that holds our datasources and calculates `g` fn RenderGraph(cx: Scope) -> Element { let seconds = use_datasource(SECONDS); - let constant = use_state(&cx, || 1); + let constant = use_state(cx, || 1); cx.render(rsx!( RenderG { seconds: seconds } diff --git a/docs/guide/src/en/__unused/fanout.md b/docs/guide/src/en/__unused/fanout.md index 087d4b543..20c4c9974 100644 --- a/docs/guide/src/en/__unused/fanout.md +++ b/docs/guide/src/en/__unused/fanout.md @@ -28,8 +28,8 @@ If we used global state like use_context or fermi, we might be tempted to inject ```rust fn Titlebar(cx: Scope) -> Element { - let title = use_read(&cx, TITLE); - let subtitle = use_read(&cx, SUBTITLE); + let title = use_read(cx, TITLE); + let subtitle = use_read(cx, SUBTITLE); cx.render(rsx!{/* ui */}) } @@ -43,11 +43,11 @@ To enable our titlebar component to be used across apps, we want to lift our ato ```rust fn DocsiteTitlesection(cx: Scope) { - let title = use_read(&cx, TITLE); - let subtitle = use_read(&cx, SUBTITLE); + let title = use_read(cx, TITLE); + let subtitle = use_read(cx, SUBTITLE); - let username = use_read(&cx, USERNAME); - let points = use_read(&cx, POINTS); + let username = use_read(cx, USERNAME); + let points = use_read(cx, POINTS); cx.render(rsx!{ TitleBar { title: title, subtitle: subtitle } diff --git a/docs/guide/src/en/__unused/localstate.md b/docs/guide/src/en/__unused/localstate.md index bf5ae7014..2a20d77d4 100644 --- a/docs/guide/src/en/__unused/localstate.md +++ b/docs/guide/src/en/__unused/localstate.md @@ -13,7 +13,7 @@ struct Todo { is_editing: bool, } -let todos = use_ref(&cx, || vec![Todo::new()]); +let todos = use_ref(cx, || vec![Todo::new()]); cx.render(rsx!{ ul { @@ -40,7 +40,7 @@ Instead, let's refactor our Todo component to handle its own state: ```rust #[inline_props] fn Todo<'a>(cx: Scope, todo: &'a Todo) -> Element { - let is_hovered = use_state(&cx, || false); + let is_hovered = use_state(cx, || false); cx.render(rsx!{ li { @@ -80,15 +80,15 @@ struct State { } // in the component -let state = use_ref(&cx, State::new) +let state = use_ref(cx, State::new) ``` The "better" approach for this particular component would be to break the state apart into different values: ```rust -let count = use_state(&cx, || 0); -let color = use_state(&cx, || "red"); -let names = use_state(&cx, HashMap::new); +let count = use_state(cx, || 0); +let color = use_state(cx, || "red"); +let names = use_state(cx, HashMap::new); ``` You might recognize that our "names" value is a HashMap – which is not terribly cheap to clone every time we update its value. To solve this issue, we *highly* suggest using a library like [`im`](https://crates.io/crates/im) which will take advantage of structural sharing to make clones and mutations much cheaper. @@ -96,7 +96,7 @@ You might recognize that our "names" value is a HashMap – which is not terribl When combined with the `make_mut` method on `use_state`, you can get really succinct updates to collections: ```rust -let todos = use_state(&cx, im_rc::HashMap::default); +let todos = use_state(cx, im_rc::HashMap::default); todos.make_mut().insert("new todo", Todo { contents: "go get groceries", diff --git a/docs/guide/src/en/__unused/memoization.md b/docs/guide/src/en/__unused/memoization.md index ef476a457..9bd0e8b24 100644 --- a/docs/guide/src/en/__unused/memoization.md +++ b/docs/guide/src/en/__unused/memoization.md @@ -8,8 +8,8 @@ For example, let's say we have a component that has two children: ```rust fn Demo(cx: Scope) -> Element { // don't worry about these 2, we'll cover them later - let name = use_state(&cx, || String::from("bob")); - let age = use_state(&cx, || 21); + let name = use_state(cx, || String::from("bob")); + let age = use_state(cx, || 21); cx.render(rsx!{ Name { name: name } diff --git a/docs/guide/src/en/__unused/model_pattern.md b/docs/guide/src/en/__unused/model_pattern.md index 67f21321a..e66b6981c 100644 --- a/docs/guide/src/en/__unused/model_pattern.md +++ b/docs/guide/src/en/__unused/model_pattern.md @@ -20,7 +20,7 @@ Our component is really simple – we just call `use_ref` to get an initial calc ```rust fn app(cx: Scope) -> Element { - let state = use_ref(&cx, Calculator::new); + let state = use_ref(cx, Calculator::new); cx.render(rsx!{ // the rendering code diff --git a/docs/guide/src/en/async/use_coroutine.md b/docs/guide/src/en/async/use_coroutine.md index b3228b3e8..326141b2b 100644 --- a/docs/guide/src/en/async/use_coroutine.md +++ b/docs/guide/src/en/async/use_coroutine.md @@ -10,7 +10,7 @@ The basic setup for coroutines is the `use_coroutine` hook. Most coroutines we w ```rust fn app(cx: Scope) -> Element { - let ws: &UseCoroutine<()> = use_coroutine(&cx, |rx| async move { + let ws: &UseCoroutine<()> = use_coroutine(cx, |rx| async move { // Connect to some sort of service let mut conn = connect_to_ws_server().await; @@ -27,7 +27,7 @@ For many services, a simple async loop will handle the majority of use cases. However, if we want to temporarily disable the coroutine, we can "pause" it using the `pause` method, and "resume" it using the `resume` method: ```rust -let sync: &UseCoroutine<()> = use_coroutine(&cx, |rx| async move { +let sync: &UseCoroutine<()> = use_coroutine(cx, |rx| async move { // code for syncing }); @@ -63,7 +63,7 @@ enum ProfileUpdate { SetAge(i32) } -let profile = use_coroutine(&cx, |mut rx: UnboundedReciver| async move { +let profile = use_coroutine(cx, |mut rx: UnboundedReciver| async move { let mut server = connect_to_server().await; while let Ok(msg) = rx.next().await { @@ -86,9 +86,9 @@ cx.render(rsx!{ For sufficiently complex apps, we could build a bunch of different useful "services" that loop on channels to update the app. ```rust -let profile = use_coroutine(&cx, profile_service); -let editor = use_coroutine(&cx, editor_service); -let sync = use_coroutine(&cx, sync_service); +let profile = use_coroutine(cx, profile_service); +let editor = use_coroutine(cx, editor_service); +let sync = use_coroutine(cx, sync_service); async fn profile_service(rx: UnboundedReceiver) { // do stuff @@ -109,9 +109,9 @@ We can combine coroutines with Fermi to emulate Redux Toolkit's Thunk system wit static USERNAME: Atom = |_| "default".to_string(); fn app(cx: Scope) -> Element { - let atoms = use_atom_root(&cx); + let atoms = use_atom_root(cx); - use_coroutine(&cx, |rx| sync_service(rx, atoms.clone())); + use_coroutine(cx, |rx| sync_service(rx, atoms.clone())); cx.render(rsx!{ Banner {} @@ -119,7 +119,7 @@ fn app(cx: Scope) -> Element { } fn Banner(cx: Scope) -> Element { - let username = use_read(&cx, USERNAME); + let username = use_read(cx, USERNAME); cx.render(rsx!{ h1 { "Welcome back, {username}" } @@ -158,8 +158,8 @@ To yield values from a coroutine, simply bring in a `UseState` handle and set th ```rust -let sync_status = use_state(&cx, || Status::Launching); -let sync_task = use_coroutine(&cx, |rx: UnboundedReceiver| { +let sync_status = use_state(cx, || Status::Launching); +let sync_task = use_coroutine(cx, |rx: UnboundedReceiver| { to_owned![sync_status]; async move { loop { @@ -176,7 +176,7 @@ Coroutine handles are automatically injected through the context API. `use_corou ```rust fn Child(cx: Scope) -> Element { - let sync_task = use_coroutine_handle::(&cx); + let sync_task = use_coroutine_handle::(cx); sync_task.send(SyncAction::SetUsername); } diff --git a/docs/guide/src/en/best_practices/error_handling.md b/docs/guide/src/en/best_practices/error_handling.md index c9a465532..afbb45417 100644 --- a/docs/guide/src/en/best_practices/error_handling.md +++ b/docs/guide/src/en/best_practices/error_handling.md @@ -56,7 +56,7 @@ The next "best" way of handling errors in Dioxus is to match on the error locall To do this, we simply have an error state built into our component: ```rust -let err = use_state(&cx, || None); +let err = use_state(cx, || None); ``` Whenever we perform an action that generates an error, we'll set that error state. We can then match on the error in a number of ways (early return, return Element, etc). @@ -64,7 +64,7 @@ Whenever we perform an action that generates an error, we'll set that error stat ```rust fn Commandline(cx: Scope) -> Element { - let error = use_state(&cx, || None); + let error = use_state(cx, || None); cx.render(match *error { Some(error) => rsx!( @@ -85,7 +85,7 @@ If you're dealing with a handful of components with minimal nesting, you can jus ```rust fn Commandline(cx: Scope) -> Element { - let error = use_state(&cx, || None); + let error = use_state(cx, || None); if let Some(error) = **error { return cx.render(rsx!{ "An error occured" }); @@ -125,7 +125,7 @@ Then, in our top level component, we want to explicitly handle the possible erro ```rust fn TopLevel(cx: Scope) -> Element { - let error = use_read(&cx, INPUT_ERROR); + let error = use_read(cx, INPUT_ERROR); match error { TooLong => return cx.render(rsx!{ "FAILED: Too long!" }), @@ -139,7 +139,7 @@ Now, whenever a downstream component has an error in its actions, it can simply ```rust fn Commandline(cx: Scope) -> Element { - let set_error = use_set(&cx, INPUT_ERROR); + let set_error = use_set(cx, INPUT_ERROR); cx.render(rsx!{ input { diff --git a/docs/guide/src/en/best_practices/index.md b/docs/guide/src/en/best_practices/index.md index 2270508f1..6b9f0ed1d 100644 --- a/docs/guide/src/en/best_practices/index.md +++ b/docs/guide/src/en/best_practices/index.md @@ -12,18 +12,3 @@ While it is possible to share state between components, this should only be done - Keep state local to a component if possible - When sharing state through props, only pass down the specific data necessary - -## Reusable Libraries - -When publishing a library designed to work with Dioxus, we highly advise using only the core feature on the `dioxus` crate. This makes your crate compile faster, makes it more stable, and avoids bringing in incompatible libraries that might make it not compile on unsupported platforms. - - -❌ Don't include unnecessary dependencies in libraries: -```toml -dioxus = { version = "...", features = ["web", "desktop", "full"]} -``` - -✅ Only add the features you need: -```toml -dioxus = { version = "...", features = "core"} -``` diff --git a/docs/guide/src/en/getting_started/desktop.md b/docs/guide/src/en/getting_started/desktop.md index c91aa634a..0a963ff61 100644 --- a/docs/guide/src/en/getting_started/desktop.md +++ b/docs/guide/src/en/getting_started/desktop.md @@ -25,19 +25,13 @@ cargo new --bin demo cd demo ``` -Add Dioxus with the `desktop` feature (this will edit `Cargo.toml`): +Add Dioxus and the `desktop` renderer (this will edit `Cargo.toml`): ```shell -cargo add dioxus --features desktop +cargo add dioxus +cargo add dioxus-desktop ``` -> If your system does not provide the `libappindicator3` library, like Debian/bullseye, you can enable the replacement `ayatana` with an additional flag: -> ->```shell -># On Debian/bullseye use: ->cargo add dioxus --features desktop --features ayatana ->``` - Edit your `main.rs`: ```rust diff --git a/docs/guide/src/en/getting_started/hot_reload.md b/docs/guide/src/en/getting_started/hot_reload.md index e5d7b0327..7434fdb9c 100644 --- a/docs/guide/src/en/getting_started/hot_reload.md +++ b/docs/guide/src/en/getting_started/hot_reload.md @@ -8,7 +8,7 @@ Install [dioxus-cli](https://github.com/DioxusLabs/cli). Enable the hot-reload feature on dioxus: ```toml -dioxus = { version = "*", features = ["web", "hot-reload"] } +dioxus = { version = "*", features = ["hot-reload"] } ``` # Usage diff --git a/docs/guide/src/en/getting_started/mobile.md b/docs/guide/src/en/getting_started/mobile.md index ad49817ec..f8dd5f4ff 100644 --- a/docs/guide/src/en/getting_started/mobile.md +++ b/docs/guide/src/en/getting_started/mobile.md @@ -5,12 +5,13 @@ 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 the platform's WebView, meaning that animations, transparency, and native widgets are not currently achievable. +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. -In addition, iOS is the only supported Mobile Platform. It is possible to get Dioxus running on Android and rendered with WebView, but the Rust windowing library that Dioxus uses – tao – does not currently support Android. 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`. + ## Getting Set up Getting set up with mobile can be quite challenging. The tooling here isn't great (yet) and might take some hacking around to get things working. macOS M1 is broadly unexplored and might not work for you. @@ -51,7 +52,8 @@ path = "gen/bin/desktop.rs" # clear all the dependencies [dependencies] mobile-entry-point = "0.1.0" -dioxus = { version = "*", features = ["mobile"] } +dioxus = { version = "*"} +dioxus-desktop = { version = "*" } simple_logger = "*" ``` @@ -61,7 +63,7 @@ Edit your `lib.rs`: use dioxus::prelude::*; fn main() { - dioxus_mobile::launch(app); + dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { diff --git a/docs/guide/src/en/getting_started/ssr.md b/docs/guide/src/en/getting_started/ssr.md index e4f825f0e..7e28cd304 100644 --- a/docs/guide/src/en/getting_started/ssr.md +++ b/docs/guide/src/en/getting_started/ssr.md @@ -36,10 +36,11 @@ cargo new --bin demo cd app ``` -Add Dioxus with the `ssr` feature: +Add Dioxus and the `ssr` renderer feature: ```shell -cargo add dioxus --features ssr +cargo add dioxus +cargo add dioxus-ssr ``` Next, add all the Axum dependencies. This will be different if you're using a different Web Framework @@ -54,7 +55,8 @@ Your dependencies should look roughly like this: ```toml [dependencies] axum = "0.4.5" -dioxus = { version = "*", features = ["ssr"] } +dioxus = { version = "*" } +dioxus-ssr = { version = "*" } tokio = { version = "1.15.0", features = ["full"] } ``` diff --git a/docs/guide/src/en/getting_started/tui.md b/docs/guide/src/en/getting_started/tui.md index 67f366e18..980af5706 100644 --- a/docs/guide/src/en/getting_started/tui.md +++ b/docs/guide/src/en/getting_started/tui.md @@ -13,12 +13,13 @@ TUI support is currently quite experimental. Even the project name will change. ## Getting Set up -Start by making a new package and adding our TUI feature. +Start by making a new package and adding our TUI renderer. ```shell cargo new --bin demo cd demo -cargo add dioxus --features tui +cargo add dioxus +cargo add dioxus-tui ``` Then, edit your `main.rs` with the basic template. @@ -42,5 +43,6 @@ Press "ctrl-c" to close the app. To switch from "ctrl-c" to just "q" to quit yo ## 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 119f9b8e5..311cf277a 100644 --- a/docs/guide/src/en/getting_started/web.md +++ b/docs/guide/src/en/getting_started/web.md @@ -38,10 +38,11 @@ cargo new --bin demo cd demo ``` -Add Dioxus as a dependency with the `web` feature: +Add Dioxus as a dependency and add the web renderer: ```bash -cargo add dioxus --features web +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": diff --git a/docs/guide/src/en/index.md b/docs/guide/src/en/index.md index 761f1e7d8..770be440e 100644 --- a/docs/guide/src/en/index.md +++ b/docs/guide/src/en/index.md @@ -6,7 +6,7 @@ Dioxus is a portable, performant, and ergonomic framework for building cross-pla ```rust fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let mut count = use_state(cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } @@ -48,4 +48,4 @@ Web: Since the web is a fairly mature platform, we expect there to be very littl Desktop: APIs will likely be in flux as we figure out better patterns than our ElectronJS counterpart. -SSR: We don't expect the SSR API to change drastically in the future. \ No newline at end of file +SSR: We don't expect the SSR API to change drastically in the future. diff --git a/docs/guide/src/en/interactivity/router.md b/docs/guide/src/en/interactivity/router.md index e1d170ec3..cf629f6d2 100644 --- a/docs/guide/src/en/interactivity/router.md +++ b/docs/guide/src/en/interactivity/router.md @@ -19,7 +19,8 @@ This is where the router crates come in handy. To make sure we're using the rout ```toml [dependencies] -dioxus = { version = "0.2", features = ["desktop", "router"] } +dioxus = { version = "*" } +dioxus-router = { version = "*" } ``` diff --git a/docs/guide/src/en/roadmap.md b/docs/guide/src/en/roadmap.md index 281520b73..1c4e9c1bf 100644 --- a/docs/guide/src/en/roadmap.md +++ b/docs/guide/src/en/roadmap.md @@ -50,7 +50,7 @@ Generally, here's the status of each platform: | 1st class global state | ✅ | redux/recoil/mobx on top of context | | Runs natively | ✅ | runs as a portable binary w/o a runtime (Node) | | Subtree Memoization | ✅ | skip diffing static element subtrees | -| High-efficiency templates | 🛠 | rsx! calls are translated to templates on the DOM's side | +| High-efficiency templates | ✅ | rsx! calls are translated to templates on the DOM's side | | Compile-time correct | ✅ | Throw errors on invalid template layouts | | Heuristic Engine | ✅ | track component memory usage to minimize future allocations | | Fine-grained reactivity | 👀 | Skip diffing for fine-grain updates | @@ -66,7 +66,7 @@ These Features are planned for the future of Dioxus: ### Core - [x] Release of Dioxus Core - [x] Upgrade documentation to include more theory and be more comprehensive -- [ ] Support for HTML-side templates for lightning-fast dom manipulation +- [x] Support for HTML-side templates for lightning-fast dom manipulation - [ ] Support for multiple renderers for same virtualdom (subtrees) - [ ] Support for ThreadSafe (Send + Sync) - [ ] Support for Portals diff --git a/docs/guide/src/pt-br/async/use_coroutine.md b/docs/guide/src/pt-br/async/use_coroutine.md index c27878fd8..cd7cbff11 100644 --- a/docs/guide/src/pt-br/async/use_coroutine.md +++ b/docs/guide/src/pt-br/async/use_coroutine.md @@ -10,7 +10,7 @@ A configuração básica para corrotinas é o _hook_ `use_coroutine`. A maioria ```rust fn app(cx: Scope) -> Element { - let ws: &UseCoroutine<()> = use_coroutine(&cx, |rx| async move { + let ws: &UseCoroutine<()> = use_coroutine(cx, |rx| async move { // Connect to some sort of service let mut conn = connect_to_ws_server().await; @@ -27,7 +27,7 @@ Para muitos serviços, um _loop_ assíncrono simples lidará com a maioria dos c No entanto, se quisermos desabilitar temporariamente a corrotina, podemos "pausá-la" usando o método `pause` e "retomá-la" usando o método `resume`: ```rust -let sync: &UseCoroutine<()> = use_coroutine(&cx, |rx| async move { +let sync: &UseCoroutine<()> = use_coroutine(cx, |rx| async move { // code for syncing }); @@ -62,7 +62,7 @@ enum ProfileUpdate { SetAge(i32) } -let profile = use_coroutine(&cx, |mut rx: UnboundedReciver| async move { +let profile = use_coroutine(cx, |mut rx: UnboundedReciver| async move { let mut server = connect_to_server().await; while let Ok(msg) = rx.next().await { @@ -85,9 +85,9 @@ cx.render(rsx!{ Para aplicativos suficientemente complexos, poderíamos criar vários "serviços" úteis diferentes que fazem um _loop_ nos canais para atualizar o aplicativo. ```rust -let profile = use_coroutine(&cx, profile_service); -let editor = use_coroutine(&cx, editor_service); -let sync = use_coroutine(&cx, sync_service); +let profile = use_coroutine(cx, profile_service); +let editor = use_coroutine(cx, editor_service); +let sync = use_coroutine(cx, sync_service); async fn profile_service(rx: UnboundedReceiver) { // do stuff @@ -108,9 +108,9 @@ Podemos combinar corrotinas com `Fermi` para emular o sistema `Thunk` do **Redux static USERNAME: Atom = |_| "default".to_string(); fn app(cx: Scope) -> Element { - let atoms = use_atom_root(&cx); + let atoms = use_atom_root(cx); - use_coroutine(&cx, |rx| sync_service(rx, atoms.clone())); + use_coroutine(cx, |rx| sync_service(rx, atoms.clone())); cx.render(rsx!{ Banner {} @@ -118,7 +118,7 @@ fn app(cx: Scope) -> Element { } fn Banner(cx: Scope) -> Element { - let username = use_read(&cx, USERNAME); + let username = use_read(cx, USERNAME); cx.render(rsx!{ h1 { "Welcome back, {username}" } @@ -156,8 +156,8 @@ async fn sync_service(mut rx: UnboundedReceiver, atoms: AtomRoot) { Para obter valores de uma corrotina, basta usar um identificador `UseState` e definir o valor sempre que sua corrotina concluir seu trabalho. ```rust -let sync_status = use_state(&cx, || Status::Launching); -let sync_task = use_coroutine(&cx, |rx: UnboundedReceiver| { +let sync_status = use_state(cx, || Status::Launching); +let sync_task = use_coroutine(cx, |rx: UnboundedReceiver| { to_owned![sync_status]; async move { loop { @@ -174,7 +174,7 @@ Os identificadores de corrotina são injetados automaticamente por meio da API d ```rust fn Child(cx: Scope) -> Element { - let sync_task = use_coroutine_handle::(&cx); + let sync_task = use_coroutine_handle::(cx); sync_task.send(SyncAction::SetUsername); } diff --git a/docs/guide/src/pt-br/best_practices/error_handling.md b/docs/guide/src/pt-br/best_practices/error_handling.md index 52c560b6d..938322234 100644 --- a/docs/guide/src/pt-br/best_practices/error_handling.md +++ b/docs/guide/src/pt-br/best_practices/error_handling.md @@ -53,14 +53,14 @@ A próxima "melhor" maneira de lidar com erros no Dioxus é combinar (`match`) o Para fazer isso, simplesmente temos um estado de erro embutido em nosso componente: ```rust -let err = use_state(&cx, || None); +let err = use_state(cx, || None); ``` Sempre que realizarmos uma ação que gere um erro, definiremos esse estado de erro. Podemos então combinar o erro de várias maneiras (retorno antecipado, elemento de retorno etc.). ```rust fn Commandline(cx: Scope) -> Element { - let error = use_state(&cx, || None); + let error = use_state(cx, || None); cx.render(match *error { Some(error) => rsx!( @@ -81,7 +81,7 @@ Se você estiver lidando com alguns componentes com um mínimo de aninhamento, b ```rust fn Commandline(cx: Scope) -> Element { - let error = use_state(&cx, || None); + let error = use_state(cx, || None); if let Some(error) = **error { return cx.render(rsx!{ "An error occured" }); @@ -120,7 +120,7 @@ Então, em nosso componente de nível superior, queremos tratar explicitamente o ```rust fn TopLevel(cx: Scope) -> Element { - let error = use_read(&cx, INPUT_ERROR); + let error = use_read(cx, INPUT_ERROR); match error { TooLong => return cx.render(rsx!{ "FAILED: Too long!" }), @@ -134,7 +134,7 @@ Agora, sempre que um componente _downstream_ tiver um erro em suas ações, ele ```rust fn Commandline(cx: Scope) -> Element { - let set_error = use_set(&cx, INPUT_ERROR); + let set_error = use_set(cx, INPUT_ERROR); cx.render(rsx!{ input { diff --git a/docs/guide/src/pt-br/best_practices/index.md b/docs/guide/src/pt-br/best_practices/index.md index 3330c4327..ee8506241 100644 --- a/docs/guide/src/pt-br/best_practices/index.md +++ b/docs/guide/src/pt-br/best_practices/index.md @@ -12,19 +12,3 @@ Embora seja possível compartilhar o estado entre os componentes, isso só deve - Mantenha o estado local para um componente, se possível - Ao compartilhar o estado por meio de adereços, passe apenas os dados específicos necessários - -## Bibliotecas Reutilizáveis - -Ao publicar uma biblioteca projetada para funcionar com o Dioxus, é altamente recomendável usar apenas o recurso principal na `crate` `dioxus`. Isso faz com que sua `crate` seja compilada mais rapidamente, mais estável e evita a inclusão de bibliotecas incompatíveis que podem fazer com que ela não seja compilada em plataformas não suportadas. - -❌ Não inclua dependências desnecessárias nas bibliotecas: - -```toml -dioxus = { version = "...", features = ["web", "desktop", "full"]} -``` - -✅ Adicione apenas os recursos que você precisa: - -```toml -dioxus = { version = "...", features = "core"} -``` diff --git a/docs/guide/src/pt-br/getting_started/desktop.md b/docs/guide/src/pt-br/getting_started/desktop.md index abb7c9309..5c6940b96 100644 --- a/docs/guide/src/pt-br/getting_started/desktop.md +++ b/docs/guide/src/pt-br/getting_started/desktop.md @@ -29,16 +29,10 @@ cd demo Adicione o Dioxus com o recurso `desktop` (isso irá editar o `Cargo.toml`): ```shell -cargo add dioxus --features desktop +cargo add dioxus +cargo add dioxus-desktop ``` -> Se seu sistema não fornece a biblioteca `libappindicator3`, como Debian/bullseye, você pode habilitar a substituta `ayatana` com um _flag_ adicional: -> -> ```shell -> # On Debian/bullseye use: -> cargo add dioxus --features desktop --features ayatana -> ``` - Edite seu `main.rs`: ```rust diff --git a/docs/guide/src/pt-br/getting_started/hot_reload.md b/docs/guide/src/pt-br/getting_started/hot_reload.md index e56499c5f..2dabde536 100644 --- a/docs/guide/src/pt-br/getting_started/hot_reload.md +++ b/docs/guide/src/pt-br/getting_started/hot_reload.md @@ -10,7 +10,7 @@ Instale o [dioxus-cli](https://github.com/DioxusLabs/cli). Habilite o recurso de _hot-reload_ no dioxus: ```toml -dioxus = { version = "*", features = ["web", "hot-reload"] } +dioxus = { version = "*", features = ["hot-reload"] } ``` # Usage diff --git a/docs/guide/src/pt-br/getting_started/mobile.md b/docs/guide/src/pt-br/getting_started/mobile.md index 57c2a6ccf..edcfbfba7 100644 --- a/docs/guide/src/pt-br/getting_started/mobile.md +++ b/docs/guide/src/pt-br/getting_started/mobile.md @@ -52,7 +52,8 @@ path = "gen/bin/desktop.rs" # clear all the dependencies [dependencies] mobile-entry-point = "0.1.0" -dioxus = { version = "*", features = ["mobile"] } +dioxus = { version = "*" } +dioxus-desktop = { version = "*" } simple_logger = "*" ``` @@ -62,7 +63,7 @@ Edite seu `lib.rs`: use dioxus::prelude::*; fn main() { - dioxus::mobile::launch(app); + dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { diff --git a/docs/guide/src/pt-br/getting_started/ssr.md b/docs/guide/src/pt-br/getting_started/ssr.md index f3e3caf8e..ddb02a289 100644 --- a/docs/guide/src/pt-br/getting_started/ssr.md +++ b/docs/guide/src/pt-br/getting_started/ssr.md @@ -38,7 +38,8 @@ cd app Adicione o Dioxus com o recurso `ssr`: ```shell -cargo add dioxus --features ssr +cargo add dioxus +cargo add dioxus-ssr ``` Em seguida, adicione todas as dependências do Axum. Isso será diferente se você estiver usando um Web Framework diferente @@ -53,7 +54,8 @@ Suas dependências devem ficar mais ou menos assim: ```toml [dependencies] axum = "0.4.5" -dioxus = { version = "*", features = ["ssr"] } +dioxus = { version = "*" } +dioxus-ssr = { version = "*" } tokio = { version = "1.15.0", features = ["full"] } ``` @@ -83,7 +85,7 @@ E, em seguida, adicione nosso _endpoint_. Podemos renderizar `rsx!` diretamente: ```rust async fn app_endpoint() -> Html { - Html(dioxus::ssr::render_lazy(rsx! { + Html(dioxus_ssr::render_lazy(rsx! { h1 { "hello world!" } })) } @@ -99,7 +101,7 @@ async fn app_endpoint() -> Html { let mut app = VirtualDom::new(app); let _ = app.rebuild(); - Html(dioxus::ssr::render_vdom(&app)) + Html(dioxus_ssr::render_vdom(&app)) } ``` diff --git a/docs/guide/src/pt-br/getting_started/tui.md b/docs/guide/src/pt-br/getting_started/tui.md index aa99ca763..deaf060b6 100644 --- a/docs/guide/src/pt-br/getting_started/tui.md +++ b/docs/guide/src/pt-br/getting_started/tui.md @@ -17,7 +17,8 @@ Comece criando um novo pacote e adicionando nosso recurso TUI. ```shell cargo new --bin demo cd demo -cargo add dioxus --features tui +cargo add dioxus +cargo add dioxus-tui ``` Em seguida, edite seu `main.rs` com o modelo básico. diff --git a/docs/guide/src/pt-br/getting_started/web.md b/docs/guide/src/pt-br/getting_started/web.md index 87fb4f76b..ffb322a25 100644 --- a/docs/guide/src/pt-br/getting_started/web.md +++ b/docs/guide/src/pt-br/getting_started/web.md @@ -43,7 +43,8 @@ cd demo Adicione o Dioxus como uma dependência com o recurso `web`: ```bash -cargo add dioxus --features web +cargo add dioxus +cargo add dioxus-web ``` Adicione um `index.html` para o `Trunk` usar. Certifique-se de que seu elemento "mount point" tenha um ID de "main": diff --git a/docs/guide/src/pt-br/index.md b/docs/guide/src/pt-br/index.md index dffd70462..121f3ef45 100644 --- a/docs/guide/src/pt-br/index.md +++ b/docs/guide/src/pt-br/index.md @@ -6,7 +6,7 @@ Dioxus é uma estrutura portátil, de alto desempenho e ergonômica para a const ```rust fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let mut count = use_state(cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } diff --git a/docs/guide/src/pt-br/interactivity/router.md b/docs/guide/src/pt-br/interactivity/router.md index c7453bddc..d3ac7abb9 100644 --- a/docs/guide/src/pt-br/interactivity/router.md +++ b/docs/guide/src/pt-br/interactivity/router.md @@ -18,7 +18,8 @@ Cada uma dessas cenas é independente – não queremos renderizar a página ini ```toml [dependencies] -dioxus = { version = "0.2", features = ["desktop", "router"] } +dioxus = { version = "*" } +dioxus-router = { version = "*" } ``` ## Usando o Roteador diff --git a/docs/posts/release-0-2-0.md b/docs/posts/release-0-2-0.md index 79a341e91..7aed2fda6 100644 --- a/docs/posts/release-0-2-0.md +++ b/docs/posts/release-0-2-0.md @@ -27,7 +27,7 @@ Dioxus is a recently-released library for building interactive user interfaces ( ```rust fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let mut count = use_state(cx, || 0); cx.render(rsx! { h1 { "Count: {count}" } @@ -102,8 +102,8 @@ We're also using hooks to parse the URL parameters and segments so you can inter struct Query { name: String } fn BlogPost(cx: Scope) -> Element { - let post = use_route(&cx).segment("post")?; - let query = use_route(&cx).query::()?; + let post = use_route(cx).segment("post")?; + let query = use_route(cx).query::()?; cx.render(rsx!{ "Viewing post {post}" @@ -128,7 +128,7 @@ static TITLE: Atom<&str> = |_| "Hello"; // Read the value from anywhere in the app, subscribing to any changes fn app(cx: Scope) -> Element { - let title = use_read(&cx, TITLE); + let title = use_read(cx, TITLE); cx.render(rsx!{ h1 { "{title}" } Child {} @@ -137,7 +137,7 @@ fn app(cx: Scope) -> Element { // Set the value from anywhere in the app fn Child(cx: Scope) -> Element { - let set_title = use_set(&cx, TITLE); + let set_title = use_set(cx, TITLE); cx.render(rsx!{ button { onclick: move |_| set_title("goodbye"), @@ -245,7 +245,7 @@ First, we upgraded the `use_future` hook. It now supports dependencies, which le ```rust fn RenderDog(cx: Scope, breed: String) -> Element { - let dog_request = use_future(&cx, (breed,), |(breed,)| async move { + let dog_request = use_future(cx, (breed,), |(breed,)| async move { reqwest::get(format!("https://dog.ceo/api/breed/{}/images/random", breed)) .await .unwrap() @@ -265,7 +265,7 @@ Additionally, we added better support for coroutines. You can now start, stop, r ```rust fn App(cx: Scope) -> Element { - let sync_task = use_coroutine(&cx, |rx| async move { + let sync_task = use_coroutine(cx, |rx| async move { connect_to_server().await; let state = MyState::new(); diff --git a/docs/posts/release.md b/docs/posts/release.md index 80f8a0f4f..b40b451cc 100644 --- a/docs/posts/release.md +++ b/docs/posts/release.md @@ -28,7 +28,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let mut count = use_state(cx, || 0); cx.render(rsx! { h1 { "Count: {count}" } @@ -138,7 +138,7 @@ struct CardProps { } static Card: Component = |cx| { - let mut count = use_state(&cx, || 0); + let mut count = use_state(cx, || 0); cx.render(rsx!( aside { h2 { "{cx.props.title}" } @@ -191,7 +191,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let mut count = use_state(cx, || 0); cx.render(rsx! { h1 { "Count: {count}" } @@ -260,7 +260,7 @@ Dioxus understands the lifetimes of data borrowed from `Scope`, so you can safel ```rust -let name = use_state(&cx, || "asd"); +let name = use_state(cx, || "asd"); rsx! { div { button { onclick: move |_| name.set("abc") } @@ -274,7 +274,7 @@ Because we know the lifetime of your handlers, we can also expose this to childr ```rust fn app(cx: Scope) -> Element { - let name = use_state(&cx, || "asd"); + let name = use_state(cx, || "asd"); cx.render(rsx!{ Button { name: name } }) diff --git a/docs/router/src/guide/building-a-nest.md b/docs/router/src/guide/building-a-nest.md index 5abe6ede7..532af6c3c 100644 --- a/docs/router/src/guide/building-a-nest.md +++ b/docs/router/src/guide/building-a-nest.md @@ -10,12 +10,12 @@ Let's create a new ``navbar`` component: fn navbar(cx: Scope) -> Element { cx.render(rsx! { ul { - + } }) } ``` -Our navbar will be a list of links going between our pages. We could always use an HTML anchor element but that would cause our page to unnecessarily reload. Instead we want to use the ``Link`` component provided by Dioxus Router. +Our navbar will be a list of links going between our pages. We could always use an HTML anchor element but that would cause our page to unnecessarily reload. Instead we want to use the ``Link`` component provided by Dioxus Router. The Link component is very similar to the Route component. It takes a path and an element. Add the Link component into your use statement and then add some links: ```rs @@ -38,7 +38,7 @@ fn navbar(cx: Scope) -> Element { } ``` >By default, the Link component only works for links within your application. To link to external sites, add the ``external: true`` property. ->```rs +>```rs >Link { to: "https://github.com", external: true, "GitHub"} >``` @@ -66,7 +66,7 @@ We want to store our blogs in a database and load them as needed. This'll help p We could utilize a search page that loads a blog when clicked but then our users won't be able to share our blogs easily. This is where URL parameters come in. And finally, we also want our site to tell users they are on a blog page whenever the URL starts with``/blog``. The path to our blog will look like ``/blog/myBlogPage``. ``myBlogPage`` being the URL parameter. -Dioxus Router uses the ``:name`` pattern so our route will look like ``/blog/:post``. +Dioxus Router uses the ``:name`` pattern so our route will look like ``/blog/:post``. First, lets tell users when they are on a blog page. Add a new route in your app component. ```rs @@ -77,7 +77,7 @@ fn app(cx: Scope) -> Element { self::navbar {} Route { to: "/", self::homepage {}} // NEW - Route { + Route { to: "/blog", } Route { to: "", self::page_not_found {}} @@ -93,7 +93,7 @@ fn app(cx: Scope) -> Element { p { "-- Dioxus Blog --" } self::navbar {} Route { to: "/", self::homepage {}} - Route { + Route { to: "/blog", Route { to: "/:post", "This is my blog post!" } // NEW } @@ -109,7 +109,7 @@ fn app(cx: Scope) -> Element { Router { self::navbar {} Route { to: "/", self::homepage {}} - Route { + Route { to: "/blog", p { "-- Dioxus Blog --" } // MOVED Route { to: "/:post", "This is my blog post!" } @@ -119,7 +119,7 @@ fn app(cx: Scope) -> Element { }) } ``` -Now our ``-- Dioxus Blog --`` text will be displayed whenever a user is on a path that starts with ``/blog``. Displaying content in a way that is page-agnostic is useful when building navigation menus, footers, and similar. +Now our ``-- Dioxus Blog --`` text will be displayed whenever a user is on a path that starts with ``/blog``. Displaying content in a way that is page-agnostic is useful when building navigation menus, footers, and similar. All that's left is to handle our URL parameter. We will begin by creating a ``get_blog_post`` function. In a real site, this function would call an API endpoint to get a blog post from the database. However, that is out of the scope of this guide so we will be utilizing static text. ```rs @@ -153,7 +153,7 @@ use dioxus::{ ... fn blog_post(cx: Scope) -> Element { - let route = use_route(&cx); // NEW + let route = use_route(cx); // NEW let blog_text = ""; cx.render(rsx! { @@ -165,7 +165,7 @@ Dioxus Router provides built in methods to extract information from a route. We The ``segment`` method also parses the parameter into any type for us. We'll use a match expression that handles a parsing error and on success, uses our helper function to grab the blog post. ```rs fn blog_post(cx: Scope) -> Element { - let route = use_route(&cx); + let route = use_route(cx); // NEW let blog_text = match route.segment::("post").unwrap() { @@ -198,4 +198,4 @@ fn app(cx: Scope) -> Element { That's it! If you head to ``/blog/foo`` you should see ``Welcome to the foo blog post!``. ### Conclusion -In this chapter we utilized Dioxus Router's Link, URL Parameter, and ``use_route`` functionality to build the blog portion of our application. In the next and final chapter, we will go over the ``Redirect`` component to redirect non-authorized users to another page. \ No newline at end of file +In this chapter we utilized Dioxus Router's Link, URL Parameter, and ``use_route`` functionality to build the blog portion of our application. In the next and final chapter, we will go over the ``Redirect`` component to redirect non-authorized users to another page. diff --git a/examples/README.md b/examples/README.md index 732b19ba8..2533722ed 100644 --- a/examples/README.md +++ b/examples/README.md @@ -134,31 +134,33 @@ cargo run --example hello_world ## Terminal UI -[tui_border](./tui_border.rs) +[tui_all_events](../packages/tui/examples/tui_all_events.rs) - All of the terminal events -[tui_buttons](./tui_buttons.rs) +[tui_border](../packages/tui/examples/tui_border.rs) - Different styles of borders and corners -[tui_color_test](./tui_color_test.rs) +[tui_buttons](../packages/tui/examples/tui_buttons.rs) - A grid of buttons that work demonstrate the focus system -[tui_components](./tui_components.rs) +[tui_color_test](../packages/tui/examples/tui_color_test.rs) - Grid of colors to demonstrate compatablility with different terminal color rendering modes -[tui_frame](./tui_frame.rs) +[tui_colorpicker](../packages/tui/examples/tui_colorpicker.rs) - A colorpicker -[tui_hover](./tui_hover.rs) +[tui_components](../packages/tui/examples/tui_components.rs) - Simple component example -[tui_keys](./tui_keys.rs) +[tui_flex](../packages/tui/examples/tui_flex.rs) - Flexbox support -[tui_list](./tui_list.rs) +[tui_hover](../packages/tui/examples/tui_hover.rs) - Detect hover and mouse events -[tui_margin](./tui_margin.rs) +[tui_list](../packages/tui/examples/tui_list.rs) - Renders a list of items -[tui_quadrants](./tui_quadrants.rs) +[tui_margin](../packages/tui/examples/tui_margin.rs) - Margins around flexboxes -[tui_readme](./tui_readme.rs) +[tui_quadrants](../packages/tui/examples/tui_quadrants.rs) - Four quadrants -[tui_task](./tui_task.rs) +[tui_readme](../packages/tui/examples/tui_readme.rs) - The readme example -[tui_text](./tui_text.rs) +[tui_task](../packages/tui/examples/tui_task.rs) - Background tasks + +[tui_text](../packages/tui/examples/tui_text.rs) - Simple text example # TODO Missing Features diff --git a/examples/all_css.rs b/examples/all_css.rs index 768383b78..5e646a284 100644 --- a/examples/all_css.rs +++ b/examples/all_css.rs @@ -7,405 +7,407 @@ fn main() { fn app(cx: Scope) -> Element { cx.render(rsx! { div { - align_content: "a", - align_items: "a", - align_self: "a", - alignment_adjust: "a", - alignment_baseline: "a", - all: "a", - alt: "a", - animation: "a", - animation_delay: "a", - animation_direction: "a", - animation_duration: "a", - animation_fill_mode: "a", - animation_iteration_count: "a", - animation_name: "a", - animation_play_state: "a", - animation_timing_function: "a", - azimuth: "a", - backface_visibility: "a", - background: "a", - background_attachment: "a", - background_clip: "a", - background_color: "a", - background_image: "a", - background_origin: "a", - background_position: "a", - background_repeat: "a", - background_size: "a", - background_blend_mode: "a", - baseline_shift: "a", - bleed: "a", - bookmark_label: "a", - bookmark_level: "a", - bookmark_state: "a", - border: "a", - border_color: "a", - border_style: "a", - border_width: "a", - border_bottom: "a", - border_bottom_color: "a", - border_bottom_style: "a", - border_bottom_width: "a", - border_left: "a", - border_left_color: "a", - border_left_style: "a", - border_left_width: "a", - border_right: "a", - border_right_color: "a", - border_right_style: "a", - border_right_width: "a", - border_top: "a", - border_top_color: "a", - border_top_style: "a", - border_top_width: "a", - border_collapse: "a", - border_image: "a", - border_image_outset: "a", - border_image_repeat: "a", - border_image_slice: "a", - border_image_source: "a", - border_image_width: "a", - border_radius: "a", - border_bottom_left_radius: "a", - border_bottom_right_radius: "a", - border_top_left_radius: "a", - border_top_right_radius: "a", - border_spacing: "a", - bottom: "a", - box_decoration_break: "a", - box_shadow: "a", - box_sizing: "a", - box_snap: "a", - break_after: "a", - break_before: "a", - break_inside: "a", - buffered_rendering: "a", - caption_side: "a", - clear: "a", - clear_side: "a", - clip: "a", - clip_path: "a", - clip_rule: "a", - color: "a", - color_adjust: "a", - color_correction: "a", - color_interpolation: "a", - color_interpolation_filters: "a", - color_profile: "a", - color_rendering: "a", - column_fill: "a", - column_gap: "a", - column_rule: "a", - column_rule_color: "a", - column_rule_style: "a", - column_rule_width: "a", - column_span: "a", - columns: "a", - column_count: "a", - column_width: "a", - contain: "a", - content: "a", - counter_increment: "a", - counter_reset: "a", - counter_set: "a", - cue: "a", - cue_after: "a", - cue_before: "a", - cursor: "a", - direction: "a", - display: "a", - display_inside: "a", - display_outside: "a", - display_extras: "a", - display_box: "a", - dominant_baseline: "a", - elevation: "a", - empty_cells: "a", - enable_background: "a", - fill: "a", - fill_opacity: "a", - fill_rule: "a", - filter: "a", - float: "a", - float_defer_column: "a", - float_defer_page: "a", - float_offset: "a", - float_wrap: "a", - flow_into: "a", - flow_from: "a", - flex: "a", - flex_basis: "a", - flex_grow: "a", - flex_shrink: "a", - flex_flow: "a", - flex_direction: "a", - flex_wrap: "a", - flood_color: "a", - flood_opacity: "a", - font: "a", - font_family: "a", - font_size: "a", - font_stretch: "a", - font_style: "a", - font_weight: "a", - font_feature_settings: "a", - font_kerning: "a", - font_language_override: "a", - font_size_adjust: "a", - font_synthesis: "a", - font_variant: "a", - font_variant_alternates: "a", - font_variant_caps: "a", - font_variant_east_asian: "a", - font_variant_ligatures: "a", - font_variant_numeric: "a", - font_variant_position: "a", - footnote_policy: "a", - glyph_orientation_horizontal: "a", - glyph_orientation_vertical: "a", - grid: "a", - grid_auto_flow: "a", - grid_auto_columns: "a", - grid_auto_rows: "a", - grid_template: "a", - grid_template_areas: "a", - grid_template_columns: "a", - grid_template_rows: "a", - grid_area: "a", - grid_column: "a", - grid_column_start: "a", - grid_column_end: "a", - grid_row: "a", - grid_row_start: "a", - grid_row_end: "a", - hanging_punctuation: "a", - height: "a", - hyphenate_character: "a", - hyphenate_limit_chars: "a", - hyphenate_limit_last: "a", - hyphenate_limit_lines: "a", - hyphenate_limit_zone: "a", - hyphens: "a", - icon: "a", - image_orientation: "a", - image_resolution: "a", - image_rendering: "a", - ime: "a", - ime_align: "a", - ime_mode: "a", - ime_offset: "a", - ime_width: "a", - initial_letters: "a", - inline_box_align: "a", - isolation: "a", - justify_content: "a", - justify_items: "a", - justify_self: "a", - kerning: "a", - left: "a", - letter_spacing: "a", - lighting_color: "a", - line_box_contain: "a", - line_break: "a", - line_grid: "a", - line_height: "a", - line_slack: "a", - line_snap: "a", - list_style: "a", - list_style_image: "a", - list_style_position: "a", - list_style_type: "a", - margin: "a", - margin_bottom: "a", - margin_left: "a", - margin_right: "a", - margin_top: "a", - marker: "a", - marker_end: "a", - marker_mid: "a", - marker_pattern: "a", - marker_segment: "a", - marker_start: "a", - marker_knockout_left: "a", - marker_knockout_right: "a", - marker_side: "a", - marks: "a", - marquee_direction: "a", - marquee_play_count: "a", - marquee_speed: "a", - marquee_style: "a", - mask: "a", - mask_image: "a", - mask_repeat: "a", - mask_position: "a", - mask_clip: "a", - mask_origin: "a", - mask_size: "a", - mask_box: "a", - mask_box_outset: "a", - mask_box_repeat: "a", - mask_box_slice: "a", - mask_box_source: "a", - mask_box_width: "a", - mask_type: "a", - max_height: "a", - max_lines: "a", - max_width: "a", - min_height: "a", - min_width: "a", - mix_blend_mode: "a", - nav_down: "a", - nav_index: "a", - nav_left: "a", - nav_right: "a", - nav_up: "a", - object_fit: "a", - object_position: "a", - offset_after: "a", - offset_before: "a", - offset_end: "a", - offset_start: "a", - opacity: "a", - order: "a", - orphans: "a", - outline: "a", - outline_color: "a", - outline_style: "a", - outline_width: "a", - outline_offset: "a", - overflow: "a", - overflow_x: "a", - overflow_y: "a", - overflow_style: "a", - overflow_wrap: "a", - padding: "a", - padding_bottom: "a", - padding_left: "a", - padding_right: "a", - padding_top: "a", - page: "a", - page_break_after: "a", - page_break_before: "a", - page_break_inside: "a", - paint_order: "a", - pause: "a", - pause_after: "a", - pause_before: "a", - perspective: "a", - perspective_origin: "a", - pitch: "a", - pitch_range: "a", - play_during: "a", - pointer_events: "a", - position: "a", - quotes: "a", - region_fragment: "a", - resize: "a", - rest: "a", - rest_after: "a", - rest_before: "a", - richness: "a", - right: "a", - ruby_align: "a", - ruby_merge: "a", - ruby_position: "a", - scroll_behavior: "a", - scroll_snap_coordinate: "a", - scroll_snap_destination: "a", - scroll_snap_points_x: "a", - scroll_snap_points_y: "a", - scroll_snap_type: "a", - shape_image_threshold: "a", - shape_inside: "a", - shape_margin: "a", - shape_outside: "a", - shape_padding: "a", - shape_rendering: "a", - size: "a", - speak: "a", - speak_as: "a", - speak_header: "a", - speak_numeral: "a", - speak_punctuation: "a", - speech_rate: "a", - stop_color: "a", - stop_opacity: "a", - stress: "a", - string_set: "a", - stroke: "a", - stroke_dasharray: "a", - stroke_dashoffset: "a", - stroke_linecap: "a", - stroke_linejoin: "a", - stroke_miterlimit: "a", - stroke_opacity: "a", - stroke_width: "a", - tab_size: "a", - table_layout: "a", - text_align: "a", - text_align_all: "a", - text_align_last: "a", - text_anchor: "a", - text_combine_upright: "a", - text_decoration: "a", - text_decoration_color: "a", - text_decoration_line: "a", - text_decoration_style: "a", - text_decoration_skip: "a", - text_emphasis: "a", - text_emphasis_color: "a", - text_emphasis_style: "a", - text_emphasis_position: "a", - text_emphasis_skip: "a", - text_height: "a", - text_indent: "a", - text_justify: "a", - text_orientation: "a", - text_overflow: "a", - text_rendering: "a", - text_shadow: "a", - text_size_adjust: "a", - text_space_collapse: "a", - text_spacing: "a", - text_transform: "a", - text_underline_position: "a", - text_wrap: "a", - top: "a", - touch_action: "a", - transform: "a", - transform_box: "a", - transform_origin: "a", - transform_style: "a", - transition: "a", - transition_delay: "a", - transition_duration: "a", - transition_property: "a", - unicode_bidi: "a", - vector_effect: "a", - vertical_align: "a", - visibility: "a", - voice_balance: "a", - voice_duration: "a", - voice_family: "a", - voice_pitch: "a", - voice_range: "a", - voice_rate: "a", - voice_stress: "a", - voice_volumn: "a", - volume: "a", - white_space: "a", - widows: "a", - width: "a", - will_change: "a", - word_break: "a", - word_spacing: "a", - word_wrap: "a", - wrap_flow: "a", - wrap_through: "a", - writing_mode: "a", - z_index: "a", + align_content: "a", + align_items: "a", + align_self: "a", + alignment_adjust: "a", + alignment_baseline: "a", + all: "a", + alt: "a", + animation: "a", + animation_delay: "a", + animation_direction: "a", + animation_duration: "a", + animation_fill_mode: "a", + animation_iteration_count: "a", + animation_name: "a", + animation_play_state: "a", + animation_timing_function: "a", + azimuth: "a", + backface_visibility: "a", + background: "a", + background_attachment: "a", + background_clip: "a", + background_color: "a", + background_image: "a", + background_origin: "a", + background_position: "a", + background_repeat: "a", + background_size: "a", + background_blend_mode: "a", + baseline_shift: "a", + bleed: "a", + bookmark_label: "a", + bookmark_level: "a", + bookmark_state: "a", + border: "a", + border_color: "a", + border_style: "a", + border_width: "a", + border_bottom: "a", + border_bottom_color: "a", + border_bottom_style: "a", + border_bottom_width: "a", + border_left: "a", + border_left_color: "a", + border_left_style: "a", + border_left_width: "a", + border_right: "a", + border_right_color: "a", + border_right_style: "a", + border_right_width: "a", + border_top: "a", + border_top_color: "a", + border_top_style: "a", + border_top_width: "a", + border_collapse: "a", + border_image: "a", + border_image_outset: "a", + border_image_repeat: "a", + border_image_slice: "a", + border_image_source: "a", + border_image_width: "a", + border_radius: "a", + border_bottom_left_radius: "a", + border_bottom_right_radius: "a", + border_top_left_radius: "a", + border_top_right_radius: "a", + border_spacing: "a", + bottom: "a", + box_decoration_break: "a", + box_shadow: "a", + box_sizing: "a", + box_snap: "a", + break_after: "a", + break_before: "a", + break_inside: "a", + buffered_rendering: "a", + caption_side: "a", + clear: "a", + clear_side: "a", + clip: "a", + clip_path: "a", + clip_rule: "a", + color: "a", + color_adjust: "a", + color_correction: "a", + color_interpolation: "a", + color_interpolation_filters: "a", + color_profile: "a", + color_rendering: "a", + column_fill: "a", + column_gap: "a", + column_rule: "a", + column_rule_color: "a", + column_rule_style: "a", + column_rule_width: "a", + column_span: "a", + columns: "a", + column_count: "a", + column_width: "a", + contain: "a", + content: "a", + counter_increment: "a", + counter_reset: "a", + counter_set: "a", + cue: "a", + cue_after: "a", + cue_before: "a", + cursor: "a", + direction: "a", + display: "a", + display_inside: "a", + display_outside: "a", + display_extras: "a", + display_box: "a", + dominant_baseline: "a", + elevation: "a", + empty_cells: "a", + enable_background: "a", + fill: "a", + fill_opacity: "a", + fill_rule: "a", + filter: "a", + float: "a", + float_defer_column: "a", + float_defer_page: "a", + float_offset: "a", + float_wrap: "a", + flow_into: "a", + flow_from: "a", + flex: "a", + flex_basis: "a", + flex_grow: "a", + flex_shrink: "a", + flex_flow: "a", + flex_direction: "a", + flex_wrap: "a", + flood_color: "a", + flood_opacity: "a", + font: "a", + font_family: "a", + font_size: "a", + font_stretch: "a", + font_style: "a", + font_weight: "a", + font_feature_settings: "a", + font_kerning: "a", + font_language_override: "a", + font_size_adjust: "a", + font_synthesis: "a", + font_variant: "a", + font_variant_alternates: "a", + font_variant_caps: "a", + font_variant_east_asian: "a", + font_variant_ligatures: "a", + font_variant_numeric: "a", + font_variant_position: "a", + footnote_policy: "a", + glyph_orientation_horizontal: "a", + glyph_orientation_vertical: "a", + grid: "a", + grid_auto_flow: "a", + grid_auto_columns: "a", + grid_auto_rows: "a", + grid_template: "a", + grid_template_areas: "a", + grid_template_columns: "a", + grid_template_rows: "a", + grid_area: "a", + grid_column: "a", + grid_column_start: "a", + grid_column_end: "a", + grid_row: "a", + grid_row_start: "a", + grid_row_end: "a", + hanging_punctuation: "a", + height: "a", + hyphenate_character: "a", + hyphenate_limit_chars: "a", + hyphenate_limit_last: "a", + hyphenate_limit_lines: "a", + hyphenate_limit_zone: "a", + hyphens: "a", + icon: "a", + image_orientation: "a", + image_resolution: "a", + image_rendering: "a", + ime: "a", + ime_align: "a", + ime_mode: "a", + ime_offset: "a", + ime_width: "a", + initial_letters: "a", + inline_box_align: "a", + isolation: "a", + justify_content: "a", + justify_items: "a", + justify_self: "a", + kerning: "a", + left: "a", + letter_spacing: "a", + lighting_color: "a", + line_box_contain: "a", + line_break: "a", + line_grid: "a", + line_height: "a", + line_slack: "a", + line_snap: "a", + list_style: "a", + list_style_image: "a", + list_style_position: "a", + list_style_type: "a", + margin: "a", + margin_bottom: "a", + margin_left: "a", + margin_right: "a", + margin_top: "a", + marker: "a", + marker_end: "a", + marker_mid: "a", + marker_pattern: "a", + marker_segment: "a", + marker_start: "a", + marker_knockout_left: "a", + marker_knockout_right: "a", + marker_side: "a", + marks: "a", + marquee_direction: "a", + marquee_play_count: "a", + marquee_speed: "a", + marquee_style: "a", + mask: "a", + mask_image: "a", + mask_repeat: "a", + mask_position: "a", + mask_clip: "a", + mask_origin: "a", + mask_size: "a", + mask_box: "a", + mask_box_outset: "a", + mask_box_repeat: "a", + mask_box_slice: "a", + mask_box_source: "a", + mask_box_width: "a", + mask_type: "a", + max_height: "a", + max_lines: "a", + max_width: "a", + min_height: "a", + min_width: "a", + mix_blend_mode: "a", + nav_down: "a", + nav_index: "a", + nav_left: "a", + nav_right: "a", + nav_up: "a", + object_fit: "a", + object_position: "a", + offset_after: "a", + offset_before: "a", + offset_end: "a", + offset_start: "a", + opacity: "a", + order: "a", + orphans: "a", + outline: "a", + outline_color: "a", + outline_style: "a", + outline_width: "a", + outline_offset: "a", + overflow: "a", + overflow_x: "a", + overflow_y: "a", + overflow_style: "a", + overflow_wrap: "a", + padding: "a", + padding_bottom: "a", + padding_left: "a", + padding_right: "a", + padding_top: "a", + page: "a", + page_break_after: "a", + page_break_before: "a", + page_break_inside: "a", + paint_order: "a", + pause: "a", + pause_after: "a", + pause_before: "a", + perspective: "a", + perspective_origin: "a", + pitch: "a", + pitch_range: "a", + play_during: "a", + pointer_events: "a", + position: "a", + quotes: "a", + region_fragment: "a", + resize: "a", + rest: "a", + rest_after: "a", + rest_before: "a", + richness: "a", + right: "a", + ruby_align: "a", + ruby_merge: "a", + ruby_position: "a", + scroll_behavior: "a", + scroll_snap_coordinate: "a", + scroll_snap_destination: "a", + scroll_snap_points_x: "a", + scroll_snap_points_y: "a", + scroll_snap_type: "a", + shape_image_threshold: "a", + shape_inside: "a", + shape_margin: "a", + shape_outside: "a", + shape_padding: "a", + shape_rendering: "a", + size: "a", + speak: "a", + speak_as: "a", + speak_header: "a", + speak_numeral: "a", + speak_punctuation: "a", + speech_rate: "a", + stop_color: "a", + stop_opacity: "a", + stress: "a", + string_set: "a", + stroke: "a", + stroke_dasharray: "a", + stroke_dashoffset: "a", + stroke_linecap: "a", + stroke_linejoin: "a", + stroke_miterlimit: "a", + stroke_opacity: "a", + stroke_width: "a", + tab_size: "a", + table_layout: "a", + text_align: "a", + text_align_all: "a", + text_align_last: "a", + text_anchor: "a", + text_combine_upright: "a", + text_decoration: "a", + text_decoration_color: "a", + text_decoration_line: "a", + text_decoration_style: "a", + text_decoration_skip: "a", + text_emphasis: "a", + text_emphasis_color: "a", + text_emphasis_style: "a", + text_emphasis_position: "a", + text_emphasis_skip: "a", + text_height: "a", + text_indent: "a", + text_justify: "a", + text_orientation: "a", + text_overflow: "a", + text_rendering: "a", + text_shadow: "a", + text_size_adjust: "a", + text_space_collapse: "a", + text_spacing: "a", + text_transform: "a", + text_underline_position: "a", + text_wrap: "a", + top: "a", + touch_action: "a", + transform: "a", + transform_box: "a", + transform_origin: "a", + transform_style: "a", + transition: "a", + transition_delay: "a", + transition_duration: "a", + transition_property: "a", + unicode_bidi: "a", + vector_effect: "a", + vertical_align: "a", + visibility: "a", + voice_balance: "a", + voice_duration: "a", + voice_family: "a", + voice_pitch: "a", + voice_range: "a", + voice_rate: "a", + voice_stress: "a", + voice_volumn: "a", + volume: "a", + white_space: "a", + widows: "a", + width: "a", + will_change: "a", + word_break: "a", + word_spacing: "a", + word_wrap: "a", + wrap_flow: "a", + wrap_through: "a", + writing_mode: "a", + z_index: "a", + + "This example isn't quite useful yet" } }) } diff --git a/examples/all_events.rs b/examples/all_events.rs index d2c2ef0d7..594a736d5 100644 --- a/examples/all_events.rs +++ b/examples/all_events.rs @@ -1,4 +1,4 @@ -use dioxus::{events::*, prelude::*}; +use dioxus::{events::*, html::MouseEvent, prelude::*}; fn main() { dioxus_desktop::launch(app); @@ -41,7 +41,7 @@ const RECT_STYLE: &str = r#" "#; fn app(cx: Scope) -> Element { - let events = use_ref(&cx, std::collections::VecDeque::new); + let events = use_ref(cx, std::collections::VecDeque::new); let log_event = move |event: Event| { let mut events = events.write(); diff --git a/examples/borrowed.rs b/examples/borrowed.rs index 7de2ff80d..95641bbbc 100644 --- a/examples/borrowed.rs +++ b/examples/borrowed.rs @@ -29,9 +29,7 @@ fn app(cx: Scope) -> Element { cx.render(rsx! { div { - Child1 { - text: first - } + Child1 { text: first } } }) } @@ -59,9 +57,7 @@ struct C2Props<'a> { fn Child2<'a>(cx: Scope<'a, C2Props<'a>>) -> Element { cx.render(rsx! { - Child3 { - text: cx.props.text - } + Child3 { text: cx.props.text } }) } diff --git a/examples/calculator.rs b/examples/calculator.rs index 4f5d7dedd..bf183ba67 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -19,12 +19,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let val = use_state(&cx, || String::from("0")); + let val = use_state(cx, || String::from("0")); let input_digit = move |num: u8| { if val.get() == "0" { val.set(String::new()); } + val.make_mut().push_str(num.to_string().as_str()); }; @@ -99,12 +100,8 @@ fn app(cx: Scope) -> Element { } } div { class: "digit-keys", - button { class: "calculator-key key-0", onclick: move |_| input_digit(0), - "0" - } - button { class: "calculator-key key-dot", onclick: move |_| val.make_mut().push('.'), - "●" - } + button { class: "calculator-key key-0", onclick: move |_| input_digit(0), "0" } + button { class: "calculator-key key-dot", onclick: move |_| val.make_mut().push('.'), "●" } (1..10).map(|k| rsx!{ button { class: "calculator-key {k}", @@ -116,22 +113,13 @@ fn app(cx: Scope) -> Element { } } div { class: "operator-keys", - button { class: "calculator-key key-divide", onclick: move |_| input_operator("/"), - "÷" - } - button { class: "calculator-key key-multiply", onclick: move |_| input_operator("*"), - "×" - } - button { class: "calculator-key key-subtract", onclick: move |_| input_operator("-"), - "−" - } - button { class: "calculator-key key-add", onclick: move |_| input_operator("+"), - "+" - } - button { class: "calculator-key key-equals", - onclick: move |_| { - val.set(format!("{}", calc_val(val.as_str()))); - }, + button { class: "calculator-key key-divide", onclick: move |_| input_operator("/"), "÷" } + button { class: "calculator-key key-multiply", onclick: move |_| input_operator("*"), "×" } + button { class: "calculator-key key-subtract", onclick: move |_| input_operator("-"), "−" } + button { class: "calculator-key key-add", onclick: move |_| input_operator("+"), "+" } + button { + class: "calculator-key key-equals", + onclick: move |_| val.set(format!("{}", calc_val(val.as_str()))), "=" } } diff --git a/examples/callback.rs b/examples/callback.rs new file mode 100644 index 000000000..bf2e3e3a0 --- /dev/null +++ b/examples/callback.rs @@ -0,0 +1,22 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let login = use_callback!(cx, move |_| async move { + let res = reqwest::get("https://dog.ceo/api/breeds/list/all") + .await + .unwrap() + .text() + .await + .unwrap(); + + println!("{:#?}, ", res); + }); + + cx.render(rsx! { + button { onclick: login, "Click me!" } + }) +} diff --git a/examples/crm.rs b/examples/crm.rs index 1943ed97c..e99cfe241 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -16,10 +16,10 @@ pub struct Client { } fn app(cx: Scope) -> Element { - let clients = use_ref(&cx, || vec![] as Vec); - let firstname = use_state(&cx, String::new); - let lastname = use_state(&cx, String::new); - let description = use_state(&cx, String::new); + let clients = use_ref(cx, || vec![] as Vec); + let firstname = use_state(cx, String::new); + let lastname = use_state(cx, String::new); + let description = use_state(cx, String::new); cx.render(rsx!( body { @@ -30,7 +30,7 @@ fn app(cx: Scope) -> Element { integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5", crossorigin: "anonymous", } - h1 {"Dioxus CRM Example"} + h1 { "Dioxus CRM Example" } Router { Route { to: "/", div { class: "crm", @@ -40,12 +40,12 @@ fn app(cx: Scope) -> Element { div { class: "client", style: "margin-bottom: 50px", p { "First Name: {client.first_name}" } p { "Last Name: {client.last_name}" } - p {"Description: {client.description}"} + p { "Description: {client.description}" } }) ) } Link { to: "/new", class: "pure-button pure-button-primary", "Add New" } - Link { to: "/new", class: "pure-button", "Settings" } + Link { to: "/settings", class: "pure-button", "Settings" } } } Route { to: "/new", diff --git a/examples/custom_element.rs b/examples/custom_element.rs index 1c870e157..6ed2e64cd 100644 --- a/examples/custom_element.rs +++ b/examples/custom_element.rs @@ -10,21 +10,31 @@ fn main() { let mut dom = VirtualDom::new(app); let _ = dom.rebuild(); - let output = dioxus_ssr::render_vdom(&dom); + let output = dioxus_ssr::render(&dom); println!("{}", output); } fn app(cx: Scope) -> Element { - let nf = NodeFactory::new(&cx); + let g = cx.component(component, (), "component"); + let c = cx.make_node(g); + cx.render(rsx! { + div { c } + }) - let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump()); + // let nf = NodeFactory::new(cx); - attrs.push(nf.attr("client-id", format_args!("abc123"), None, false)); + // let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump()); - attrs.push(nf.attr("name", format_args!("bob"), None, false)); + // attrs.push(nf.attr("client-id", format_args!("abc123"), None, false)); - attrs.push(nf.attr("age", format_args!("47"), None, false)); + // attrs.push(nf.attr("name", format_args!("bob"), None, false)); - Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None)) + // 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/disabled.rs b/examples/disabled.rs index f89aafe56..5be75b8b4 100644 --- a/examples/disabled.rs +++ b/examples/disabled.rs @@ -5,7 +5,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let disabled = use_state(&cx, || false); + let disabled = use_state(cx, || false); cx.render(rsx! { div { diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 5c78ed0bf..363d85f4b 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -1,12 +1,8 @@ -#![allow(non_snake_case)] - -//! Render a bunch of doggos! - use dioxus::prelude::*; use std::collections::HashMap; fn main() { - dioxus_desktop::launch(app); + dioxus_desktop::launch(|cx| render!(app_root {})); } #[derive(Debug, Clone, PartialEq, serde::Deserialize)] @@ -14,10 +10,10 @@ struct ListBreeds { message: HashMap>, } -fn app(cx: Scope) -> Element { - let breed = use_state(&cx, || None); +async fn app_root(cx: Scope<'_>) -> Element { + let breed = use_state(cx, || "deerhound".to_string()); - let breeds = use_future(&cx, (), |_| async move { + let breeds = use_future!(cx, || async move { reqwest::get("https://dog.ceo/api/breeds/list/all") .await .unwrap() @@ -25,32 +21,26 @@ fn app(cx: Scope) -> Element { .await }); - match breeds.value() { - Some(Ok(breeds)) => cx.render(rsx! { - div { + match breeds.await { + Ok(breeds) => cx.render(rsx! { + div { height: "500px", h1 { "Select a dog breed!" } div { display: "flex", ul { flex: "50%", - breeds.message.keys().map(|cur_breed| rsx!( - li { + for cur_breed in breeds.message.keys().take(10) { + li { key: "{cur_breed}", button { - onclick: move |_| breed.set(Some(cur_breed.clone())), + onclick: move |_| breed.set(cur_breed.clone()), "{cur_breed}" } } - )) - } - div { flex: "50%", - match breed.get() { - Some(breed) => rsx!( Breed { breed: breed.clone() } ), - None => rsx!("No Breed selected"), } } + div { flex: "50%", breed_pic { breed: breed.to_string() } } } } }), - Some(Err(_e)) => cx.render(rsx! { div { "Error fetching breeds" } }), - None => cx.render(rsx! { div { "Loading dogs..." } }), + Err(_e) => cx.render(rsx! { div { "Error fetching breeds" } }), } } @@ -60,8 +50,8 @@ struct DogApi { } #[inline_props] -fn Breed(cx: Scope, breed: String) -> Element { - let fut = use_future(&cx, (breed,), |(breed,)| async move { +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)) .await .unwrap() @@ -69,21 +59,23 @@ fn Breed(cx: Scope, breed: String) -> Element { .await }); - cx.render(match fut.value() { - Some(Ok(resp)) => rsx! { - button { - onclick: move |_| fut.restart(), - "Click to fetch another doggo" - } + match fut.await { + Ok(resp) => render! { div { + button { + onclick: move |_| { + println!("clicked"); + fut.restart() + }, + "Click to fetch another doggo" + } img { + src: "{resp.message}", max_width: "500px", max_height: "500px", - src: "{resp.message}", } } }, - Some(Err(_)) => rsx! { div { "loading dogs failed" } }, - None => rsx! { div { "loading dogs..." } }, - }) + Err(_) => render! { div { "loading dogs failed" } }, + } } diff --git a/examples/error_handle.rs b/examples/error_handle.rs index 1d5d82b6d..b4a6dbb05 100644 --- a/examples/error_handle.rs +++ b/examples/error_handle.rs @@ -5,7 +5,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let val = use_state(&cx, || "0.0001"); + let val = use_state(cx, || "0.0001"); let num = match val.parse::() { Err(_) => return cx.render(rsx!("Parsing failed")), @@ -18,5 +18,17 @@ fn app(cx: Scope) -> Element { onclick: move |_| val.set("invalid"), "Set an invalid number" } + (0..5).map(|i| rsx! { + demo_c { x: i } + }) + }) +} + +#[inline_props] +fn demo_c(cx: Scope, x: i32) -> Element { + cx.render(rsx! { + h1 { + "asdasdasdasd {x}" + } }) } diff --git a/examples/eval.rs b/examples/eval.rs index 495288fcb..22e036b9c 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -1,12 +1,22 @@ 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 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) + } + }); + } cx.render(rsx! { div { @@ -16,7 +26,10 @@ fn app(cx: Scope) -> Element { oninput: move |e| script.set(e.value.clone()), } button { - onclick: move |_| eval(script), + onclick: move |_| { + let fut = eval(script); + future.set(Some(fut)); + }, "Execute" } } diff --git a/examples/fermi.rs b/examples/fermi.rs index 06bc38fc5..babe3e024 100644 --- a/examples/fermi.rs +++ b/examples/fermi.rs @@ -10,17 +10,17 @@ fn main() { static NAME: Atom = |_| "world".to_string(); fn app(cx: Scope) -> Element { - let name = use_read(&cx, NAME); + let name = use_read(cx, NAME); cx.render(rsx! { div { "hello {name}!" } Child {} - ChildWithRef{} + ChildWithRef {} }) } fn Child(cx: Scope) -> Element { - let set_name = use_set(&cx, NAME); + let set_name = use_set(cx, NAME); cx.render(rsx! { button { @@ -33,7 +33,7 @@ fn Child(cx: Scope) -> Element { static NAMES: AtomRef> = |_| vec!["world".to_string()]; fn ChildWithRef(cx: Scope) -> Element { - let names = use_atom_ref(&cx, NAMES); + let names = use_atom_ref(cx, NAMES); cx.render(rsx! { div { diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index e0acb4ae6..64173ad11 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -19,9 +19,9 @@ fn main() { } fn app(cx: Scope) -> Element { - let files = use_ref(&cx, Files::new); + let files = use_ref(cx, Files::new); - render! { + cx.render(rsx! { div { link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", } style { include_str!("./assets/fileexplorer.css") } @@ -62,7 +62,7 @@ fn app(cx: Scope) -> Element { }) } } - } + }) } struct Files { diff --git a/examples/flat_router.rs b/examples/flat_router.rs index 752b58760..4de9e7a7d 100644 --- a/examples/flat_router.rs +++ b/examples/flat_router.rs @@ -7,7 +7,6 @@ fn main() { let cfg = Config::new().with_window( WindowBuilder::new() - .with_title("Spinsense Client") .with_inner_size(LogicalSize::new(600, 1000)) .with_resizable(false), ); @@ -17,21 +16,21 @@ fn main() { fn app(cx: Scope) -> Element { cx.render(rsx! { - Router { - Route { to: "/", "Home" } - Route { to: "/games", "Games" } - Route { to: "/play", "Play" } - Route { to: "/settings", "Settings" } + div { + Router { + Route { to: "/", "Home" } + Route { to: "/games", "Games" } + Route { to: "/play", "Play" } + Route { to: "/settings", "Settings" } - p { - "----" - } - nav { - ul { - Link { to: "/", li { "Home" } } - Link { to: "/games", li { "Games" } } - Link { to: "/play", li { "Play" } } - Link { to: "/settings", li { "Settings" } } + p { "----" } + nav { + ul { + Link { to: "/", li { "Home" } } + Link { to: "/games", li { "Games" } } + Link { to: "/play", li { "Play" } } + Link { to: "/settings", li { "Settings" } } + } } } } diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index a88946af4..7e7a320a9 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -32,8 +32,8 @@ impl Label { } fn app(cx: Scope) -> Element { - let items = use_ref(&cx, Vec::new); - let selected = use_state(&cx, || None); + let items = use_ref(cx, Vec::new); + let selected = use_state(cx, || None); cx.render(rsx! { div { class: "container", @@ -72,7 +72,7 @@ fn app(cx: Scope) -> Element { td { class:"col-md-1" } td { class:"col-md-1", "{item.key}" } td { class:"col-md-1", onclick: move |_| selected.set(Some(id)), - a { class: "lbl", item.labels } + a { class: "lbl", "{item.labels[0]}{item.labels[1]}{item.labels[2]}" } } td { class: "col-md-1", a { class: "remove", onclick: move |_| { items.write().remove(id); }, diff --git a/examples/generic_component.rs b/examples/generic_component.rs new file mode 100644 index 000000000..9757b1032 --- /dev/null +++ b/examples/generic_component.rs @@ -0,0 +1,13 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { generic_child::{} }) +} + +fn generic_child(cx: Scope) -> Element { + cx.render(rsx! { div {} }) +} diff --git a/examples/hydration.rs b/examples/hydration.rs index 692944f6b..ed43a9f70 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -14,13 +14,13 @@ use dioxus_desktop::Config; fn main() { let vdom = VirtualDom::new(app); - let content = dioxus_ssr::render_vdom_cfg(&vdom, |f| f.pre_render(true)); + let content = dioxus_ssr::pre_render(&vdom); dioxus_desktop::launch_cfg(app, Config::new().with_prerendered(content)); } fn app(cx: Scope) -> Element { - let val = use_state(&cx, || 0); + let val = use_state(cx, || 0); cx.render(rsx! { div { diff --git a/examples/inlineprops.rs b/examples/inlineprops.rs index 786714a2e..8c626d346 100644 --- a/examples/inlineprops.rs +++ b/examples/inlineprops.rs @@ -28,7 +28,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let state = use_state(&cx, || 1); + let state = use_state(cx, || 1); cx.render(rsx! { div { diff --git a/examples/inputs.rs b/examples/inputs.rs index 9b6ec104f..223e3a7e7 100644 --- a/examples/inputs.rs +++ b/examples/inputs.rs @@ -2,7 +2,7 @@ //! //! There is some conversion happening when input types are checkbox/radio/select/textarea etc. -use dioxus::{events::FormEvent, prelude::*}; +use dioxus::prelude::*; fn main() { dioxus_desktop::launch(app); @@ -28,7 +28,6 @@ const FIELDS: &[(&str, &str)] = &[ ("text", ""), ("time", ""), ("url", ""), - // // less supported things ("hidden", ""), ("month", ""), // degrades to text most of the time, but works properly as "value'" @@ -114,7 +113,7 @@ fn app(cx: Scope) -> Element { } } - FIELDS.iter().map(|(field, value)| rsx!( + FIELDS.iter().map(|(field, value)| rsx! { div { input { id: "{field}", @@ -131,8 +130,7 @@ fn app(cx: Scope) -> Element { } br {} } - )) - + }) } }) } diff --git a/examples/login_form.rs b/examples/login_form.rs index a0bc85a17..74e634d5d 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -1,7 +1,6 @@ //! This example demonstrates the following: //! Futures in a callback, Router, and Forms -use dioxus::events::*; use dioxus::prelude::*; fn main() { @@ -37,11 +36,10 @@ fn app(cx: Scope) -> Element { form { onsubmit: onsubmit, prevent_default: "onsubmit", // Prevent the default behavior of
to post - - input { "type": "text", id: "username", name: "username" } + input { r#type: "text", id: "username", name: "username" } label { "Username" } br {} - input { "type": "password", id: "password", name: "password" } + input { r#type: "password", id: "password", name: "password" } label { "Password" } br {} button { "Login" } diff --git a/examples/nested_listeners.rs b/examples/nested_listeners.rs index 163a827a2..3938959a7 100644 --- a/examples/nested_listeners.rs +++ b/examples/nested_listeners.rs @@ -22,7 +22,7 @@ fn app(cx: Scope) -> Element { button { onclick: move |evt| { println!("clicked! bottom no bubbling"); - evt.cancel_bubble(); + evt.stop_propogation(); }, "Dont propogate" } diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index 7feff27b6..a6c35d17a 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -19,6 +19,7 @@ use dioxus::events::*; use dioxus::html::input_data::keyboard_types::Key; +use dioxus::html::MouseEvent; use dioxus::prelude::*; use dioxus_desktop::wry::application::dpi::LogicalSize; use dioxus_desktop::{Config, WindowBuilder}; @@ -35,7 +36,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let state = use_ref(&cx, Calculator::new); + let state = use_ref(cx, Calculator::new); cx.render(rsx! { style { include_str!("./assets/calculator.css") } diff --git a/examples/pattern_reducer.rs b/examples/pattern_reducer.rs index 6778324a4..6f7e16f6e 100644 --- a/examples/pattern_reducer.rs +++ b/examples/pattern_reducer.rs @@ -15,7 +15,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let state = use_state(&cx, PlayerState::new); + let state = use_state(cx, PlayerState::new); cx.render(rsx!( div { diff --git a/examples/readme.rs b/examples/readme.rs index 59816357a..f4668488b 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -9,7 +9,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let mut count = use_state(cx, || 0); cx.render(rsx! { h1 { "High-Five counter: {count}" } diff --git a/examples/router.rs b/examples/router.rs index 20b168670..5766b8e1c 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -30,7 +30,7 @@ fn app(cx: Scope) -> Element { } fn BlogPost(cx: Scope) -> Element { - let post = dioxus_router::use_route(&cx).last_segment()?; + let post = dioxus_router::use_route(cx).last_segment().unwrap(); cx.render(rsx! { div { @@ -46,9 +46,9 @@ struct Query { } fn User(cx: Scope) -> Element { - let post = dioxus_router::use_route(&cx).last_segment()?; + let post = dioxus_router::use_route(cx).last_segment().unwrap(); - let query = dioxus_router::use_route(&cx) + let query = dioxus_router::use_route(cx) .query::() .unwrap_or(Query { bold: false }); diff --git a/examples/rsx_compile_fail.rs b/examples/rsx_compile_fail.rs index 2859dc53e..40bad709b 100644 --- a/examples/rsx_compile_fail.rs +++ b/examples/rsx_compile_fail.rs @@ -5,21 +5,22 @@ use dioxus::prelude::*; fn main() { let mut vdom = VirtualDom::new(example); - vdom.rebuild(); + _ = vdom.rebuild(); - let out = dioxus_ssr::render_vdom_cfg(&vdom, |c| c.newline(true).indent(true)); - println!("{}", out); + let mut renderer = dioxus_ssr::Renderer::new(); + renderer.pretty = true; + renderer.render(&vdom); } fn example(cx: Scope) -> Element { - let items = use_state(&cx, || { + let items = use_state(cx, || { vec![Thing { a: "asd".to_string(), b: 10, }] }); - let things = use_ref(&cx, || { + let things = use_ref(cx, || { vec![Thing { a: "asd".to_string(), b: 10, @@ -27,7 +28,7 @@ fn example(cx: Scope) -> Element { }); let things_list = things.read(); - let mything = use_ref(&cx, || Some(String::from("asd"))); + let mything = use_ref(cx, || Some(String::from("asd"))); let mything_read = mything.read(); cx.render(rsx!( diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index a610fe334..23ab19cf9 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -1,5 +1,4 @@ #![allow(non_snake_case)] - //! A tour of the rsx! macro //! ------------------------ //! @@ -39,7 +38,7 @@ //! - Accept a list of vnodes as children for a Fragment component //! - Allow keyed fragments in iterators //! - Allow top-level fragments -//! + fn main() { dioxus_desktop::launch(app); } @@ -165,13 +164,13 @@ fn app(cx: Scope) -> Element { // Can pass in props directly as an expression { - let props = TallerProps {a: "hello", children: Default::default()}; + let props = TallerProps {a: "hello", children: cx.render(rsx!(()))}; rsx!(Taller { ..props }) } // Spreading can also be overridden manually Taller { - ..TallerProps { a: "ballin!", children: Default::default() }, + ..TallerProps { a: "ballin!", children: cx.render(rsx!(()) )}, a: "not ballin!" } @@ -183,7 +182,7 @@ fn app(cx: Scope) -> Element { // Components can be generic too // This component takes i32 type to give you typed input - TypedInput::> {} + TypedInput:: {} // Type inference can be used too TypedInput { initial: 10.0 } @@ -200,7 +199,7 @@ fn app(cx: Scope) -> Element { // helper functions // Anything that implements IntoVnode can be dropped directly into Rsx - helper(&cx, "hello world!") + helper(cx, "hello world!") // Strings can be supplied directly String::from("Hello world!") diff --git a/examples/simple_desktop.rs b/examples/simple_desktop.rs new file mode 100644 index 000000000..d8c00afac --- /dev/null +++ b/examples/simple_desktop.rs @@ -0,0 +1,60 @@ +use dioxus::prelude::*; +use dioxus_router::*; + +fn main() { + simple_logger::SimpleLogger::new() + .with_level(log::LevelFilter::Debug) + .with_module_level("dioxus_router", log::LevelFilter::Trace) + .with_module_level("dioxus", log::LevelFilter::Trace) + .init() + .unwrap(); + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + Router { + h1 { "Your app here" } + ul { + Link { to: "/", li { "home" } } + Link { to: "/blog", li { "blog" } } + Link { to: "/blog/tim", li { "tims' blog" } } + Link { to: "/blog/bill", li { "bills' blog" } } + Link { to: "/blog/james", + li { "james amazing' blog" } + } + Link { to: "/apples", li { "go to apples" } } + } + Route { to: "/", Home {} } + Route { to: "/blog/", BlogList {} } + Route { to: "/blog/:id/", BlogPost {} } + Route { to: "/oranges", "Oranges are not apples!" } + Redirect { from: "/apples", to: "/oranges" } + } + }) +} + +fn Home(cx: Scope) -> Element { + log::debug!("rendering home {:?}", cx.scope_id()); + cx.render(rsx! { h1 { "Home" } }) +} + +fn BlogList(cx: Scope) -> Element { + log::debug!("rendering blog list {:?}", cx.scope_id()); + cx.render(rsx! { div { "Blog List" } }) +} + +fn BlogPost(cx: Scope) -> Element { + let Some(id) = use_route(cx).segment("id") else { + return cx.render(rsx! { div { "No blog post id" } }) + }; + + log::debug!("rendering blog post {}", id); + + cx.render(rsx! { + div { + h3 { "blog post: {id:?}" } + Link { to: "/blog/", "back to blog list" } + } + }) +} diff --git a/examples/simple_list.rs b/examples/simple_list.rs index d615e2371..84cd9db71 100644 --- a/examples/simple_list.rs +++ b/examples/simple_list.rs @@ -6,14 +6,29 @@ fn main() { fn app(cx: Scope) -> Element { cx.render(rsx!( - // Use Map directly to lazily pull elements - (0..10).map(|f| rsx! { "{f}" }), - // Collect into an intermediate collection if necessary - ["a", "b", "c"] - .into_iter() - .map(|f| rsx! { "{f}" }) - .collect::>(), - // Use optionals - Some(rsx! { "Some" }), + div { + // Use Map directly to lazily pull elements + (0..10).map(|f| rsx! { "{f}" }), + + // Collect into an intermediate collection if necessary, and call into_iter + ["a", "b", "c", "d", "e", "f"] + .into_iter() + .map(|f| rsx! { "{f}" }) + .collect::>() + .into_iter(), + + // Use optionals + Some(rsx! { "Some" }), + + // use a for loop where the body itself is RSX + for name in 0..10 { + div {"{name}"} + } + + // Or even use an unterminated conditional + if true { + rsx!{ "hello world!" } + } + } )) } diff --git a/examples/ssr.rs b/examples/ssr.rs index 7d3e5b615..bac3759dd 100644 --- a/examples/ssr.rs +++ b/examples/ssr.rs @@ -2,15 +2,13 @@ //! //! This example shows how we can render the Dioxus Virtualdom using SSR. -use std::fmt::Write; - use dioxus::prelude::*; fn main() { // We can render VirtualDoms let mut vdom = VirtualDom::new(app); let _ = vdom.rebuild(); - println!("{}", dioxus_ssr::render_vdom(&vdom)); + println!("{}", dioxus_ssr::render(&vdom)); // Or we can render rsx! calls themselves println!( @@ -23,17 +21,12 @@ fn main() { ); // We can configure the SSR rendering to add ids for rehydration - println!( - "{}", - dioxus_ssr::render_vdom_cfg(&vdom, |c| c.pre_render(true)) - ); + println!("{}", dioxus_ssr::pre_render(&vdom)); - // We can even render as a writer + // We can render to a buf directly too let mut file = String::new(); - let _ = file.write_fmt(format_args!( - "{}", - dioxus_ssr::TextRenderer::from_vdom(&vdom, Default::default()) - )); + let mut renderer = dioxus_ssr::Renderer::default(); + renderer.render_to(&mut file, &vdom).unwrap(); println!("{}", file); } diff --git a/examples/suspense.rs b/examples/suspense.rs index c1a0e4c03..dbdad2eba 100644 --- a/examples/suspense.rs +++ b/examples/suspense.rs @@ -53,7 +53,7 @@ fn app(cx: Scope) -> Element { /// Suspense is achieved my moving the future into only the component that /// actually renders the data. fn Doggo(cx: Scope) -> Element { - let fut = use_future(&cx, (), |_| async move { + let fut = use_future(cx, (), |_| async move { reqwest::get("https://dog.ceo/api/breeds/image/random/") .await .unwrap() diff --git a/examples/svg.rs b/examples/svg.rs index b331e7a6a..829a559ad 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -1,15 +1,15 @@ // Thanks to @japsu and their project https://github.com/japsu/jatsi for the example! -use dioxus::{events::MouseEvent, prelude::*}; +use dioxus::prelude::*; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - let val = use_state(&cx, || 5); + let val = use_state(cx, || 5); - render! { + cx.render(rsx! { div { user_select: "none", webkit_user_select: "none", @@ -31,7 +31,7 @@ fn app(cx: Scope) -> Element { } } } - } + }) } #[derive(Props)] @@ -70,19 +70,21 @@ pub fn Die<'a>(cx: Scope<'a, DieProps<'a>>) -> Element { .map(|((x, y), _)| { let dcx = x * OFFSET; let dcy = y * OFFSET; - rsx!(circle { - cx: "{dcx}", - cy: "{dcy}", - r: "{DOT_RADIUS}", - fill: "#333" - }) + + rsx! { + circle { + cx: "{dcx}", + cy: "{dcy}", + r: "{DOT_RADIUS}", + fill: "#333" + } + } }); - render! { + cx.render(rsx! { svg { onclick: move |e| cx.props.onclick.call(e), prevent_default: "onclick", - "dioxus-prevent-default": "onclick", class: "die", view_box: "-1000 -1000 2000 2000", @@ -97,5 +99,5 @@ pub fn Die<'a>(cx: Scope<'a, DieProps<'a>>) -> Element { dots } - } + }) } diff --git a/examples/svg_basic.rs b/examples/svg_basic.rs index e950eb339..1264aa001 100644 --- a/examples/svg_basic.rs +++ b/examples/svg_basic.rs @@ -67,6 +67,12 @@ fn app(cx: Scope) -> Element { stroke: "blue", stroke_width: "5", } + path { + d: "M9.00001 9C9 62 103.5 124 103.5 178", + stroke: "#3CC4DC", + "stroke-linecap": "square", + "stroke-width": "square", + } })) } diff --git a/examples/tasks.rs b/examples/tasks.rs index 7c192a593..874666c22 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -10,9 +10,9 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let count = use_state(cx, || 0); - use_future(&cx, (), move |_| { + use_future(cx, (), move |_| { let mut count = count.clone(); async move { loop { diff --git a/examples/textarea.rs b/examples/textarea.rs index 26283b23d..b27a9c99a 100644 --- a/examples/textarea.rs +++ b/examples/textarea.rs @@ -7,7 +7,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let model = use_state(&cx, || String::from("asd")); + let model = use_state(cx, || String::from("asd")); println!("{}", model); diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 71ceee8e1..b5277133e 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -22,10 +22,10 @@ pub struct TodoItem { } pub fn app(cx: Scope<()>) -> Element { - let todos = use_state(&cx, im_rc::HashMap::::default); - let filter = use_state(&cx, || FilterState::All); - let draft = use_state(&cx, || "".to_string()); - let todo_id = use_state(&cx, || 0); + let todos = use_state(cx, im_rc::HashMap::::default); + let filter = use_state(cx, || FilterState::All); + let draft = use_state(cx, || "".to_string()); + let todo_id = use_state(cx, || 0); // Filter the todos based on the filter state let mut filtered_todos = todos @@ -57,7 +57,9 @@ pub fn app(cx: Scope<()>) -> Element { placeholder: "What needs to be done?", value: "{draft}", autofocus: "true", - oninput: move |evt| draft.set(evt.value.clone()), + oninput: move |evt| { + draft.set(evt.value.clone()); + }, onkeydown: move |evt| { if evt.key() == Key::Enter && !draft.is_empty() { todos.make_mut().insert( @@ -114,7 +116,7 @@ pub struct TodoEntryProps<'a> { } pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { - let is_editing = use_state(&cx, || false); + let is_editing = use_state(cx, || false); let todos = cx.props.todos.get(); let todo = &todos[&cx.props.id]; diff --git a/examples/window_event.rs b/examples/window_event.rs index 8805bcfdd..44ddebeca 100644 --- a/examples/window_event.rs +++ b/examples/window_event.rs @@ -12,15 +12,15 @@ fn main() { } fn app(cx: Scope) -> Element { - let window = dioxus_desktop::use_window(&cx); + let window = dioxus_desktop::use_window(cx); // if you want to make window fullscreen, you need close the resizable. // window.set_fullscreen(true); // window.set_resizable(false); - let fullscreen = use_state(&cx, || false); - let always_on_top = use_state(&cx, || false); - let decorations = use_state(&cx, || false); + let fullscreen = use_state(cx, || false); + let always_on_top = use_state(cx, || false); + let decorations = use_state(cx, || false); cx.render(rsx!( link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } @@ -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.cancel_bubble(), + onmousedown: |evt| evt.stop_propogation(), 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.cancel_bubble(), + onmousedown: |evt| evt.stop_propogation(), 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.cancel_bubble(), + onmousedown: |evt| evt.stop_propogation(), 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.cancel_bubble(), + onmousedown: |evt| evt.stop_propogation(), 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.cancel_bubble(), + onmousedown: |evt| evt.stop_propogation(), 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.cancel_bubble(), + onmousedown: |evt| evt.stop_propogation(), onclick: move |_| window.set_title("Dioxus Application"), "Change Title" } diff --git a/examples/window_zoom.rs b/examples/window_zoom.rs index d312b0af8..b7c190896 100644 --- a/examples/window_zoom.rs +++ b/examples/window_zoom.rs @@ -6,9 +6,9 @@ fn main() { } fn app(cx: Scope) -> Element { - let window = use_window(&cx); + let window = use_window(cx); - let level = use_state(&cx, || 1.0); + let level = use_state(cx, || 1.0); cx.render(rsx! { input { r#type: "number", diff --git a/examples/xss_safety.rs b/examples/xss_safety.rs index 2d5eba20c..5272d661b 100644 --- a/examples/xss_safety.rs +++ b/examples/xss_safety.rs @@ -9,7 +9,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let contents = use_state(&cx, || { + let contents = use_state(cx, || { String::from("") }); diff --git a/notes/README.md b/notes/README.md index a182191ae..2e5a03b1e 100644 --- a/notes/README.md +++ b/notes/README.md @@ -233,7 +233,7 @@ use hooks to define state and modify it from within listeners. ```rust, ignore fn app(cx: Scope) -> Element { - let name = use_state(&cx, || "world"); + let name = use_state(cx, || "world"); render!("hello {name}!") } @@ -280,7 +280,7 @@ fn main() { } fn App(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let count = use_state(cx, || 0); cx.render(rsx!( div { "Count: {count}" } diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 4da075c7d..8b1e6185b 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -1,9 +1,6 @@ -
-

🌗🚀 Dioxus

-

- Frontend that scales. -

-
+

+ +

@@ -26,9 +23,7 @@ CI status -
- - - - -
-Dioxus 是一个可移植、高性能的框架,用于在 Rust 中构建跨平台的用户界面。 +Dioxus 是一个可移植的、高性能的、符合人体工程学的框架,使用 Rust 语言构建跨平台的用户界面。 ```rust fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let mut count = use_state(cx, || 0); cx.render(rsx! { h1 { "High-Five counter: {count}" } @@ -75,103 +64,111 @@ fn app(cx: Scope) -> Element { } ``` -Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应用。 +Dioxus 可用于生成 网页前端、桌面应用、静态网站、移动端应用、TUI程序、等多类平台应用。 -Dioxus 为不同的平台都提供了很好的开发文档。 +如果你能够熟悉使用 React 框架,那 Dioxus 对你来说将非常简单。 -如果你会使用 React ,那 Dioxus 对你来说会很简单。 +## 独特的特性: +- 桌面程序完全基于本地环境运行(并非 Electron 的封装) +- 符合人体工程学的设计以及拥有强大的状态管理 +- 全面的内联文档 - 包含所有 HTML 元素、监听器 和 事件 指南。 +- 极快的运行效率和极高的内存效率 +- 智能项目热更新和高效的项目迭代 +- 一流的异步支持🔥 +- 更多内容请查看 [版本发布信息](https://dioxuslabs.com/blog/introducing-dioxus/). -### 项目特点: -- 对桌面应用的原生支持。 -- 强大的状态管理工具。 -- 支持所有 HTML 标签,监听器和事件。 -- 超高的内存使用率,稳定的组件分配器。 -- 多通道异步调度器,超强的异步支持。 -- 更多信息请查阅: [版本发布文档](https://dioxuslabs.com/blog/introducing-dioxus/). - -### 示例 - -本项目中的所有例子都是 `桌面应用` 程序,请使用 `cargo run --example XYZ` 运行这些例子。 - -``` -cargo run --example EXAMPLE -``` - -## 进入学习 - - - - - - - - - +## 已支持的平台 +
+
教程网页端桌面端SSR移动端状态管理
-
+ 网站项目 + +
    +
  • 使用 WebAssembly 直接对 DOM 进行渲染
  • +
  • 为 SSR 提供预渲染或作为客户端使用
  • +
  • 简单的 "Hello World" 仅仅 65kb, 媲美 React 框架
  • +
  • CLI 提供热更新支持,方便项目快速迭代
  • +
+ + + + 桌面应用 + +
    +
  • 使用 Webview 进行渲染 或 使用 WGPU 和 Skia(试验性的)
  • +
  • 无多余配置,使用 `cargo build` 即可快速构建
  • +
  • 对原生系统的全面支持
  • +
  • 支持 Macos、Linux、Windows 等系统,极小的二进制文件
  • +
+ + + + 移动端应用 + +
    +
  • 使用 Webview 进行渲染 或 使用 WGPU 和 Skia(试验性的)
  • +
  • 支持 IOS 和 安卓系统
  • +
  • 显著的 性能强于 React Native 框架
  • +
+ + + + Liveview + +
    +
  • 使用服务器渲染组件与应用程序
  • +
  • 与受欢迎的后端框架进行融合(Axum、Wrap)
  • +
  • 及低的延迟
  • +
+ + + + 终端程序 + +
    +
  • 在终端程序中渲染,类似于: ink.js
  • +
  • 支持 CSS 相关模型(类似于浏览器内的)
  • +
  • Built-in widgets like text input, buttons, and focus system
  • +
+ + + + + +## Why Dioxus? + +目前有非常多的应用开发选择,为什么偏偏要选择 Dioxus 呢? + +首先,Dioxus将开发者的经验放在首位。这反映在 Dioxus 特有的各种功能上。 + +- 自动格式化 RSX 格式代码,并拥有 VSCode 插件作为支持。 +- 热加载基于 RSX 代码解析器,同时支持桌面程序和网页程序。 +- 强调文档的重要性,我们对所有 HTML 元素都提供文档支持。 + +Dioxus 也是一个可扩展化的平台。 + +- 通过实现一个非常简单的优化堆栈机,轻松构建新的渲染器。 +- 构建并分享开发者自定义的组件代码。 + +Dioxus 那么优秀,但什么时候它不适合我呢? +- 它还没有完全成熟。api仍在变化,可能会出现故障(尽管我们试图避免) +- 您需要运行在 no-std 的环境之中。 +- 你不喜欢使用 React-like 的方式构建 UI 项目。 -## Dioxus 项目 - -| 文件浏览器 (桌面应用) | WiFi 扫描器 (桌面应用) | Todo管理 (所有平台) | 商城系统 (SSR/liveview) | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![E-commerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) | +## 贡献代码 +- 在我们的 [问题追踪](https://github.com/dioxuslabs/dioxus/issues) 中汇报你遇到的问题。 +- 加入我们的 Discord 与我们交流。 -查看 [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) 查看更多有趣(~~NiuBi~~)的项目! + + + -## 为什么使用 Dioxus 和 Rust ? +## 开源协议 -TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScript。 +本项目使用 [MIT license]. -TS 代码运行效率不高,而且有大量的配置项。 +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT -相比之下,Dioxus 使用 Rust 编写将大大的提高效能。 - -使用 Rust 开发,我们能获得: - -- 静态类型支持。 -- 变量默认不变性。 -- 简单直观的模块系统。 -- 内部集成的文档系统。 -- 先进的模式匹配系统。 -- 简洁、高效、强大的迭代器。 -- 内置的 单元测试 / 集成测试。 -- 优秀的异常处理系统。 -- 强大且健全的标准库。 -- 灵活的 `宏` 系统。 -- 使用 `crates.io` 管理包。 - -Dioxus 能为开发者提供的: - -- 安全使用数据结构。 -- 安全的错误处理结果。 -- 拥有原生移动端的性能。 -- 直接访问系统的IO层。 - -Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让团队能在更短的时间内做出强大功能。 - -### 不建议使用 Dioxus 的情况? - -您不该在这些情况下使用 Dioxus : - -- 您不喜欢类似 React 的开发风格。 -- 您需要一个 `no-std` 的渲染器。 -- 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 -- 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。 - -### 项目生态 - -想要加入我们一起为 Dioxus 生态努力吗?有很多项目都能在您的帮助下获得改变: - -- [TUI 渲染器](https://github.com/dioxusLabs/rink) -- [CLI 开发工具](https://github.com/dioxusLabs/cli) -- [官网及文档](https://github.com/dioxusLabs/docsite) -- 动态网站 及 Web 服务器 -- 资源系统 - -## 协议 - -这个项目使用 [MIT 协议]. - -[MIT 协议]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE +除非您另有明确声明,否则有意提交的任何贡献将被授权为 MIT 协议,没有任何附加条款或条件。 diff --git a/notes/SOLVEDPROBLEMS.md b/notes/SOLVEDPROBLEMS.md index c74b4117f..b978c923d 100644 --- a/notes/SOLVEDPROBLEMS.md +++ b/notes/SOLVEDPROBLEMS.md @@ -406,8 +406,8 @@ enum Patch { ``` ```rust -let node_ref = use_node_ref(&cx); -use_effect(&cx, || { +let node_ref = use_node_ref(cx); +use_effect(cx, || { }, []); div { ref: node_ref, diff --git a/notes/header.svg b/notes/header.svg new file mode 100644 index 000000000..59c96f2f2 --- /dev/null +++ b/notes/header.svg @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs index efb4754c9..81c35a702 100644 --- a/packages/autofmt/src/buffer.rs +++ b/packages/autofmt/src/buffer.rs @@ -3,7 +3,7 @@ use std::{ fmt::{Result, Write}, }; -use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed}; +use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, IfmtInput}; use proc_macro2::{LineColumn, Span}; use syn::{spanned::Spanned, Expr}; @@ -67,13 +67,14 @@ impl Buffer { match node { BodyNode::Element(el) => self.write_element(el), BodyNode::Component(component) => self.write_component(component), - BodyNode::Text(text) => self.write_text(&text.text), - BodyNode::RawExpr(exp) => self.write_raw_expr(&exp.expr), + BodyNode::Text(text) => self.write_text(text), + BodyNode::RawExpr(exp) => self.write_raw_expr(exp), + _ => Ok(()), } } - pub fn write_text(&mut self, text: &syn::LitStr) -> Result { - write!(self.buf, "\"{}\"", text.value()) + pub fn write_text(&mut self, text: &IfmtInput) -> Result { + write!(self.buf, "\"{}\"", text.source.as_ref().unwrap().value()) } pub fn consume(self) -> Option { @@ -165,13 +166,13 @@ impl Buffer { total += match &attr.attr { ElementAttr::AttrText { value, name } => { - value.value().len() + name.span().line_length() + 3 + 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.value().len() + name.value().len() + 3 + value.source.as_ref().unwrap().value().len() + name.value().len() + 3 } ElementAttr::CustomAttrExpression { name, value } => { name.value().len() + value.span().line_length() + 3 diff --git a/packages/autofmt/src/component.rs b/packages/autofmt/src/component.rs index 036d0dd17..0da8a85d7 100644 --- a/packages/autofmt/src/component.rs +++ b/packages/autofmt/src/component.rs @@ -167,7 +167,12 @@ impl Buffer { write!(self.buf, "{}: {}", name, out)?; } ContentField::Formatted(s) => { - write!(self.buf, "{}: \"{}\"", name, s.value())?; + write!( + self.buf, + "{}: \"{}\"", + name, + s.source.as_ref().unwrap().value() + )?; } ContentField::OnHandlerRaw(exp) => { let out = prettyplease::unparse_expr(exp); @@ -209,7 +214,7 @@ impl Buffer { let attr_len = fields .iter() .map(|field| match &field.content { - ContentField::Formatted(s) => s.value().len() , + ContentField::Formatted(s) => s.source.as_ref().unwrap().value().len() , ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => { let formatted = prettyplease::unparse_expr(exp); let len = if formatted.contains('\n') { diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 916545deb..16b4d8a68 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -136,7 +136,7 @@ impl Buffer { fn write_attributes( &mut self, attributes: &[ElementAttrNamed], - key: &Option, + key: &Option, sameline: bool, ) -> Result { let mut attr_iter = attributes.iter().peekable(); @@ -145,7 +145,11 @@ impl Buffer { if !sameline { self.indented_tabbed_line()?; } - write!(self.buf, "key: \"{}\"", key.value())?; + write!( + self.buf, + "key: \"{}\"", + key.source.as_ref().unwrap().value() + )?; if !attributes.is_empty() { write!(self.buf, ",")?; if sameline { @@ -182,7 +186,11 @@ impl Buffer { fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result { match &attr.attr { ElementAttr::AttrText { name, value } => { - write!(self.buf, "{name}: \"{value}\"", value = value.value())?; + write!( + self.buf, + "{name}: \"{value}\"", + value = value.source.as_ref().unwrap().value() + )?; } ElementAttr::AttrExpression { name, value } => { let out = prettyplease::unparse_expr(value); @@ -194,7 +202,7 @@ impl Buffer { self.buf, "\"{name}\": \"{value}\"", name = name.value(), - value = value.value() + value = value.source.as_ref().unwrap().value() )?; } @@ -276,7 +284,7 @@ impl Buffer { } match children { - [BodyNode::Text(ref text)] => Some(text.text.value().len()), + [BodyNode::Text(ref text)] => Some(text.source.as_ref().unwrap().value().len()), [BodyNode::Component(ref comp)] => { let attr_len = self.field_len(&comp.fields, &comp.manual_props); diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index f6b352689..e8dd1e1ef 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -18,8 +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" } -dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true } +dioxus-rsx = { path = "../rsx", version = "0.0.1" } # testing [dev-dependencies] @@ -28,4 +27,3 @@ trybuild = "1.0" [features] default = [] -hot-reload = ["dioxus-rsx-interpreter"] \ No newline at end of file diff --git a/packages/core-macro/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs index 05d9d5240..dc9c4711b 100644 --- a/packages/core-macro/src/inlineprops.rs +++ b/packages/core-macro/src/inlineprops.rs @@ -9,6 +9,7 @@ use syn::{ pub struct InlinePropsBody { pub attrs: Vec, pub vis: syn::Visibility, + pub maybe_async: Option, pub fn_token: Token![fn], pub ident: Ident, pub cx_token: Box, @@ -25,6 +26,7 @@ pub struct InlinePropsBody { impl Parse for InlinePropsBody { fn parse(input: ParseStream) -> Result { let attrs: Vec = input.call(Attribute::parse_outer)?; + let maybe_async: Option = input.parse().ok(); let vis: Visibility = input.parse()?; let fn_token = input.parse()?; @@ -57,6 +59,7 @@ impl Parse for InlinePropsBody { Ok(Self { vis, + maybe_async, fn_token, ident, generics, @@ -84,6 +87,7 @@ impl ToTokens for InlinePropsBody { block, cx_token, attrs, + maybe_async, .. } = self; @@ -151,7 +155,7 @@ impl ToTokens for InlinePropsBody { } #(#attrs)* - #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>) #output #where_clause { let #struct_name { #(#field_names),* } = &cx.props; diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 43ad7015a..4e4f5600a 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -11,8 +11,7 @@ use dioxus_rsx as rsx; #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { use rsx::*; - let item = parse_macro_input!(input as IfmtInput); - format_args_f_impl(item) + format_args_f_impl(parse_macro_input!(input as IfmtInput)) .unwrap_or_else(|err| err.to_compile_error()) .into() } @@ -27,68 +26,25 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token } /// The rsx! macro makes it easy for developers to write jsx-style markup in their components. -/// -/// ## Complete Reference Guide: -/// ```ignore -#[doc = include_str!("../../../examples/rsx_usage.rs")] -/// ``` #[proc_macro] pub fn rsx(s: TokenStream) -> TokenStream { - #[cfg(feature = "hot-reload")] - let rsx_text = s.to_string(); match syn::parse::(s) { Err(err) => err.to_compile_error().into(), - Ok(body) => { - #[cfg(feature = "hot-reload")] - { - use dioxus_rsx_interpreter::captuered_context::CapturedContextBuilder; - - match CapturedContextBuilder::from_call_body(body) { - Ok(captured) => { - let lazy = quote::quote! { - LazyNodes::new(move |__cx|{ - let code_location = get_line_num!(); - let captured = #captured; - let text = #rsx_text; - - resolve_scope(code_location, text, captured, __cx) - }) - }; - if let Some(cx) = captured.custom_context { - quote::quote! { - #cx.render(#lazy) - } - .into() - } else { - lazy.into() - } - } - Err(err) => err.into_compile_error().into(), - } - } - #[cfg(not(feature = "hot-reload"))] - body.to_token_stream().into() - } + Ok(body) => body.to_token_stream().into(), } } /// The render! macro makes it easy for developers to write jsx-style markup in their components. /// /// The render macro automatically renders rsx - making it unhygenic. -/// -/// ## Complete Reference Guide: -/// ```ignore -#[doc = include_str!("../../../examples/rsx_usage.rs")] -/// ``` #[proc_macro] pub fn render(s: TokenStream) -> TokenStream { match syn::parse::(s) { Err(err) => err.to_compile_error().into(), - Ok(body) => quote::quote! { - cx.render(#body) + Ok(mut body) => { + body.inline_cx = true; + body.into_token_stream().into() } - .into_token_stream() - .into(), } } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 1d5c58d1f..b614acb43 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -194,7 +194,7 @@ mod field_info { // children field is automatically defaulted to None if name == "children" { builder_attr.default = - Some(syn::parse(quote!(Default::default()).into()).unwrap()); + Some(syn::parse(quote!(::dioxus::core::VNode::empty()).into()).unwrap()); } // auto detect optional @@ -781,7 +781,7 @@ Finally, call `.build()` to create the instance of `{name}`. let arg_type = if field.builder_attr.strip_option { field.type_from_inside_option(false).ok_or_else(|| { Error::new_spanned( - &field_type, + field_type, "can't `strip_option` - field is not `Option<...>`", ) })? diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index b3a8007f0..af134b8ce 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -18,34 +18,31 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] bumpalo = { version = "3.6", features = ["collections", "boxed"] } # faster hashmaps -fxhash = "0.2" +rustc-hash = "1.1.0" # Used in diffing longest-increasing-subsequence = "0.1.0" -# internall used -log = { version = "0.4" } - futures-util = { version = "0.3", default-features = false } -smallvec = "1.6" - slab = "0.4" futures-channel = "0.3.21" -# used for noderefs -once_cell = "1.8" - indexmap = "1.7" # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } +anyhow = "1.0.66" -# todo: I want to get rid of this -backtrace = "0.3" +smallbox = "0.8.1" +log = "0.4.17" + +[dev-dependencies] +tokio = { version = "*", features = ["full"] } +dioxus = { path = "../dioxus" } +pretty_assertions = "1.3.0" [features] default = [] serialize = ["serde"] -debug_vdom = [] \ No newline at end of file diff --git a/packages/core/README.md b/packages/core/README.md index 9b0b14fff..d3ad1946f 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,52 +1,84 @@ -# Dioxus-core +# dioxus-core -This is the core crate for the Dioxus Virtual DOM. This README will focus on the technical design and layout of this Virtual DOM implementation. If you want to read more about using Dioxus, then check out the Dioxus crate, documentation, and website. +dioxus-core is a fast and featureful VirtualDom implementation written in and for Rust. -To build new apps with Dioxus or to extend the ecosystem with new hooks or components, use the higher-level `dioxus` crate with the appropriate feature flags. +# Features +- Functions as components +- Hooks for local state +- Task pool for spawning futures +- Template-based architecture +- Asynchronous components +- Suspense boundaries +- Error boundaries through the `anyhow` crate +- Customizable memoization + +If just starting out, check out the Guides first. + +# General Theory + +The dioxus-core `VirtualDom` object is built around the concept of a `Template`. Templates describe a layout tree known at compile time with dynamic parts filled at runtime. + +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. + +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. + +# Usage + +All Dioxus apps start as just a function that takes the [`Scope`] object and returns an [`Element`]. + +The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler syntax of Rust into the logic required to build Templates. + +First, start with your app: ```rust, ignore fn app(cx: Scope) -> Element { - render!(div { "hello world" }) + cx.render(rsx!( div { "hello world" } )) } +``` -fn main() { - let mut renderer = SomeRenderer::new(); +Then, we'll want to create a new VirtualDom using this app as the root component. - // Creating a new virtualdom from a component - let mut dom = VirtualDom::new(app); +```rust, ignore +let mut dom = VirtualDom::new(app); +``` - // Patching the renderer with the changes to draw the screen - let edits = dom.rebuild(); - renderer.apply(edits); +To build the app into a stream of mutations, we'll use [`VirtualDom::rebuild`]: - // Injecting events - dom.handle_message(SchedulerMsg::Event(UserEvent { - scope_id: None, - priority: EventPriority::High, - element: ElementId(0), - name: "onclick", - data: Arc::new(()), - })); +```rust, ignore +let mutations = dom.rebuild(); - // polling asynchronously - dom.wait_for_work().await; +apply_edits_to_real_dom(mutations); +``` - // working with a deadline - if let Some(edits) = dom.work_with_deadline(|| false) { - renderer.apply(edits); +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, ignore +// 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))); +``` + +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 { + select! { + evt = real_dom.event() => dom.handle_event("click", evt.data, evt.element, evt.bubbles), + _ = dom.wait_for_work() => {} } - // getting state of scopes - let scope = dom.get_scope(ScopeId(0)).unwrap(); + // Render any work without blocking the main thread for too long + let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(10))); - // iterating through the tree - match scope.root_node() { - VNodes::Text(vtext) => dbg!(vtext), - VNodes::Element(vel) => dbg!(vel), - _ => todo!() - } + // And then apply the edits + real_dom.apply(mutations); } + ``` ## Internals @@ -82,17 +114,3 @@ The final implementation of Dioxus must: - 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. - - -## Safety - -Dioxus uses unsafe. The design of Dioxus *requires* unsafe (self-referential trees). - -All of our test suite passes MIRI without errors. - -Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinning, and a lot more foundational low-level work that is very difficult to implement with 0 unsafe. - -If you don't want to use a crate that uses unsafe, then this crate is not for you. - -However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous invariants required to execute certain features. - diff --git a/packages/core/architecture.md b/packages/core/architecture.md index 87c3cf4a6..84fea3917 100644 --- a/packages/core/architecture.md +++ b/packages/core/architecture.md @@ -114,6 +114,11 @@ Some essential reading: - https://web.dev/rail/ - https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react +# Templates + +If everything is a template, then we'll have the idea that the only children can b Templates + + # What's going on? Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs new file mode 100644 index 000000000..c3ea4dc2c --- /dev/null +++ b/packages/core/src/any_props.rs @@ -0,0 +1,73 @@ +use std::marker::PhantomData; + +use crate::{ + innerlude::Scoped, + nodes::{ComponentReturn, RenderReturn}, + scopes::{Scope, ScopeState}, + Element, +}; + +/// A trait that essentially allows VComponentProps to be used generically +/// +/// # Safety +/// +/// This should not be implemented outside this module +pub(crate) unsafe trait AnyProps<'a> { + fn props_ptr(&self) -> *const (); + fn render(&'a self, bump: &'a ScopeState) -> RenderReturn<'a>; + unsafe fn memoize(&self, other: &dyn AnyProps) -> bool; +} + +pub(crate) struct VProps<'a, P, A, F: ComponentReturn<'a, A> = Element<'a>> { + pub render_fn: fn(Scope<'a, P>) -> F, + pub memo: unsafe fn(&P, &P) -> bool, + pub props: P, + _marker: PhantomData, +} + +impl<'a, P, A, F> VProps<'a, P, A, F> +where + F: ComponentReturn<'a, A>, +{ + pub(crate) fn new( + render_fn: fn(Scope<'a, P>) -> F, + memo: unsafe fn(&P, &P) -> bool, + props: P, + ) -> Self { + Self { + render_fn, + memo, + props, + _marker: PhantomData, + } + } +} + +unsafe impl<'a, P, A, F> AnyProps<'a> for VProps<'a, P, A, F> +where + F: ComponentReturn<'a, A>, +{ + fn props_ptr(&self) -> *const () { + &self.props as *const _ as *const () + } + + // Safety: + // this will downcast the other ptr as our swallowed type! + // you *must* make this check *before* calling this method + // if your functions are not the same, then you will downcast a pointer into a different type (UB) + unsafe fn memoize(&self, other: &dyn AnyProps) -> bool { + let real_other: &P = &*(other.props_ptr() as *const _ as *const P); + let real_us: &P = &*(self.props_ptr() as *const _ as *const P); + (self.memo)(real_us, real_other) + } + + fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> { + 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) + } +} diff --git a/packages/core/src/arbitrary_value.rs b/packages/core/src/arbitrary_value.rs deleted file mode 100644 index cc6b27838..000000000 --- a/packages/core/src/arbitrary_value.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::fmt::Formatter; - -/// Possible values for an attribute -// trying to keep values at 3 bytes -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serialize", serde(untagged))] -#[derive(Clone, Debug, PartialEq)] -#[allow(missing_docs)] -pub enum AttributeValue<'a> { - Text(&'a str), - Float32(f32), - Float64(f64), - Int32(i32), - Int64(i64), - Uint32(u32), - Uint64(u64), - Bool(bool), - - Vec3Float(f32, f32, f32), - Vec3Int(i32, i32, i32), - Vec3Uint(u32, u32, u32), - - Vec4Float(f32, f32, f32, f32), - Vec4Int(i32, i32, i32, i32), - Vec4Uint(u32, u32, u32, u32), - - Bytes(&'a [u8]), - Any(ArbitraryAttributeValue<'a>), -} - -// todo -#[allow(missing_docs)] -impl<'a> AttributeValue<'a> { - pub fn is_truthy(&self) -> bool { - match self { - AttributeValue::Text(t) => *t == "true", - AttributeValue::Bool(t) => *t, - _ => false, - } - } - - pub fn is_falsy(&self) -> bool { - match self { - AttributeValue::Text(t) => *t == "false", - AttributeValue::Bool(t) => !(*t), - _ => false, - } - } -} - -impl<'a> std::fmt::Display for AttributeValue<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - AttributeValue::Text(a) => write!(f, "{}", a), - AttributeValue::Float32(a) => write!(f, "{}", a), - AttributeValue::Float64(a) => write!(f, "{}", a), - AttributeValue::Int32(a) => write!(f, "{}", a), - AttributeValue::Int64(a) => write!(f, "{}", a), - AttributeValue::Uint32(a) => write!(f, "{}", a), - AttributeValue::Uint64(a) => write!(f, "{}", a), - AttributeValue::Bool(a) => write!(f, "{}", a), - AttributeValue::Vec3Float(_, _, _) => todo!(), - AttributeValue::Vec3Int(_, _, _) => todo!(), - AttributeValue::Vec3Uint(_, _, _) => todo!(), - AttributeValue::Vec4Float(_, _, _, _) => todo!(), - AttributeValue::Vec4Int(_, _, _, _) => todo!(), - AttributeValue::Vec4Uint(_, _, _, _) => todo!(), - AttributeValue::Bytes(a) => write!(f, "{:?}", a), - AttributeValue::Any(_) => todo!(), - } - } -} - -#[derive(Clone, Copy)] -pub struct ArbitraryAttributeValue<'a> { - pub value: &'a dyn std::any::Any, - pub cmp: fn(&'a dyn std::any::Any, &'a dyn std::any::Any) -> bool, -} - -impl PartialEq for ArbitraryAttributeValue<'_> { - fn eq(&self, other: &Self) -> bool { - (self.cmp)(self.value, other.value) - } -} - -impl std::fmt::Debug for ArbitraryAttributeValue<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ArbitraryAttributeValue").finish() - } -} - -#[cfg(feature = "serialize")] -impl<'a> serde::Serialize for ArbitraryAttributeValue<'a> { - fn serialize(&self, _serializer: S) -> Result - where - S: serde::Serializer, - { - panic!("ArbitraryAttributeValue should not be serialized") - } -} -#[cfg(feature = "serialize")] -impl<'de, 'a> serde::Deserialize<'de> for &'a ArbitraryAttributeValue<'a> { - fn deserialize(_deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - panic!("ArbitraryAttributeValue is not deserializable!") - } -} -#[cfg(feature = "serialize")] -impl<'de, 'a> serde::Deserialize<'de> for ArbitraryAttributeValue<'a> { - fn deserialize(_deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - panic!("ArbitraryAttributeValue is not deserializable!") - } -} - -// todo -#[allow(missing_docs)] -impl<'a> AttributeValue<'a> { - pub fn as_text(&self) -> Option<&'a str> { - match self { - AttributeValue::Text(s) => Some(s), - _ => None, - } - } - - pub fn as_float32(&self) -> Option { - match self { - AttributeValue::Float32(f) => Some(*f), - _ => None, - } - } - - pub fn as_float64(&self) -> Option { - match self { - AttributeValue::Float64(f) => Some(*f), - _ => None, - } - } - - pub fn as_int32(&self) -> Option { - match self { - AttributeValue::Int32(i) => Some(*i), - _ => None, - } - } - - pub fn as_int64(&self) -> Option { - match self { - AttributeValue::Int64(i) => Some(*i), - _ => None, - } - } - - pub fn as_uint32(&self) -> Option { - match self { - AttributeValue::Uint32(i) => Some(*i), - _ => None, - } - } - - pub fn as_uint64(&self) -> Option { - match self { - AttributeValue::Uint64(i) => Some(*i), - _ => None, - } - } - - pub fn as_bool(&self) -> Option { - match self { - AttributeValue::Bool(b) => Some(*b), - _ => None, - } - } - - pub fn as_vec3_float(&self) -> Option<(f32, f32, f32)> { - match self { - AttributeValue::Vec3Float(x, y, z) => Some((*x, *y, *z)), - _ => None, - } - } - - pub fn as_vec3_int(&self) -> Option<(i32, i32, i32)> { - match self { - AttributeValue::Vec3Int(x, y, z) => Some((*x, *y, *z)), - _ => None, - } - } - - pub fn as_vec3_uint(&self) -> Option<(u32, u32, u32)> { - match self { - AttributeValue::Vec3Uint(x, y, z) => Some((*x, *y, *z)), - _ => None, - } - } - - pub fn as_vec4_float(&self) -> Option<(f32, f32, f32, f32)> { - match self { - AttributeValue::Vec4Float(x, y, z, w) => Some((*x, *y, *z, *w)), - _ => None, - } - } - - pub fn as_vec4_int(&self) -> Option<(i32, i32, i32, i32)> { - match self { - AttributeValue::Vec4Int(x, y, z, w) => Some((*x, *y, *z, *w)), - _ => None, - } - } - - pub fn as_vec4_uint(&self) -> Option<(u32, u32, u32, u32)> { - match self { - AttributeValue::Vec4Uint(x, y, z, w) => Some((*x, *y, *z, *w)), - _ => None, - } - } - - pub fn as_bytes(&self) -> Option<&[u8]> { - match self { - AttributeValue::Bytes(b) => Some(b), - _ => None, - } - } - - pub fn as_any(&self) -> Option<&'a ArbitraryAttributeValue> { - match self { - AttributeValue::Any(a) => Some(a), - _ => None, - } - } -} diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs new file mode 100644 index 000000000..5702a9c9b --- /dev/null +++ b/packages/core/src/arena.rs @@ -0,0 +1,181 @@ +use crate::{ + nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode, + ScopeId, +}; +use bumpalo::boxed::Box as BumpBox; + +/// An Element's unique identifier. +/// +/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is +/// unmounted, then the `ElementId` will be reused for a new component. +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct ElementId(pub usize); + +pub(crate) struct ElementRef { + // the pathway of the real element inside the template + pub path: ElementPath, + + // The actual template + pub template: *const VNode<'static>, +} + +#[derive(Clone, Copy)] +pub enum ElementPath { + Deep(&'static [u8]), + Root(usize), +} + +impl ElementRef { + pub(crate) fn null() -> Self { + Self { + template: std::ptr::null_mut(), + path: ElementPath::Root(0), + } + } +} + +impl VirtualDom { + pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { + self.next_reference(template, ElementPath::Deep(path)) + } + + pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { + self.next_reference(template, ElementPath::Root(path)) + } + + 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 _, + path, + }); + ElementId(id) + } + + pub(crate) fn reclaim(&mut self, el: ElementId) { + self.try_reclaim(el) + .unwrap_or_else(|| panic!("cannot reclaim {:?}", el)); + } + + pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option { + if el.0 == 0 { + panic!( + "Cannot reclaim the root element - {:#?}", + std::backtrace::Backtrace::force_capture() + ); + } + + self.elements.try_remove(el.0) + } + + pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) { + let node: *const VNode = node as *const _; + self.elements[el.0].template = unsafe { std::mem::transmute(node) }; + } + + // Drop a scope and all its children + pub(crate) fn drop_scope(&mut self, id: ScopeId) { + self.ensure_drop_safety(id); + + if let Some(root) = self.scopes[id.0].as_ref().try_root_node() { + if let RenderReturn::Sync(Ok(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(Ok(node)) = unsafe { root.extend_lifetime_ref() } { + self.drop_scope_inner(node) + } + } + + self.scopes[id.0].props.take(); + + let scope = &mut self.scopes[id.0]; + + // 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) }); + } + } + + 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); + } + 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); + } + } + }); + + 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: ScopeId) { + let scope = &self.scopes[scope.0]; + + // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we + // run the hooks (which hold an &mut Reference) + // recursively call ensure_drop_safety on all children + let mut props = scope.borrowed_props.borrow_mut(); + props.drain(..).for_each(|comp| { + let comp = unsafe { &*comp }; + if let Some(scope_id) = comp.scope.get() { + self.ensure_drop_safety(scope_id); + } + drop(comp.props.take()); + }); + + // Now that all the references are gone, we can safely drop our own references in our listeners. + let mut listeners = scope.listeners.borrow_mut(); + listeners.drain(..).for_each(|listener| { + let listener = unsafe { &*listener }; + if let AttributeValue::Listener(l) = &listener.value { + _ = l.take(); + } + }); + } +} + +impl ElementPath { + pub(crate) fn is_ascendant(&self, big: &&[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, + } + } +} + +impl PartialEq<&[u8]> for ElementPath { + fn eq(&self, other: &&[u8]) -> bool { + match *self { + ElementPath::Deep(deep) => deep.eq(*other), + ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8, + } + } +} diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs new file mode 100644 index 000000000..44ea4f2d6 --- /dev/null +++ b/packages/core/src/bump_frame.rs @@ -0,0 +1,29 @@ +use crate::nodes::RenderReturn; +use bumpalo::Bump; +use std::cell::Cell; + +pub(crate) struct BumpFrame { + pub bump: Bump, + pub node: Cell<*const RenderReturn<'static>>, +} + +impl BumpFrame { + pub(crate) fn new(capacity: usize) -> Self { + let bump = Bump::with_capacity(capacity); + Self { + bump, + node: Cell::new(std::ptr::null()), + } + } + + /// Creates a new lifetime out of thin air + pub(crate) unsafe fn try_load_node<'b>(&self) -> Option<&'b RenderReturn<'b>> { + let node = self.node.get(); + + if node.is_null() { + return None; + } + + unsafe { std::mem::transmute(&*node) } + } +} diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs new file mode 100644 index 000000000..1e0889208 --- /dev/null +++ b/packages/core/src/create.rs @@ -0,0 +1,472 @@ +use crate::any_props::AnyProps; +use crate::innerlude::{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 std::cell::Cell; +use std::iter::{Enumerate, Peekable}; +use std::rc::Rc; +use std::slice; +use TemplateNode::*; + +impl<'b> VirtualDom { + /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. + /// + /// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created. + pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize { + self.scope_stack.push(scope); + let out = self.create(template); + self.scope_stack.pop(); + out + } + + /// 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); + } + + // we know that this will generate at least one mutation per node + self.mutations.edits.reserve(node.template.roots.len()); + + // Walk the roots, creating nodes and assigning IDs + // 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(); + + node.template + .roots + .iter() + .enumerate() + .map(|(idx, root)| match root { + DynamicText { id } | Dynamic { id } => { + nodes.next().unwrap(); + self.write_dynamic_root(node, *id) + } + Element { .. } => self.write_element_root(node, idx, &mut attrs, &mut nodes), + Text { .. } => self.write_static_text_root(node, idx), + }) + .sum() + } + + fn write_static_text_root(&mut self, node: &VNode, idx: usize) -> usize { + // Simply just load the template root, no modifications needed + self.load_template_root(node, idx); + + // Text producs just one node on the stack + 1 + } + + 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), + Placeholder(VPlaceholder { id }) => { + let id = self.set_slot(template, id, idx); + self.mutations.push(CreatePlaceholder { id }); + 1 + } + Text(VText { id, value }) => { + let id = self.set_slot(template, id, idx); + self.create_static_text(value, id); + 1 + } + } + } + + fn create_static_text(&mut self, value: &str, id: ElementId) { + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_text: &str = unsafe { std::mem::transmute(value) }; + self.mutations.push(CreateTextNode { + value: unbounded_text, + id, + }); + } + + /// We write all the descndent data for this element + /// + /// Elements can contain other nodes - and those nodes can be dynamic or static + /// + /// We want to make sure we write these nodes while on top of the root + fn write_element_root( + &mut self, + template: &'b VNode<'b>, + root_idx: usize, + dynamic_attrs: &mut Peekable>>, + dynamic_nodes: &mut Peekable>>, + ) -> 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); + + // Load in all of the placeholder or dynamic content under this root too + self.load_placeholders(dynamic_nodes, root_idx, template); + + 1 + } + + /// Load all of the placeholder nodes for descendents of this root node + /// + /// ```rust, ignore + /// rsx! { + /// div { + /// // This is a placeholder + /// some_value, + /// + /// // Load this too + /// "{some_text}" + /// } + /// } + /// ``` + fn load_placeholders( + &mut self, + dynamic_nodes: &mut Peekable>>, + root_idx: usize, + template: &'b VNode<'b>, + ) { + let (start, end) = match collect_dyn_node_range(dynamic_nodes, root_idx) { + Some((a, b)) => (a, b), + None => return, + }; + + for idx in (start..=end).rev() { + 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..]; + self.mutations.push(ReplacePlaceholder { m, path }); + } + } + } + + fn write_attrs_on_root( + &mut self, + attrs: &mut Peekable>>, + root_idx: usize, + root: ElementId, + node: &VNode, + ) { + while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p[0] == root_idx as u8) { + let id = self.assign_static_node_as_dynamic(path, root, node, attr_id); + + loop { + self.write_attribute(&node.dynamic_attrs[attr_id], id); + + // Only push the dynamic attributes forward if they match the current path (same element) + match attrs.next_if(|(_, p)| *p == path) { + Some((next_attr_id, _)) => attr_id = next_attr_id, + None => break, + } + } + } + } + + fn write_attribute(&mut self, attribute: &crate::Attribute, id: ElementId) { + // Make sure we set the attribute's associated id + attribute.mounted_element.set(id); + + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) }; + + match &attribute.value { + AttributeValue::Text(value) => { + // 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) }; + + self.mutations.push(SetAttribute { + name: unbounded_name, + value: unbounded_value, + ns: attribute.namespace, + 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)); + + self.mutations.push(LoadTemplate { + name: template.template.name, + index: root_idx, + id: this_id, + }); + + this_id + } + + /// We have some dynamic attributes attached to a some node + /// + /// That node needs to be loaded at runtime, so we need to give it an ID + /// + /// If the node in question is on the stack, we just return that ID + /// + /// If the node is not on the stack, we create a new ID for it and assign it + fn assign_static_node_as_dynamic( + &mut self, + path: &'static [u8], + this_id: ElementId, + template: &VNode, + attr_id: usize, + ) -> ElementId { + if path.len() == 1 { + return this_id; + } + + // 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]); + + self.mutations.push(Mutation::AssignId { + path: &path[1..], + id, + }); + + id + } + + /// Insert a new template into the VirtualDom's template registry + fn register_template(&mut self, template: &'b VNode<'b>) { + // First, make sure we mark the template as seen, regardless if we process it + self.templates + .insert(template.template.name, template.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); + } + } + + pub(crate) fn create_dynamic_node( + &mut self, + template: &'b VNode<'b>, + node: &'b DynamicNode<'b>, + idx: usize, + ) -> usize { + 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), + Component(component) => self.create_component_node(template, component, idx), + } + } + + fn create_dynamic_text( + &mut self, + template: &'b VNode<'b>, + text: &'b VText<'b>, + idx: usize, + ) -> usize { + // Allocate a dynamic element reference for this text node + let new_id = self.next_element(template, template.template.node_paths[idx]); + + // Make sure the text node is assigned to the correct element + text.id.set(Some(new_id)); + + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let value = unsafe { std::mem::transmute(text.value) }; + + // Add the mutation to the list + self.mutations.push(HydrateText { + id: new_id, + path: &template.template.node_paths[idx][1..], + value, + }); + + // Since we're hydrating an existing node, we don't create any new nodes + 0 + } + + pub(crate) fn create_placeholder( + &mut self, + placeholder: &VPlaceholder, + template: &'b VNode<'b>, + idx: usize, + ) -> usize { + // Allocate a dynamic element reference for this text node + let id = self.next_element(template, template.template.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..], + id, + }); + + // Since the placeholder is already in the DOM, we don't create any new nodes + 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 + } + + // Component is coming back, it probably still exists, right? + None => component.scope.get().unwrap(), + }; + + component.scope.set(Some(scope)); + + let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() }; + + use RenderReturn::*; + + match return_nodes { + Sync(Ok(t)) => self.mount_component(scope, template, t, idx), + Sync(Err(_e)) => todo!("Propogate error upwards"), + Async(_) => self.mount_component_placeholder(template, idx, scope), + } + } + + fn mount_component( + &mut self, + scope: ScopeId, + parent: &'b VNode<'b>, + new: &'b VNode<'b>, + idx: usize, + ) -> usize { + // Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary + // is encountered + let mutations_to_this_point = self.mutations.edits.len(); + + // Create the component's root element + let created = self.create_scope(scope, new); + + // If there are no suspense leaves below us, then just don't bother checking anything suspense related + if self.collected_leaves.is_empty() { + return created; + } + + // 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::>() { + 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]); + + // Now connect everything to the boundary + self.scopes[scope.0].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 + let split_off = unsafe { + std::mem::transmute::, Vec>( + self.mutations.edits.split_off(mutations_to_this_point), + ) + }; + boundary.mutations.borrow_mut().edits.extend(split_off); + boundary.created_on_stack.set(created); + boundary + .waiting_on + .borrow_mut() + .extend(self.collected_leaves.drain(..)); + + // Now assign the placeholder in the DOM + self.mutations.push(AssignId { + id: new_id, + path: &parent.template.node_paths[idx][1..], + }); + + 0 + } + + /// 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]); + + // Set the placeholder of the scope + self.scopes[scope.0].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..], + }); + + 0 + } + + fn set_slot( + &mut self, + template: &'b VNode<'b>, + slot: &'b Cell>, + id: usize, + ) -> ElementId { + let id = self.next_element(template, template.template.node_paths[id]); + slot.set(Some(id)); + id + } +} + +fn collect_dyn_node_range( + dynamic_nodes: &mut Peekable>>, + root_idx: usize, +) -> Option<(usize, usize)> { + let start = match dynamic_nodes.peek() { + Some((idx, p)) if p[0] == root_idx as u8 => *idx, + _ => return None, + }; + + let mut end = start; + + while let Some((idx, p)) = dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) { + if p.len() == 1 { + continue; + } + + end = idx; + } + + Some((start, end)) +} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index e2ef92b93..7b8e33083 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,575 +1,293 @@ -#![warn(clippy::pedantic)] -#![allow(clippy::cast_possible_truncation)] - -//! This module contains the stateful [`DiffState`] and all methods to diff [`VNode`]s, their properties, and their children. -//! -//! The [`DiffState`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set -//! of mutations for the renderer to apply. -//! -//! ## Notice: -//! -//! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support -//! Components, Fragments, Suspense, `SubTree` memoization, incremental diffing, cancellation, pausing, priority -//! scheduling, and additional batching operations. -//! -//! ## Implementation Details: -//! -//! ### IDs for elements -//! -------------------- -//! All nodes are addressed by their IDs. -//! We don't necessarily require that DOM changes happen instantly during the diffing process, so the implementor may choose -//! to batch nodes if it is more performant for their application. The element IDs are indices into the internal element -//! array. The expectation is that implementors will use the ID as an index into a Vec of real nodes, allowing for passive -//! garbage collection as the [`crate::VirtualDom`] replaces old nodes. -//! -//! When new vnodes are created through `cx.render`, they won't know which real node they correspond to. During diffing, -//! we always make sure to copy over the ID. If we don't do this properly, the [`ElementId`] will be populated incorrectly -//! and brick the user's page. -//! -//! ### Fragment Support -//! -------------------- -//! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments -//! can be particularly challenging when they are empty, so the anchor node lets us "reserve" a spot for the empty -//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the [`crate::innerlude::NodeFactory`] - it is -//! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. Adding -//! "dummy" nodes _is_ inefficient, but it makes our diffing algorithm faster and the implementation is completely up to -//! the platform. -//! -//! Other implementations either don't support fragments or use a "child + sibling" pattern to represent them. Our code is -//! vastly simpler and more performant when we can just create a placeholder element while the fragment has no children. -//! -//! ### Suspense -//! ------------ -//! Dioxus implements Suspense slightly differently than React. In React, each fiber is manually progressed until it runs -//! into a promise-like value. React will then work on the next "ready" fiber, checking back on the previous fiber once -//! it has finished its new work. In Dioxus, we use a similar approach, but try to completely render the tree before -//! switching sub-fibers. Instead, each future is submitted into a futures-queue and the node is manually loaded later on. -//! Due to the frequent calls to [`crate::virtual_dom::VirtualDom::work_with_deadline`] we can get the pure "fetch-as-you-render" behavior of React Fiber. -//! -//! We're able to use this approach because we use placeholder nodes - futures that aren't ready still get submitted to -//! DOM, but as a placeholder. -//! -//! Right now, the "suspense" queue is intertwined with hooks. In the future, we should allow any future to drive attributes -//! and contents, without the need for a `use_suspense` hook. In the interim, this is the quickest way to get Suspense working. -//! -//! ## Subtree Memoization -//! ----------------------- -//! We also employ "subtree memoization" which saves us from having to check trees which hold no dynamic content. We can -//! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the -//! calls to "create" propagate this information upwards. Structures like the one below are entirely static: -//! ```rust, ignore -//! rsx!( div { class: "hello world", "this node is entirely static" } ) -//! ``` -//! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so it's up to the reconciler to -//! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP and depends on comp-time -//! hashing of the subtree from the rsx! macro. We do a very limited form of static analysis via static string pointers as -//! a way of short-circuiting the most expensive checks. -//! -//! ## Bloom Filter and Heuristics -//! ------------------------------ -//! For all components, we employ some basic heuristics to speed up allocations and pre-size bump arenas. The heuristics are -//! currently very rough, but will get better as time goes on. The information currently tracked includes the size of a -//! bump arena after first render, the number of hooks, and the number of nodes in the tree. -//! -//! ## Garbage Collection -//! --------------------- -//! Dioxus uses a passive garbage collection system to clean up old nodes once the work has been completed. This garbage -//! collection is done internally once the main diffing work is complete. After the "garbage" is collected, Dioxus will then -//! start to re-use old keys for new nodes. This results in a passive memory management system that is very efficient. -//! -//! The IDs used by the key/map are just an index into a Vec. This means that Dioxus will drive the key allocation strategy -//! so the client only needs to maintain a simple list of nodes. By default, Dioxus will not manually clean up old nodes -//! for the client. As new nodes are created, old nodes will be over-written. -//! -//! ## Further Reading and Thoughts -//! ---------------------------- -//! There are more ways of increasing diff performance here that are currently not implemented. -//! - Strong memoization of subtrees. -//! - Guided diffing. -//! - Certain web-dom-specific optimizations. -//! -//! More info on how to improve this diffing algorithm: -//! - - -use crate::innerlude::{ - AnyProps, ElementId, Mutations, ScopeArena, ScopeId, VComponent, VElement, VFragment, VNode, - VPlaceholder, VText, +use crate::{ + any_props::AnyProps, + arena::ElementId, + innerlude::{DirtyScope, VComponent, VPlaceholder, VText}, + mutations::Mutation, + nodes::RenderReturn, + nodes::{DynamicNode, VNode}, + scopes::ScopeId, + virtual_dom::VirtualDom, + Attribute, AttributeValue, TemplateNode, }; -use fxhash::{FxHashMap, FxHashSet}; -use smallvec::{smallvec, SmallVec}; -pub(crate) struct DiffState<'bump> { - pub(crate) scopes: &'bump ScopeArena, - pub(crate) mutations: Mutations<'bump>, - pub(crate) force_diff: bool, - pub(crate) element_stack: SmallVec<[ElementId; 10]>, - pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, -} +use rustc_hash::{FxHashMap, FxHashSet}; +use DynamicNode::*; -impl<'b> DiffState<'b> { - pub fn new(scopes: &'b ScopeArena) -> Self { - Self { - scopes, - mutations: Mutations::new(), - force_diff: false, - element_stack: smallvec![], - scope_stack: smallvec![], +impl<'b> VirtualDom { + pub(super) fn diff_scope(&mut self, scope: ScopeId) { + let scope_state = &mut self.scopes[scope.0]; + + self.scope_stack.push(scope); + unsafe { + // Load the old and new bump arenas + let old = scope_state + .previous_frame() + .try_load_node() + .expect("Call rebuild before diffing"); + + let new = scope_state + .current_frame() + .try_load_node() + .expect("Call rebuild before diffing"); + + use RenderReturn::{Async, Sync}; + + match (old, new) { + (Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r), + + // Err cases + (Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e), + (Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r), + (Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ } + + // Async + (Sync(Ok(_l)), Async(_)) => todo!(), + (Sync(Err(_e)), Async(_)) => todo!(), + (Async(_), Sync(Ok(_r))) => todo!(), + (Async(_), Sync(Err(_e))) => { /* nothing */ } + (Async(_), Async(_)) => { /* nothing */ } + }; } - } - - pub fn diff_scope(&mut self, scopeid: ScopeId) { - let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); - let scope = self.scopes.get_scope(scopeid).unwrap(); - - self.scope_stack.push(scopeid); - self.element_stack.push(scope.container); - { - self.diff_node(old, new); - } - self.element_stack.pop(); self.scope_stack.pop(); - - self.mutations.mark_dirty_scope(scopeid); } - pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) { - use VNode::{Component, Element, Fragment, Placeholder, Text}; - match (old_node, new_node) { - (Text(old), Text(new)) => { - self.diff_text_nodes(old, new, old_node, new_node); - } + fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {} + fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {} - (Placeholder(old), Placeholder(new)) => { - self.diff_placeholder_nodes(old, new, old_node, new_node); - } - - (Element(old), Element(new)) => { - self.diff_element_nodes(old, new, old_node, new_node); - } - - (Component(old), Component(new)) => { - self.diff_component_nodes(old_node, new_node, *old, *new); - } - - (Fragment(old), Fragment(new)) => { - self.diff_fragment_nodes(old, new); - } - - ( - Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), - Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), - ) => self.replace_node(old_node, new_node), + fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { + // 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; } - } - pub fn create_node(&mut self, node: &'b VNode<'b>) -> usize { - match node { - VNode::Text(vtext) => self.create_text_node(vtext, node), - VNode::Placeholder(anchor) => self.create_anchor_node(anchor, node), - VNode::Element(element) => self.create_element_node(element, node), - VNode::Fragment(frag) => self.create_fragment_node(frag), - VNode::Component(component) => self.create_component_node(*component), + // If the templates are different by name, we need to replace the entire template + if templates_are_different(left_template, right_template) { + return self.light_diff_templates(left_template, right_template); } + + // If the templates are the same, we can diff the attributes and children + // Start with the attributes + left_template + .dynamic_attrs + .iter() + .zip(right_template.dynamic_attrs.iter()) + .for_each(|(left_attr, right_attr)| { + // Move over the ID from the old to the new + right_attr + .mounted_element + .set(left_attr.mounted_element.get()); + + // We want to make sure anything listener that gets pulled is valid + if let AttributeValue::Listener(_) = right_attr.value { + self.update_template(left_attr.mounted_element.get(), right_template); + } + + // If the attributes are different (or volatile), we need to update them + if left_attr.value != right_attr.value || left_attr.volatile { + self.update_attribute(right_attr, left_attr); + } + }); + + // Now diff the dynamic nodes + left_template + .dynamic_nodes + .iter() + .zip(right_template.dynamic_nodes.iter()) + .enumerate() + .for_each(|(idx, (left_node, right_node))| { + self.diff_dynamic_node(left_node, right_node, right_template, idx); + }); + + // 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())); } - fn create_text_node(&mut self, text: &'b VText<'b>, node: &'b VNode<'b>) -> usize { - let real_id = self.scopes.reserve_node(node); - text.id.set(Some(real_id)); - self.mutations.create_text_node(text.text, real_id); - 1 - } - - fn create_anchor_node(&mut self, anchor: &'b VPlaceholder, node: &'b VNode<'b>) -> usize { - let real_id = self.scopes.reserve_node(node); - anchor.id.set(Some(real_id)); - self.mutations.create_placeholder(real_id); - 1 - } - - fn create_element_node(&mut self, element: &'b VElement<'b>, node: &'b VNode<'b>) -> usize { - let VElement { - tag: tag_name, - listeners, - attributes, - children, - namespace, - id: dom_id, - parent: parent_id, - .. - } = &element; - - parent_id.set(self.element_stack.last().copied()); - - let real_id = self.scopes.reserve_node(node); - - dom_id.set(Some(real_id)); - - self.element_stack.push(real_id); - { - self.mutations.create_element(tag_name, *namespace, real_id); - - let cur_scope_id = self.current_scope(); - - for listener in listeners.iter() { - listener.mounted_node.set(Some(real_id)); - self.mutations.new_event_listener(listener, cur_scope_id); - } - - for attr in attributes.iter() { - self.mutations.set_attribute(attr, real_id.as_u64()); - } - - if !children.is_empty() { - self.create_and_append_children(children); - } - } - self.element_stack.pop(); - - 1 - } - - fn create_fragment_node(&mut self, frag: &'b VFragment<'b>) -> usize { - self.create_children(frag.children) - } - - fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize { - let parent_idx = self.current_scope(); - - // the component might already exist - if it does, we need to reuse it - // this makes figure out when to drop the component more complicated - let new_idx = if let Some(idx) = vcomponent.scope.get() { - assert!(self.scopes.get_scope(idx).is_some()); - idx - } else { - // Insert a new scope into our component list - let props: Box = vcomponent.props.borrow_mut().take().unwrap(); - let props: Box = unsafe { std::mem::transmute(props) }; - self.scopes.new_with_key( - vcomponent.user_fc, - props, - Some(parent_idx), - self.element_stack.last().copied().unwrap(), - 0, - ) + fn diff_dynamic_node( + &mut self, + left_node: &'b DynamicNode<'b>, + right_node: &'b DynamicNode<'b>, + node: &'b VNode<'b>, + idx: usize, + ) { + match (left_node, right_node) { + (Text(left), Text(right)) => self.diff_vtext(left, right, node), + (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), + (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), + (Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx), + (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."), }; + } - // Actually initialize the caller's slot with the right address - vcomponent.scope.set(Some(new_idx)); + 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"), + } + } - log::trace!( - "created component \"{}\", id: {:?} parent {:?}", - vcomponent.fn_name, - new_idx, - parent_idx, + fn diff_vcomponent( + &mut self, + left: &'b VComponent<'b>, + right: &'b VComponent<'b>, + right_template: &'b VNode<'b>, + idx: usize, + ) { + if std::ptr::eq(left, right) { + return; + } + + // Replace components that have different render fns + if left.render_fn != right.render_fn { + return self.replace_vcomponent(right_template, right, idx, left); + } + + // Make sure the new vcomponent has the right scopeid associated to it + let scope_id = left.scope.get().unwrap(); + + right.scope.set(Some(scope_id)); + + // copy out the box for both + let old = self.scopes[scope_id.0].props.as_ref(); + let new: Box = right.props.take().unwrap(); + let new: Box = unsafe { std::mem::transmute(new) }; + + // If the props are static, then we try to memoize by setting the new with the old + // The target scopestate still has the reference to the old props, so there's no need to update anything + // This also implicitly drops the new props since they're not used + if left.static_props && unsafe { old.as_ref().unwrap().memoize(new.as_ref()) } { + return; + } + + // 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); + + // 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, + id: scope_id, + }); + } + + fn replace_vcomponent( + &mut self, + right_template: &'b VNode<'b>, + right: &'b VComponent<'b>, + idx: usize, + left: &'b VComponent<'b>, + ) { + let m = self.create_component_node(right_template, right, idx); + + self.remove_component_node(left, true); + + // 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 }), + at => panic!("Expected remove mutation from remove_node {:#?}", at), + }; + } + + /// Lightly diff the two templates, checking only their roots. + /// + /// The goal here is to preserve any existing component state that might exist. This is to preserve some React-like + /// behavior where the component state is preserved when the component is re-rendered. + /// + /// This is implemented by iterating each root, checking if the component is the same, if it is, then diff it. + /// + /// We then pass the new template through "create" which should be smart enough to skip roots. + /// + /// Currently, we only handle the case where the roots are the same component list. If there's any sort of deviation, + /// IE more nodes, less nodes, different nodes, or expressions, then we just replace the whole thing. + /// + /// This is mostly implemented to help solve the issue where the same component is rendered under two different + /// conditions: + /// + /// ```rust, ignore + /// if enabled { + /// rsx!{ Component { enabled_sign: "abc" } } + /// } else { + /// rsx!{ Component { enabled_sign: "xyz" } } + /// } + /// ``` + /// + /// However, we should not that it's explicit in the docs that this is not a guarantee. If you need to preserve state, + /// then you should be passing in separate props instead. + /// + /// ```rust, ignore + /// let props = if enabled { + /// ComponentProps { enabled_sign: "abc" } + /// } else { + /// ComponentProps { enabled_sign: "xyz" } + /// }; + /// + /// rsx! { + /// Component { ..props } + /// } + /// ``` + fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { + match matching_components(left, right) { + None => self.replace(left, [right]), + Some(components) => components + .into_iter() + .enumerate() + .for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)), + } + } + + /// Diff the two text nodes + /// + /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's + /// different. + fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) { + let id = left + .id + .get() + .unwrap_or_else(|| self.next_element(node, &[0])); + + right.id.set(Some(id)); + if left.value != right.value { + let value = unsafe { std::mem::transmute(right.value) }; + self.mutations.push(Mutation::SetText { id, value }); + } + } + + fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + let new_is_keyed = new[0].key.is_some(); + let old_is_keyed = old[0].key.is_some(); + debug_assert!( + new.iter().all(|n| n.key.is_some() == new_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + debug_assert!( + old.iter().all(|o| o.key.is_some() == old_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" ); - // if vcomponent.can_memoize { - // // todo: implement promotion logic. save us from boxing props that we don't need - // } else { - // // track this component internally so we know the right drop order - // } - - self.enter_scope(new_idx); - - let created = { - // Run the scope for one iteration to initialize it - self.scopes.run_scope(new_idx); - self.mutations.mark_dirty_scope(new_idx); - - // Take the node that was just generated from running the component - let nextnode = self.scopes.fin_head(new_idx); - self.create_node(nextnode) - }; - - self.leave_scope(); - - created - } - - pub(crate) fn diff_text_nodes( - &mut self, - old: &'b VText<'b>, - new: &'b VText<'b>, - _old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - ) { - if std::ptr::eq(old, new) { - return; - } - - // if the node is comming back not assigned, that means it was borrowed but removed - let root = match old.id.get() { - Some(id) => id, - None => self.scopes.reserve_node(new_node), - }; - - if old.text != new.text { - self.mutations.set_text(new.text, root.as_u64()); - } - - self.scopes.update_node(new_node, root); - - new.id.set(Some(root)); - } - - pub(crate) fn diff_placeholder_nodes( - &mut self, - old: &'b VPlaceholder, - new: &'b VPlaceholder, - _old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - ) { - if std::ptr::eq(old, new) { - return; - } - - // if the node is comming back not assigned, that means it was borrowed but removed - let root = match old.id.get() { - Some(id) => id, - None => self.scopes.reserve_node(new_node), - }; - - self.scopes.update_node(new_node, root); - new.id.set(Some(root)); - } - - fn diff_element_nodes( - &mut self, - old: &'b VElement<'b>, - new: &'b VElement<'b>, - old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - ) { - if std::ptr::eq(old, new) { - return; - } - - // if the node is comming back not assigned, that means it was borrowed but removed - let root = match old.id.get() { - Some(id) => id, - None => self.scopes.reserve_node(new_node), - }; - - // If the element type is completely different, the element needs to be re-rendered completely - // This is an optimization React makes due to how users structure their code - // - // This case is rather rare (typically only in non-keyed lists) - if new.tag != old.tag || new.namespace != old.namespace { - self.replace_node(old_node, new_node); - return; - } - - self.scopes.update_node(new_node, root); - - new.id.set(Some(root)); - new.parent.set(old.parent.get()); - - // todo: attributes currently rely on the element on top of the stack, but in theory, we only need the id of the - // element to modify its attributes. - // it would result in fewer instructions if we just set the id directly. - // it would also clean up this code some, but that's not very important anyways - - // Diff Attributes - // - // It's extraordinarily rare to have the number/order of attributes change - // In these cases, we just completely erase the old set and make a new set - // - // TODO: take a more efficient path than this - if old.attributes.len() == new.attributes.len() { - for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { - if old_attr.value != new_attr.value || new_attr.is_volatile { - self.mutations.set_attribute(new_attr, root.as_u64()); - } - } + if new_is_keyed && old_is_keyed { + self.diff_keyed_children(old, new); } else { - for attribute in old.attributes { - self.mutations.remove_attribute(attribute, root.as_u64()); - } - for attribute in new.attributes { - self.mutations.set_attribute(attribute, root.as_u64()); - } - } - - // Diff listeners - // - // It's extraordinarily rare to have the number/order of listeners change - // In the cases where the listeners change, we completely wipe the data attributes and add new ones - // - // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) - // - // TODO: take a more efficient path than this - let cur_scope_id = self.current_scope(); - - if old.listeners.len() == new.listeners.len() { - for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { - new_l.mounted_node.set(old_l.mounted_node.get()); - if old_l.event != new_l.event { - self.mutations - .remove_event_listener(old_l.event, root.as_u64()); - self.mutations.new_event_listener(new_l, cur_scope_id); - } - } - } else { - for listener in old.listeners { - self.mutations - .remove_event_listener(listener.event, root.as_u64()); - } - for listener in new.listeners { - listener.mounted_node.set(Some(root)); - self.mutations.new_event_listener(listener, cur_scope_id); - } - } - - match (old.children.len(), new.children.len()) { - (0, 0) => {} - (0, _) => { - self.mutations.push_root(root); - let created = self.create_children(new.children); - self.mutations.append_children(created as u32); - self.mutations.pop_root(); - } - (_, _) => self.diff_children(old.children, new.children), - }; - } - - fn diff_component_nodes( - &mut self, - old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - old: &'b VComponent<'b>, - new: &'b VComponent<'b>, - ) { - let scope_addr = old - .scope - .get() - .expect("existing component nodes should have a scope"); - - if std::ptr::eq(old, new) { - return; - } - - // Make sure we're dealing with the same component (by function pointer) - if old.user_fc == new.user_fc { - self.enter_scope(scope_addr); - { - // Make sure the new component vnode is referencing the right scope id - new.scope.set(Some(scope_addr)); - - // make sure the component's caller function is up to date - let scope = self - .scopes - .get_scope(scope_addr) - .unwrap_or_else(|| panic!("could not find {:?}", scope_addr)); - - // take the new props out regardless - // when memoizing, push to the existing scope if memoization happens - let new_props = new - .props - .borrow_mut() - .take() - .expect("new component props should exist"); - - let should_diff = { - if old.can_memoize { - // safety: we trust the implementation of "memoize" - let props_are_the_same = unsafe { - let new_ref = new_props.as_ref(); - scope.props.borrow().as_ref().unwrap().memoize(new_ref) - }; - !props_are_the_same || self.force_diff - } else { - true - } - }; - - if should_diff { - let _old_props = scope - .props - .replace(unsafe { std::mem::transmute(Some(new_props)) }); - - // this should auto drop the previous props - self.scopes.run_scope(scope_addr); - self.mutations.mark_dirty_scope(scope_addr); - - self.diff_node( - self.scopes.wip_head(scope_addr), - self.scopes.fin_head(scope_addr), - ); - } else { - // memoization has taken place - drop(new_props); - }; - } - self.leave_scope(); - } else { - self.replace_node(old_node, new_node); - } - } - - fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) { - if std::ptr::eq(old, new) { - return; - } - - // This is the case where options or direct vnodes might be used. - // In this case, it's faster to just skip ahead to their diff - if old.children.len() == 1 && new.children.len() == 1 { - if !std::ptr::eq(old, new) { - self.diff_node(&old.children[0], &new.children[0]); - } - return; - } - - debug_assert!(!old.children.is_empty()); - debug_assert!(!new.children.is_empty()); - - self.diff_children(old.children, new.children); - } - - // Diff the given set of old and new children. - // - // The parent must be on top of the change list stack when this function is - // entered: - // - // [... parent] - // - // the change list stack is in the same state when this function returns. - // - // If old no anchors are provided, then it's assumed that we can freely append to the parent. - // - // Remember, non-empty lists does not mean that there are real elements, just that there are virtual elements. - // - // Fragment nodes cannot generate empty children lists, so we can assume that when a list is empty, it belongs only - // to an element, and appending makes sense. - fn diff_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { - if std::ptr::eq(old, new) { - return; - } - - // Remember, fragments can never be empty (they always have a single child) - match (old, new) { - ([], []) => {} - ([], _) => self.create_and_append_children(new), - (_, []) => self.remove_nodes(old, true), - _ => { - let new_is_keyed = new[0].key().is_some(); - let old_is_keyed = old[0].key().is_some(); - - debug_assert!( - new.iter().all(|n| n.key().is_some() == new_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - debug_assert!( - old.iter().all(|o| o.key().is_some() == old_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - - if new_is_keyed && old_is_keyed { - self.diff_keyed_children(old, new); - } else { - self.diff_non_keyed_children(old, new); - } - } + self.diff_non_keyed_children(old, new); } } @@ -589,7 +307,7 @@ impl<'b> DiffState<'b> { debug_assert!(!old.is_empty()); match old.len().cmp(&new.len()) { - Ordering::Greater => self.remove_nodes(&old[new.len()..], true), + Ordering::Greater => self.remove_nodes(&old[new.len()..]), Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), Ordering::Equal => {} } @@ -617,11 +335,11 @@ impl<'b> DiffState<'b> { // The stack is empty upon entry. fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { if cfg!(debug_assertions) { - let mut keys = fxhash::FxHashSet::default(); + let mut keys = rustc_hash::FxHashSet::default(); let mut assert_unique_keys = |children: &'b [VNode<'b>]| { keys.clear(); for child in children { - let key = child.key(); + let key = child.key; debug_assert!( key.is_some(), "if any sibling is keyed, all siblings must be keyed" @@ -662,7 +380,7 @@ impl<'b> DiffState<'b> { if new_middle.is_empty() { // remove the old elements - self.remove_nodes(old_middle, true); + self.remove_nodes(old_middle); } else if old_middle.is_empty() { // there were no old elements, so just create the new elements // we need to find the right "foothold" though - we shouldn't use the "append" at all @@ -698,7 +416,7 @@ impl<'b> DiffState<'b> { for (old, new) in old.iter().zip(new.iter()) { // abort early if we finally run into nodes with different keys - if old.key() != new.key() { + if old.key != new.key { break; } self.diff_node(old, new); @@ -715,7 +433,7 @@ impl<'b> DiffState<'b> { // And if that was all of the new children, then remove all of the remaining // old children and we're finished. if left_offset == new.len() { - self.remove_nodes(&old[left_offset..], true); + self.remove_nodes(&old[left_offset..]); return None; } @@ -723,7 +441,7 @@ impl<'b> DiffState<'b> { let mut right_offset = 0; for (old, new) in old.iter().rev().zip(new.iter().rev()) { // abort early if we finally run into nodes with different keys - if old.key() != new.key() { + if old.key != new.key { break; } self.diff_node(old, new); @@ -769,11 +487,10 @@ impl<'b> DiffState<'b> { 8. Finally, generate instructions to remove any old children. 9. Generate instructions to finally diff children that are the same between both */ - // 0. Debug sanity checks // Should have already diffed the shared-key prefixes and suffixes. - debug_assert_ne!(new.first().map(VNode::key), old.first().map(VNode::key)); - debug_assert_ne!(new.last().map(VNode::key), old.last().map(VNode::key)); + debug_assert_ne!(new.first().map(|i| i.key), old.first().map(|i| i.key)); + debug_assert_ne!(new.last().map(|i| i.key), old.last().map(|i| i.key)); // 1. Map the old keys into a numerical ordering based on indices. // 2. Create a map of old key to its index @@ -781,7 +498,7 @@ impl<'b> DiffState<'b> { let old_key_to_old_index = old .iter() .enumerate() - .map(|(i, o)| (o.key().unwrap(), i)) + .map(|(i, o)| (o.key.unwrap(), i)) .collect::>(); let mut shared_keys = FxHashSet::default(); @@ -790,7 +507,7 @@ impl<'b> DiffState<'b> { let new_index_to_old_index = new .iter() .map(|node| { - let key = node.key().unwrap(); + let key = node.key.unwrap(); if let Some(&index) = old_key_to_old_index.get(&key) { shared_keys.insert(key); index @@ -803,14 +520,15 @@ impl<'b> DiffState<'b> { // If none of the old keys are reused by the new children, then we remove all the remaining old children and // create the new children afresh. if shared_keys.is_empty() { - if let Some(first_old) = old.get(0) { - self.remove_nodes(&old[1..], true); - let nodes_created = self.create_children(new); - self.replace_inner(first_old, nodes_created); + if old.get(0).is_some() { + self.remove_nodes(&old[1..]); + self.replace(&old[0], new); } else { // I think this is wrong - why are we appending? // only valid of the if there are no trailing elements - self.create_and_append_children(new); + // self.create_and_append_children(new); + + todo!("we should never be appending - just creating N"); } return; } @@ -818,9 +536,9 @@ impl<'b> DiffState<'b> { // remove any old children that are not shared // todo: make this an iterator for child in old { - let key = child.key().unwrap(); + let key = child.key.unwrap(); if !shared_keys.contains(&key) { - self.remove_nodes([child], true); + self.remove_node(child, true); } } @@ -860,17 +578,18 @@ impl<'b> DiffState<'b> { let new_idx = idx + last + 1; let old_index = new_index_to_old_index[new_idx]; if old_index == u32::MAX as usize { - nodes_created += self.create_node(new_node); + nodes_created += self.create(new_node); } else { self.diff_node(&old[old_index], new_node); nodes_created += self.push_all_real_nodes(new_node); } } - self.mutations.insert_after( - self.find_last_element(&new[last]).unwrap(), - nodes_created as u32, - ); + let id = self.find_last_element(&new[last]); + self.mutations.push(Mutation::InsertAfter { + id, + m: nodes_created, + }); nodes_created = 0; } @@ -883,17 +602,18 @@ impl<'b> DiffState<'b> { let new_idx = idx + next + 1; let old_index = new_index_to_old_index[new_idx]; if old_index == u32::MAX as usize { - nodes_created += self.create_node(new_node); + nodes_created += self.create(new_node); } else { self.diff_node(&old[old_index], new_node); nodes_created += self.push_all_real_nodes(new_node); } } - self.mutations.insert_before( - self.find_first_element(&new[last]).unwrap(), - nodes_created as u32, - ); + let id = self.find_first_element(&new[last]); + self.mutations.push(Mutation::InsertBefore { + id, + m: nodes_created, + }); nodes_created = 0; } @@ -906,223 +626,357 @@ impl<'b> DiffState<'b> { for (idx, new_node) in new[..first_lis].iter().enumerate() { let old_index = new_index_to_old_index[idx]; if old_index == u32::MAX as usize { - nodes_created += self.create_node(new_node); + nodes_created += self.create(new_node); } else { self.diff_node(&old[old_index], new_node); nodes_created += self.push_all_real_nodes(new_node); } } - self.mutations.insert_before( - self.find_first_element(&new[first_lis]).unwrap(), - nodes_created as u32, - ); + let id = self.find_first_element(&new[first_lis]); + self.mutations.push(Mutation::InsertBefore { + id, + m: nodes_created, + }); } } - fn replace_node(&mut self, old: &'b VNode<'b>, new: &'b VNode<'b>) { - let nodes_created = self.create_node(new); - self.replace_inner(old, nodes_created); - } + /// Push all the real nodes on the stack + fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize { + node.template + .roots + .iter() + .enumerate() + .map(|(idx, _)| { + let node = match node.dynamic_root(idx) { + Some(node) => node, + None => { + self.mutations.push(Mutation::PushRoot { + id: node.root_ids[idx].get().unwrap(), + }); + return 1; + } + }; - fn replace_inner(&mut self, old: &'b VNode<'b>, nodes_created: usize) { - match old { - VNode::Element(el) => { - let id = old - .try_mounted_id() - .unwrap_or_else(|| panic!("broke on {:?}", old)); + match node { + Text(t) => { + self.mutations.push(Mutation::PushRoot { + id: t.id.get().unwrap(), + }); + 1 + } + Placeholder(t) => { + self.mutations.push(Mutation::PushRoot { + id: t.id.get().unwrap(), + }); + 1 + } + Fragment(nodes) => nodes + .iter() + .map(|node| self.push_all_real_nodes(node)) + .count(), - self.mutations.replace_with(id, nodes_created as u32); - self.remove_nodes(el.children, false); - self.scopes.collect_garbage(id); - } - - VNode::Text(_) | VNode::Placeholder(_) => { - let id = old - .try_mounted_id() - .unwrap_or_else(|| panic!("broke on {:?}", old)); - - self.mutations.replace_with(id, nodes_created as u32); - self.scopes.collect_garbage(id); - } - - VNode::Fragment(f) => { - self.replace_inner(&f.children[0], nodes_created); - self.remove_nodes(f.children.iter().skip(1), true); - } - - VNode::Component(c) => { - log::trace!("Replacing component {:?}", old); - let scope_id = c.scope.get().unwrap(); - let node = self.scopes.fin_head(scope_id); - - self.enter_scope(scope_id); - { - self.replace_inner(node, nodes_created); - - log::trace!("Replacing component x2 {:?}", old); - - let scope = self.scopes.get_scope(scope_id).unwrap(); - c.scope.set(None); - let props = scope.props.take().unwrap(); - c.props.borrow_mut().replace(props); - self.scopes.try_remove(scope_id); - } - self.leave_scope(); - } - } - } - - pub fn remove_nodes(&mut self, nodes: impl IntoIterator>, gen_muts: bool) { - for node in nodes { - match node { - VNode::Text(t) => { - // this check exists because our null node will be removed but does not have an ID - if let Some(id) = t.id.get() { - self.scopes.collect_garbage(id); - t.id.set(None); - - if gen_muts { - self.mutations.remove(id.as_u64()); + Component(comp) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node), + _ => todo!(), } } } - VNode::Placeholder(a) => { - let id = a.id.get().unwrap(); - self.scopes.collect_garbage(id); - a.id.set(None); + }) + .count() + } - if gen_muts { - self.mutations.remove(id.as_u64()); - } - } - VNode::Element(e) => { - let id = e.id.get().unwrap(); - - if gen_muts { - self.mutations.remove(id.as_u64()); - } - - self.scopes.collect_garbage(id); - e.id.set(None); - - self.remove_nodes(e.children, false); - } - - VNode::Fragment(f) => { - self.remove_nodes(f.children, gen_muts); - } - - VNode::Component(c) => { - self.enter_scope(c.scope.get().unwrap()); - { - let scope_id = c.scope.get().unwrap(); - let root = self.scopes.root_node(scope_id); - self.remove_nodes([root], gen_muts); - - let scope = self.scopes.get_scope(scope_id).unwrap(); - c.scope.set(None); - - let props = scope.props.take().unwrap(); - c.props.borrow_mut().replace(props); - self.scopes.try_remove(scope_id); - } - self.leave_scope(); + fn create_children(&mut self, nodes: impl IntoIterator>) -> usize { + nodes + .into_iter() + .fold(0, |acc, child| acc + self.create(child)) + } + + fn create_and_insert_before(&mut self, new: &'b [VNode<'b>], before: &'b VNode<'b>) { + let m = self.create_children(new); + let id = self.find_first_element(before); + self.mutations.push(Mutation::InsertBefore { id, m }) + } + + fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) { + let m = self.create_children(new); + let id = self.find_last_element(after); + self.mutations.push(Mutation::InsertAfter { id, m }) + } + + /// Simply replace a placeholder with a list of nodes + fn replace_placeholder(&mut self, l: &'b VPlaceholder, r: &'b [VNode<'b>]) { + let m = self.create_children(r); + let id = l.id.get().unwrap(); + self.mutations.push(Mutation::ReplaceWith { id, m }); + self.reclaim(id); + } + + fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator>) { + let m = self.create_children(right); + + let pre_edits = self.mutations.edits.len(); + + self.remove_node(left, 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 }), + _ => panic!("Expected remove mutation from remove_node"), + }; + } + + fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) { + // Create the placeholder first, ensuring we get a dedicated ID for the placeholder + let placeholder = self.next_element(&l[0], &[]); + + r.id.set(Some(placeholder)); + + self.mutations + .push(Mutation::CreatePlaceholder { id: placeholder }); + + self.remove_nodes(l); + + // 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"), + }; + } + + /// Remove these nodes from the dom + /// Wont generate mutations for the inner nodes + fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) { + nodes + .iter() + .rev() + .for_each(|node| self.remove_node(node, true)); + } + + fn remove_node(&mut self, node: &'b VNode<'b>, gen_muts: bool) { + // Clean up any attributes that have claimed a static node as dynamic for mount/unmounta + // Will not generate mutations! + self.reclaim_attributes(node); + + // Remove the nested dynamic nodes + // We don't generate mutations for these, as they will be removed by the parent (in the next line) + // But we still need to make sure to reclaim them from the arena and drop their hooks, etc + self.remove_nested_dyn_nodes(node); + + // Clean up the roots, assuming we need to generate mutations for these + // This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim) + self.reclaim_roots(node, gen_muts); + } + + fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) { + for (idx, _) in node.template.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(); + if gen_muts { + self.mutations.push(Mutation::Remove { id }); } + self.reclaim(id); } } } - fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { - let mut created = 0; - for node in nodes { - created += self.create_node(node); - } - created - } - - fn create_and_append_children(&mut self, nodes: &'b [VNode<'b>]) { - let created = self.create_children(nodes); - self.mutations.append_children(created as u32); - } - - fn create_and_insert_after(&mut self, nodes: &'b [VNode<'b>], after: &'b VNode<'b>) { - let created = self.create_children(nodes); - let last = self.find_last_element(after).unwrap(); - self.mutations.insert_after(last, created as u32); - } - - fn create_and_insert_before(&mut self, nodes: &'b [VNode<'b>], before: &'b VNode<'b>) { - let created = self.create_children(nodes); - let first = self.find_first_element(before).unwrap(); - self.mutations.insert_before(first, created as u32); - } - - fn current_scope(&self) -> ScopeId { - self.scope_stack.last().copied().expect("no current scope") - } - - fn enter_scope(&mut self, scope: ScopeId) { - self.scope_stack.push(scope); - } - - fn leave_scope(&mut self) { - self.scope_stack.pop(); - } - - fn find_last_element(&self, vnode: &'b VNode<'b>) -> Option { - let mut search_node = Some(vnode); - loop { - match &search_node.take().unwrap() { - VNode::Text(t) => break t.id.get(), - VNode::Element(t) => break t.id.get(), - VNode::Placeholder(t) => break t.id.get(), - VNode::Fragment(frag) => search_node = frag.children.last(), - VNode::Component(el) => { - let scope_id = el.scope.get().unwrap(); - search_node = Some(self.scopes.root_node(scope_id)); - } + fn reclaim_attributes(&mut self, node: &VNode) { + 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; } - } - } - fn find_first_element(&self, vnode: &'b VNode<'b>) -> Option { - let mut search_node = Some(vnode); - loop { - match &search_node.take().expect("search node to have an ID") { - VNode::Text(t) => break t.id.get(), - VNode::Element(t) => break t.id.get(), - VNode::Placeholder(t) => break t.id.get(), - VNode::Fragment(frag) => search_node = Some(&frag.children[0]), - VNode::Component(el) => { - let scope = el.scope.get().expect("element to have a scope assigned"); - search_node = Some(self.scopes.root_node(scope)); - } + let next_id = attr.mounted_element.get(); + + if id == Some(next_id) { + continue; } + + id = Some(next_id); + + self.reclaim(next_id); } } - // recursively push all the nodes of a tree onto the stack and return how many are there - fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize { + 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; + } + + self.remove_dynamic_node(dyn_node, false); + } + } + + fn remove_dynamic_node(&mut self, node: &DynamicNode, gen_muts: bool) { match node { - VNode::Text(_) | VNode::Placeholder(_) | VNode::Element(_) => { - self.mutations.push_root(node.mounted_id()); - 1 - } + Component(comp) => self.remove_component_node(comp, gen_muts), + Text(t) => self.remove_text_node(t, gen_muts), + Placeholder(t) => self.remove_placeholder(t, gen_muts), + Fragment(nodes) => nodes + .iter() + .for_each(|node| self.remove_node(node, gen_muts)), + }; + } - VNode::Fragment(frag) => { - let mut added = 0; - for child in frag.children { - added += self.push_all_real_nodes(child); + fn remove_placeholder(&mut self, t: &VPlaceholder, gen_muts: bool) { + if let Some(id) = t.id.take() { + if gen_muts { + self.mutations.push(Mutation::Remove { id }); + } + self.reclaim(id) + } + } + + fn remove_text_node(&mut self, t: &VText, gen_muts: bool) { + if let Some(id) = t.id.take() { + if gen_muts { + self.mutations.push(Mutation::Remove { id }); + } + self.reclaim(id) + } + } + + fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) { + let scope = comp.scope.take().unwrap(); + + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => { + println!("Removing component node sync {:?}", gen_muts); + self.remove_node(t, gen_muts) + } + _ => todo!("cannot handle nonstandard nodes"), + }; + + let props = self.scopes[scope.0].props.take(); + + self.dirty_scopes.remove(&DirtyScope { + height: self.scopes[scope.0].height, + id: scope, + }); + + *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); + } + + fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { + match node.dynamic_root(0) { + None => node.root_ids[0].get().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(Ok(t)) => self.find_first_element(t), + _ => todo!("cannot handle nonstandard nodes"), } - added } + } + } - VNode::Component(c) => { - let scope_id = c.scope.get().unwrap(); - let root = self.scopes.root_node(scope_id); - self.push_all_real_nodes(root) + 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(), + 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(Ok(t)) => self.find_last_element(t), + _ => todo!("cannot handle nonstandard nodes"), + } } } } } + +/// Are the templates the same? +/// +/// We need to check for the obvious case, and the non-obvious case where the template as cloned +/// +/// 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 +} + +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() { + return None; + } + + // run through the components, ensuring they're the same + left.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), + _ => return None, + }; + + let (l, r) = match (&left.dynamic_nodes[*l], &right.dynamic_nodes[*r]) { + (Component(l), Component(r)) => (l, r), + _ => return None, + }; + + Some((l, r)) + }) + .collect() +} + +/// We can apply various optimizations to dynamic nodes that are the single child of their parent. +/// +/// IE +/// - for text - we can use SetTextContent +/// - for clearning children we can use RemoveChildren +/// - 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]; + + // 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]; + + for i in 1..path.len() - 1 { + match static_node { + TemplateNode::Element { children, .. } => static_node = &children[path[i] as usize], + _ => return false, + } + } + + match static_node { + TemplateNode::Element { children, .. } => children.len() == 1, + _ => false, + } +} diff --git a/packages/core/src/dirty_scope.rs b/packages/core/src/dirty_scope.rs new file mode 100644 index 000000000..87fff3f45 --- /dev/null +++ b/packages/core/src/dirty_scope.rs @@ -0,0 +1,21 @@ +use std::hash::Hash; + +use crate::ScopeId; + +#[derive(Debug, Clone, Eq, PartialOrd, Ord)] +pub struct DirtyScope { + pub height: u32, + pub id: ScopeId, +} + +impl PartialEq for DirtyScope { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Hash for DirtyScope { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs new file mode 100644 index 000000000..6f40c2484 --- /dev/null +++ b/packages/core/src/error_boundary.rs @@ -0,0 +1,19 @@ +use std::cell::RefCell; + +use crate::ScopeId; + +/// A boundary that will capture any errors from child components +#[allow(dead_code)] +pub struct ErrorBoundary { + error: RefCell>, + id: ScopeId, +} + +impl ErrorBoundary { + pub fn new(id: ScopeId) -> Self { + Self { + error: RefCell::new(None), + id, + } + } +} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 0668f7582..9ab01e4d9 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,182 +1,165 @@ -//! Internal and external event system -//! -//! -//! This is all kinda WIP, but the bones are there. +use std::{ + cell::{Cell, RefCell}, + rc::Rc, +}; -use crate::{ElementId, ScopeId}; -use std::{any::Any, cell::Cell, fmt::Debug, rc::Rc, sync::Arc}; - -pub(crate) struct BubbleState { - pub canceled: Cell, +/// A wrapper around some generic data that handles the event's state +/// +/// +/// Prevent this event from continuing to bubble up the tree to parent elements. +/// +/// # Example +/// +/// ```rust, ignore +/// rsx! { +/// button { +/// onclick: move |evt: Event| { +/// evt.cancel_bubble(); +/// +/// } +/// } +/// } +/// ``` +pub struct Event { + /// The data associated with this event + pub data: Rc, + pub(crate) propogates: Rc>, } -impl BubbleState { - pub fn new() -> Self { +impl Event { + /// Prevent this event from continuing to bubble up the tree to parent elements. + /// + /// # Example + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// evt.cancel_bubble(); + /// } + /// } + /// } + /// ``` + #[deprecated = "use stop_propogation instead"] + pub fn cancel_bubble(&self) { + self.propogates.set(false); + } + + /// Prevent this event from continuing to bubble up the tree to parent elements. + /// + /// # Example + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// evt.cancel_bubble(); + /// } + /// } + /// } + /// ``` + pub fn stop_propogation(&self) { + self.propogates.set(false); + } + + /// Get a reference to the inner data from this event + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// let data = evt.inner.clone(); + /// cx.spawn(async move { + /// println!("{:?}", data); + /// }); + /// } + /// } + /// } + /// ``` + pub fn inner(&self) -> &Rc { + &self.data + } +} + +impl Clone for Event { + fn clone(&self) -> Self { Self { - canceled: Cell::new(false), + propogates: self.propogates.clone(), + data: self.data.clone(), } } } -/// User Events are events that are shuttled from the renderer into the [`VirtualDom`] through the scheduler channel. +impl std::ops::Deref for Event { + type Target = Rc; + fn deref(&self) -> &Self::Target { + &self.data + } +} + +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("data", &self.data) + .finish() + } +} + +/// The callback type generated by the `rsx!` macro when an `on` field is specified for components. /// -/// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree -/// where each listener is checked and fired if the event name matches. +/// This makes it possible to pass `move |evt| {}` style closures into components as property fields. /// -/// It is the expectation that the event name matches the corresponding event listener, otherwise Dioxus will panic in -/// attempting to downcast the event data. -/// -/// Because Event Data is sent across threads, it must be `Send + Sync`. We are hoping to lift the `Sync` restriction but -/// `Send` will not be lifted. The entire `UserEvent` must also be `Send + Sync` due to its use in the scheduler channel. /// /// # Example +/// /// ```rust, ignore -/// fn App(cx: Scope) -> Element { -/// render!(div { -/// onclick: move |_| println!("Clicked!") +/// rsx!{ +/// MyComponent { onclick: move |evt| log::info!("clicked") } +/// } +/// +/// #[derive(Props)] +/// struct MyProps<'a> { +/// onclick: EventHandler<'a, MouseEvent>, +/// } +/// +/// fn MyComponent(cx: Scope<'a, MyProps<'a>>) -> Element { +/// cx.render(rsx!{ +/// button { +/// onclick: move |evt| cx.props.onclick.call(evt), +/// } /// }) /// } /// -/// let mut dom = VirtualDom::new(App); -/// let mut scheduler = dom.get_scheduler_channel(); -/// scheduler.unbounded_send(SchedulerMsg::UiEvent( -/// UserEvent { -/// scope_id: None, -/// priority: EventPriority::Medium, -/// name: "click", -/// element: Some(ElementId(0)), -/// data: Arc::new(ClickEvent { .. }) -/// } -/// )).unwrap(); /// ``` -#[derive(Debug, Clone)] -pub struct UserEvent { - /// The originator of the event trigger if available - pub scope_id: Option, - - /// The priority of the event to be scheduled around ongoing work - pub priority: EventPriority, - - /// The optional real node associated with the trigger - pub element: Option, - - /// The event type IE "onclick" or "onmouseover" - pub name: &'static str, - - /// If the event is bubbles up through the vdom - pub bubbles: bool, - - /// The event data to be passed onto the event handler - pub data: Arc, +pub struct EventHandler<'bump, T = ()> { + pub(super) callback: RefCell>>, } -/// Priority of Event Triggers. -/// -/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus -/// won't be afraid to pause work or flush changes to the Real Dom. This is called "cooperative scheduling". Some Renderers -/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. -/// -/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. -/// -/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, -/// we keep it simple, and just use a 3-tier priority system. -/// -/// - `NoPriority` = 0 -/// - `LowPriority` = 1 -/// - `NormalPriority` = 2 -/// - `UserBlocking` = 3 -/// - `HighPriority` = 4 -/// - `ImmediatePriority` = 5 -/// -/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. -/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be -/// flushed before proceeding. Multiple discrete events is highly unlikely, though. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] -pub enum EventPriority { - /// Work that must be completed during the EventHandler phase. - /// - /// Currently this is reserved for controlled inputs. - Immediate = 3, - - /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. - /// - /// This is typically reserved for things like user interaction. - /// - /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). - High = 2, - - /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important - /// than "High Priority" events and will take precedence over low priority events. - /// - /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. - /// - /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). - Medium = 1, - - /// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be - /// advanced to the front of the work queue until completed. - /// - /// The primary user of Low Priority work is the asynchronous work system (Suspense). - /// - /// This is considered "idle" work or "background" work. - Low = 0, -} - -/// The internal Dioxus type that carries any event data to the relevant handler. - -pub struct AnyEvent { - pub(crate) bubble_state: Rc, - pub(crate) data: Arc, -} - -impl AnyEvent { - /// Convert this [`AnyEvent`] into a specific [`UiEvent`] with [`EventData`]. - /// - /// ```rust, ignore - /// let evt: FormEvent = evvt.downcast().unwrap(); - /// ``` - #[must_use] - pub fn downcast(self) -> Option> { - let AnyEvent { data, bubble_state } = self; - - data.downcast::() - .ok() - .map(|data| UiEvent { data, bubble_state }) +impl Default for EventHandler<'_, T> { + fn default() -> Self { + Self { + callback: Default::default(), + } } } -/// A [`UiEvent`] is a type that wraps various [`EventData`]. -/// -/// You should prefer to use the name of the event directly, rather than -/// the [`UiEvent`] generic type. -/// -/// For the HTML crate, this would include `MouseEvent`, `FormEvent` etc. -pub struct UiEvent { - /// The internal data of the event - /// This is wrapped in an Arc so that it can be sent across threads - pub data: Arc, +type ExternalListenerCallback<'bump, T> = bumpalo::boxed::Box<'bump, dyn FnMut(T) + 'bump>; - #[allow(unused)] - bubble_state: Rc, -} +impl EventHandler<'_, T> { + /// Call this event handler with the appropriate event type + /// + /// This borrows the event using a RefCell. Recursively calling a listener will cause a panic. + pub fn call(&self, event: T) { + if let Some(callback) = self.callback.borrow_mut().as_mut() { + callback(event); + } + } -impl std::fmt::Debug for UiEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("UiEvent").field("data", &self.data).finish() - } -} - -impl std::ops::Deref for UiEvent { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.data.as_ref() - } -} - -impl UiEvent { - /// Prevent this event from bubbling up the tree. - pub fn cancel_bubble(&self) { - self.bubble_state.canceled.set(true); + /// Forcibly drop the internal handler callback, releasing memory + /// + /// This will force any future calls to "call" to not doing anything + pub fn release(&self) { + self.callback.replace(None); } } diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs new file mode 100644 index 000000000..776d3e37a --- /dev/null +++ b/packages/core/src/fragment.rs @@ -0,0 +1,103 @@ +use crate::innerlude::*; + +/// Create inline fragments using Component syntax. +/// +/// ## Details +/// +/// Fragments capture a series of children without rendering extra nodes. +/// +/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and +/// a key is needed to identify each item. +/// +/// ## Example +/// +/// ```rust, ignore +/// rsx!{ +/// Fragment { key: "abc" } +/// } +/// ``` +/// +/// ## Usage +/// +/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. +/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments. +/// +/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro. +/// +/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. +#[allow(non_upper_case_globals, non_snake_case)] +pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { + let children = cx.props.0.as_ref().map_err(|e| anyhow::anyhow!("{}", e))?; + Ok(VNode { + key: children.key, + parent: children.parent, + template: children.template, + root_ids: children.root_ids, + dynamic_nodes: children.dynamic_nodes, + dynamic_attrs: children.dynamic_attrs, + }) +} + +pub struct FragmentProps<'a>(Element<'a>); +pub struct FragmentBuilder<'a, const BUILT: bool>(Element<'a>); +impl<'a> FragmentBuilder<'a, false> { + pub fn children(self, children: Element<'a>) -> FragmentBuilder<'a, true> { + FragmentBuilder(children) + } +} +impl<'a, const A: bool> FragmentBuilder<'a, A> { + pub fn build(self) -> FragmentProps<'a> { + FragmentProps(self.0) + } +} + +/// Access the children elements passed into the component +/// +/// This enables patterns where a component is passed children from its parent. +/// +/// ## Details +/// +/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions +/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure +/// on the props that takes Context. +/// +/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other +/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's +/// props are valid for the static lifetime. +/// +/// ## Example +/// +/// ```rust, ignore +/// fn App(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// CustomCard { +/// h1 {} +/// p {} +/// } +/// }) +/// } +/// +/// #[derive(PartialEq, Props)] +/// struct CardProps { +/// children: Element +/// } +/// +/// fn CustomCard(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// div { +/// h1 {"Title card"} +/// {cx.props.children} +/// } +/// }) +/// } +/// ``` +impl<'a> Properties for FragmentProps<'a> { + type Builder = FragmentBuilder<'a, false>; + const IS_STATIC: bool = false; + fn builder() -> Self::Builder { + FragmentBuilder(VNode::empty()) + } + unsafe fn memoize(&self, _other: &Self) -> bool { + false + } +} diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 23c74eca0..2b7c4feb3 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -3,7 +3,7 @@ //! This module provides support for a type called `LazyNodes` which is a micro-heap located on the stack to make calls //! to `rsx!` more efficient. //! -//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`NodeFactory`] closures. +//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`ScopeState`] closures. //! //! This can be done either through boxing directly, or by using dynamic-sized-types and a custom allocator. In our case, //! we build a tiny alloactor in the stack and allocate the closure into that. @@ -11,27 +11,25 @@ //! 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. -use crate::innerlude::{NodeFactory, VNode}; -use std::mem; +use smallbox::{smallbox, space::S16, SmallBox}; + +use crate::{innerlude::VNode, ScopeState}; /// A concrete type provider for closures that build [`VNode`] structures. /// /// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over -/// closures, but if we wrap the closure in a concrete type, we can maintain separate implementations of [`IntoVNode`]. +/// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching. /// /// /// ```rust, ignore /// LazyNodes::new(|f| f.element("div", [], [], [] None)) /// ``` pub struct LazyNodes<'a, 'b> { - inner: StackNodeStorage<'a, 'b>, -} + #[cfg(not(miri))] + inner: SmallBox VNode<'a> + 'b, S16>, -type StackHeapSize = [usize; 16]; - -enum StackNodeStorage<'a, 'b> { - Stack(LazyStack), - Heap(Box>) -> Option> + 'b>), + #[cfg(miri)] + inner: Box VNode<'a> + 'b>, } impl<'a, 'b> LazyNodes<'a, 'b> { @@ -40,118 +38,22 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// If the closure cannot fit into the stack allocation (16 bytes), then it /// is placed on the heap. Most closures will fit into the stack, and is /// the most optimal way to use the creation function. - pub fn new(val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self { + pub fn new(val: impl FnOnce(&'a ScopeState) -> VNode<'a> + 'b) -> Self { // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch let mut slot = Some(val); - let val = move |fac: Option>| { - fac.map( - slot.take() - .expect("LazyNodes closure to be called only once"), - ) - }; - - // miri does not know how to work with mucking directly into bytes - // just use a heap allocated type when miri is running - if cfg!(miri) { - Self { - inner: StackNodeStorage::Heap(Box::new(val)), - } - } else { - unsafe { LazyNodes::new_inner(val) } - } - } - - /// Create a new [`LazyNodes`] closure, but force it onto the heap. - pub fn new_boxed(inner: F) -> Self - where - F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b, - { - // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch - let mut slot = Some(inner); - Self { - inner: StackNodeStorage::Heap(Box::new(move |fac: Option>| { - fac.map( - slot.take() - .expect("LazyNodes closure to be called only once"), - ) - })), - } - } + #[cfg(not(miri))] + inner: smallbox!(move |f| { + let val = slot.take().expect("cannot call LazyNodes twice"); + val(f) + }), - unsafe fn new_inner(val: F) -> Self - where - F: FnMut(Option>) -> Option> + 'b, - { - let mut ptr: *const _ = &val as &dyn FnMut(Option>) -> Option>; - - assert_eq!( - ptr as *const u8, &val as *const _ as *const u8, - "MISUSE: Closure returned different pointer" - ); - assert_eq!( - std::mem::size_of_val(&*ptr), - std::mem::size_of::(), - "MISUSE: Closure returned a subset pointer" - ); - - let words = ptr_as_slice(&mut ptr); - assert!( - words[0] == &val as *const _ as usize, - "BUG: Pointer layout is not (data_ptr, info...)" - ); - - // - Ensure that Self is aligned same as data requires - assert!( - std::mem::align_of::() <= std::mem::align_of::(), - "TODO: Enforce alignment >{} (requires {})", - std::mem::align_of::(), - std::mem::align_of::() - ); - - let info = &words[1..]; - let data = words[0] as *mut (); - let size = mem::size_of::(); - - let stored_size = info.len() * mem::size_of::() + size; - let max_size = mem::size_of::(); - - if stored_size > max_size { - Self { - inner: StackNodeStorage::Heap(Box::new(val)), - } - } else { - let mut buf: StackHeapSize = StackHeapSize::default(); - - assert!(info.len() + round_to_words(size) <= buf.as_ref().len()); - - // Place pointer information at the end of the region - // - Allows the data to be at the start for alignment purposes - { - let info_ofs = buf.as_ref().len() - info.len(); - let info_dst = &mut buf.as_mut()[info_ofs..]; - for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) { - *d = *v; - } - } - - let src_ptr = data as *const u8; - let dataptr = buf.as_mut_ptr().cast::(); - - for i in 0..size { - *dataptr.add(i) = *src_ptr.add(i); - } - - std::mem::forget(val); - - Self { - inner: StackNodeStorage::Stack(LazyStack { - _align: [], - buf, - dropped: false, - }), - } + #[cfg(miri)] + inner: Box::new(move |f| { + let val = slot.take().expect("cannot call LazyNodes twice"); + val(f) + }), } } @@ -160,163 +62,10 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// ```rust, ignore /// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None)); /// - /// let fac = NodeFactory::new(&cx); - /// /// let node = f.call(cac); /// ``` #[must_use] - pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> { - match self.inner { - StackNodeStorage::Heap(mut lazy) => { - lazy(Some(f)).expect("Closure should not be called twice") - } - StackNodeStorage::Stack(mut stack) => stack.call(f), - } - } -} - -struct LazyStack { - _align: [u64; 0], - buf: StackHeapSize, - dropped: bool, -} - -impl LazyStack { - fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> { - let LazyStack { buf, .. } = self; - let data = buf.as_ref(); - - let info_size = - mem::size_of::<*mut dyn FnMut(Option>) -> Option>>() - / mem::size_of::() - - 1; - - let info_ofs = data.len() - info_size; - - let g: *mut dyn FnMut(Option>) -> Option> = - unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; - - self.dropped = true; - - let clos = unsafe { &mut *g }; - clos(Some(f)).unwrap() - } -} -impl Drop for LazyStack { - fn drop(&mut self) { - if !self.dropped { - let LazyStack { buf, .. } = self; - let data = buf.as_ref(); - - let info_size = mem::size_of::< - *mut dyn FnMut(Option>) -> Option>, - >() / mem::size_of::() - - 1; - - let info_ofs = data.len() - info_size; - - let g: *mut dyn FnMut(Option>) -> Option> = - unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; - - self.dropped = true; - - let clos = unsafe { &mut *g }; - clos(None); - } - } -} - -/// Obtain mutable access to a pointer's words -fn ptr_as_slice(ptr: &mut T) -> &mut [usize] { - assert!(mem::size_of::() % mem::size_of::() == 0); - let words = mem::size_of::() / mem::size_of::(); - // SAFE: Points to valid memory (a raw pointer) - unsafe { core::slice::from_raw_parts_mut(ptr as *mut _ as *mut usize, words) } -} - -/// Re-construct a fat pointer -unsafe fn make_fat_ptr(data_ptr: usize, meta_vals: &[usize]) -> *mut T { - let mut rv = mem::MaybeUninit::<*mut T>::uninit(); - { - let s = ptr_as_slice(&mut rv); - s[0] = data_ptr; - s[1..].copy_from_slice(meta_vals); - } - let rv = rv.assume_init(); - assert_eq!(rv as *const (), data_ptr as *const ()); - rv -} - -fn round_to_words(len: usize) -> usize { - (len + mem::size_of::() - 1) / mem::size_of::() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::innerlude::{Element, Scope, VirtualDom}; - - #[test] - fn it_works() { - fn app(cx: Scope<()>) -> Element { - cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) - } - - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - let g = dom.base_scope().root_node(); - dbg!(g); - } - - #[test] - fn it_drops() { - use std::rc::Rc; - - struct AppProps { - inner: Rc, - } - - fn app(cx: Scope) -> Element { - struct DropInner { - id: i32, - } - impl Drop for DropInner { - fn drop(&mut self) { - eprintln!("dropping inner"); - } - } - - let caller = { - let it = (0..10).map(|i| { - let val = cx.props.inner.clone(); - LazyNodes::new(move |f| { - eprintln!("hell closure"); - let inner = DropInner { id: i }; - f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) - }) - }); - - LazyNodes::new(|f| { - eprintln!("main closure"); - f.fragment_from_iter(it) - }) - }; - - cx.render(caller) - } - - let inner = Rc::new(0); - let mut dom = VirtualDom::new_with_props( - app, - AppProps { - inner: inner.clone(), - }, - ); - dom.rebuild(); - - drop(dom); - - assert_eq!(Rc::strong_count(&inner), 1); + pub fn call(mut self, f: &'a ScopeState) -> VNode<'a> { + (self.inner)(f) } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 4e497d690..7ce2b723f 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,33 +1,43 @@ -#![allow(non_snake_case)] #![doc = include_str!("../README.md")] -#![deny(missing_docs)] +#![warn(missing_docs)] -pub(crate) mod arbitrary_value; -pub(crate) mod diff; -pub(crate) mod events; -pub(crate) mod lazynodes; -pub(crate) mod mutations; -pub(crate) mod nodes; -pub(crate) mod properties; -pub(crate) mod scopes; -pub(crate) mod util; -pub(crate) mod virtual_dom; +mod any_props; +mod arena; +mod bump_frame; +mod create; +mod diff; +mod dirty_scope; +mod error_boundary; +mod events; +mod fragment; +mod lazynodes; +mod mutations; +mod nodes; +mod properties; +mod scheduler; +mod scope_arena; +mod scopes; +mod virtual_dom; pub(crate) mod innerlude { - pub use crate::arbitrary_value::*; + pub use crate::arena::*; + pub use crate::dirty_scope::*; + pub use crate::error_boundary::*; pub use crate::events::*; + pub use crate::fragment::*; pub use crate::lazynodes::*; pub use crate::mutations::*; + pub use crate::nodes::RenderReturn; pub use crate::nodes::*; pub use crate::properties::*; + pub use crate::scheduler::*; pub use crate::scopes::*; - pub use crate::util::*; pub use crate::virtual_dom::*; - /// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`]. + /// An [`Element`] is a possibly-errored [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`]. /// - /// Any [`None`] [`Element`] will automatically be coerced into a placeholder [`VNode`] with the [`VNode::Placeholder`] variant. - pub type Element<'a> = Option>; + /// An Errored [`Element`] will propagate the error to the nearest error boundary. + pub type Element<'a> = Result, anyhow::Error>; /// A [`Component`] is a function that takes a [`Scope`] and returns an [`Element`]. /// @@ -57,16 +67,13 @@ pub(crate) mod innerlude { /// ) /// ``` pub type Component

= fn(Scope

) -> Element; - - /// A list of attributes - pub type Attributes<'a> = Option<&'a [Attribute<'a>]>; } pub use crate::innerlude::{ - AnyEvent, Attribute, AttributeValue, Component, DioxusElement, DomEdit, Element, ElementId, - ElementIdIterator, EventHandler, EventPriority, IntoVNode, LazyNodes, Listener, Mutations, - NodeFactory, Properties, SchedulerMsg, Scope, ScopeId, ScopeState, TaskId, UiEvent, UserEvent, - VComponent, VElement, VFragment, VNode, VPlaceholder, VText, VirtualDom, + 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, }; /// The purpose of this module is to alleviate imports of many common types @@ -74,8 +81,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, Attributes, Component, DioxusElement, Element, EventHandler, Fragment, - LazyNodes, NodeFactory, Properties, Scope, ScopeId, ScopeState, VNode, VirtualDom, + fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties, + Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, + VNode, VirtualDom, }; } @@ -83,37 +91,4 @@ pub mod exports { //! Important dependencies that are used by the rest of the library //! Feel free to just add the dependencies in your own Crates.toml pub use bumpalo; - pub use futures_channel; -} - -/// Functions that wrap unsafe functionality to prevent us from misusing it at the callsite -pub(crate) mod unsafe_utils { - use crate::VNode; - - pub(crate) unsafe fn extend_vnode<'a, 'b>(node: &'a VNode<'a>) -> &'b VNode<'b> { - std::mem::transmute(node) - } -} - -#[macro_export] -/// A helper macro for using hooks in async environements. -/// -/// # Usage -/// -/// -/// ```ignore -/// let (data) = use_ref(&cx, || {}); -/// -/// let handle_thing = move |_| { -/// to_owned![data] -/// cx.spawn(async move { -/// // do stuff -/// }); -/// } -/// ``` -macro_rules! to_owned { - ($($es:ident),+) => {$( - #[allow(unused_mut)] - let mut $es = $es.to_owned(); - )*} } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 4218e613f..f8553cd46 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,350 +1,257 @@ -//! Instructions returned by the VirtualDOM on how to modify the Real DOM. -//! -//! This module contains an internal API to generate these instructions. -//! -//! Beware that changing code in this module will break compatibility with -//! interpreters for these types of DomEdits. +use rustc_hash::FxHashSet; -use crate::innerlude::*; -use std::{any::Any, fmt::Debug}; +use crate::{arena::ElementId, ScopeId, Template}; -/// ## Mutations +/// A container for all the relevant steps to modify the Real DOM /// -/// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also -/// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has -/// applied the edits. +/// This object provides a bunch of important information for a renderer to use patch the Real Dom with the state of the +/// VirtualDom. This includes the scopes that were modified, the templates that were discovered, and a list of changes +/// in the form of a [`Mutation`]. +/// +/// These changes are specific to one subtree, so to patch multiple subtrees, you'd need to handle each set separately. +/// +/// Templates, however, apply to all subtrees, not just target subtree. /// /// Mutations are the only link between the RealDOM and the VirtualDOM. +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +#[derive(Debug, Default)] +#[must_use = "not handling edits can lead to visual inconsistencies in UI"] pub struct Mutations<'a> { - /// The list of edits that need to be applied for the RealDOM to match the VirtualDOM. - pub edits: Vec>, + /// The ID of the subtree that these edits are targetting + pub subtree: usize, /// The list of Scopes that were diffed, created, and removed during the Diff process. pub dirty_scopes: FxHashSet, - /// The list of nodes to connect to the RealDOM. - pub refs: Vec>, + /// Any templates encountered while diffing the DOM. + /// + /// These must be loaded into a cache before applying the edits + pub templates: Vec>, + + /// Any mutations required to patch the renderer to match the layout of the VirtualDom + pub edits: Vec>, } -impl Debug for Mutations<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Mutations") - .field("edits", &self.edits) - .field("noderefs", &self.refs) - .finish() +impl<'a> Mutations<'a> { + /// Rewrites IDs to just be "template", so you can compare the mutations + /// + /// Used really only for testing + pub fn santize(mut self) -> Self { + for edit in self.edits.iter_mut() { + if let Mutation::LoadTemplate { name, .. } = edit { + *name = "template" + } + } + + self + } + + /// Push a new mutation into the dom_edits list + pub(crate) fn push(&mut self, mutation: Mutation<'static>) { + self.edits.push(mutation) } } -/// A `DomEdit` represents a serialized form of the VirtualDom's trait-based API. This allows streaming edits across the -/// network or through FFI boundaries. -#[derive(Debug, PartialEq)] +/// A `Mutation` represents a single instruction for the renderer to use to modify the UI tree to match the state +/// of the Dioxus VirtualDom. +/// +/// These edits can be serialized and sent over the network or through any interface #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), serde(tag = "type") )] -pub enum DomEdit<'bump> { - /// Push the given root node onto our stack. - PushRoot { - /// The ID of the root node to push. - root: u64, - }, - - /// Pop the topmost node from our stack and append them to the node - /// at the top of the stack. +#[derive(Debug, PartialEq, Eq)] +pub enum Mutation<'a> { + /// Add these m children to the target element AppendChildren { - /// How many nodes should be popped from the stack. - /// The node remaining on the stack will be the target for the append. - many: u32, + /// The ID of the element being mounted to + id: ElementId, + + /// The number of nodes on the stack + m: usize, }, - /// Replace a given (single) node with a handful of nodes currently on the stack. - ReplaceWith { - /// The ID of the node to be replaced. - root: u64, + /// Assign the element at the given path the target ElementId. + /// + /// The path is in the form of a list of indices based on children. Templates cannot have more than 255 children per + /// element, hence the use of a single byte. + /// + /// + AssignId { + /// The path of the child of the topmost node on the stack + /// + /// A path of `[]` represents the topmost node. A path of `[0]` represents the first child. + /// `[0,1,2]` represents 1st child's 2nd child's 3rd child. + path: &'static [u8], - /// How many nodes should be popped from the stack to replace the target node. - m: u32, + /// The ID we're assigning to this element/placeholder. + /// + /// This will be used later to modify the element or replace it with another element. + id: ElementId, + }, + + /// Create an placeholder int he DOM that we will use later. + /// + /// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing + CreatePlaceholder { + /// The ID we're assigning to this element/placeholder. + /// + /// This will be used later to modify the element or replace it with another element. + id: ElementId, + }, + + /// Create a node specifically for text with the given value + CreateTextNode { + /// The text content of this text node + value: &'a str, + + /// The ID we're assigning to this specific text nodes + /// + /// This will be used later to modify the element or replace it with another element. + id: ElementId, + }, + + /// Hydrate an existing text node at the given path with the given text. + /// + /// Assign this text node the given ID since we will likely need to modify this text at a later point + HydrateText { + /// The path of the child of the topmost node on the stack + /// + /// A path of `[]` represents the topmost node. A path of `[0]` represents the first child. + /// `[0,1,2]` represents 1st child's 2nd child's 3rd child. + path: &'static [u8], + + /// The value of the textnode that we want to set the placeholder with + value: &'a str, + + /// The ID we're assigning to this specific text nodes + /// + /// This will be used later to modify the element or replace it with another element. + id: ElementId, + }, + + /// Load and clone an existing node from a template saved under that specific name + /// + /// Dioxus guarantees that the renderer will have already been provided the template. + /// When the template is picked up in the template list, it should be saved under its "name" - here, the name + LoadTemplate { + /// The "name" of the template. When paired with `rsx!`, this is autogenerated + name: &'static str, + + /// Which root are we loading from the template? + /// + /// The template is stored as a list of nodes. This index represents the position of that root + index: usize, + + /// The ID we're assigning to this element being loaded from the template + /// + /// This will be used later to move the element around in lists + id: ElementId, + }, + + /// Replace the target element (given by its ID) with the topmost m nodes on the stack + ReplaceWith { + /// The ID of the node we're going to replace with + id: ElementId, + + /// The number of nodes on the stack to use to replace + m: usize, + }, + + /// Replace an existing element in the template at the given path with the m nodes on the stack + ReplacePlaceholder { + /// The path of the child of the topmost node on the stack + /// + /// A path of `[]` represents the topmost node. A path of `[0]` represents the first child. + /// `[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 + m: usize, }, /// Insert a number of nodes after a given node. InsertAfter { /// The ID of the node to insert after. - root: u64, + id: ElementId, - /// How many nodes should be popped from the stack to insert after the target node. - n: u32, + /// The ids of the nodes to insert after the target node. + m: usize, }, /// Insert a number of nodes before a given node. InsertBefore { /// The ID of the node to insert before. - root: u64, + id: ElementId, - /// How many nodes should be popped from the stack to insert before the target node. - n: u32, + /// The ids of the nodes to insert before the target node. + m: usize, }, - /// Remove a particular node from the DOM - Remove { - /// The ID of the node to remove. - root: u64, + /// Set the value of a node's attribute. + SetAttribute { + /// The name of the attribute to set. + name: &'a str, + /// The value of the attribute. + value: &'a str, + + /// The ID of the node to set the attribute of. + id: ElementId, + + /// The (optional) namespace of the attribute. + /// For instance, "style" is in the "style" namespace. + ns: Option<&'a str>, }, - /// Create a new purely-text node - CreateTextNode { - /// The ID the new node should have. - root: u64, + /// 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 - text: &'bump str, - }, + value: &'a str, - /// Create a new purely-element node - CreateElement { - /// The ID the new node should have. - root: u64, - - /// The tagname of the node - tag: &'bump str, - }, - - /// Create a new purely-comment node with a given namespace - CreateElementNs { - /// The ID the new node should have. - root: u64, - - /// The namespace of the node - tag: &'bump str, - - /// The namespace of the node (like `SVG`) - ns: &'static str, - }, - - /// Create a new placeholder node. - /// In most implementations, this will either be a hidden div or a comment node. - CreatePlaceholder { - /// The ID the new node should have. - root: u64, + /// The ID of the node to set the textcontent of. + id: ElementId, }, /// Create a new Event Listener. NewEventListener { /// The name of the event to listen for. - event_name: &'static str, + name: &'a str, /// The ID of the node to attach the listener to. - scope: ScopeId, - - /// The ID of the node to attach the listener to. - root: u64, + id: ElementId, }, /// Remove an existing Event Listener. RemoveEventListener { - /// The ID of the node to remove. - root: u64, - /// The name of the event to remove. - event: &'static str, - }, + name: &'a str, - /// Set the textcontent of a node. - SetText { - /// The ID of the node to set the textcontent of. - root: u64, - - /// The textcontent of the node - text: &'bump str, - }, - - /// Set the value of a node's attribute. - SetAttribute { - /// The ID of the node to set the attribute of. - root: u64, - - /// The name of the attribute to set. - field: &'static str, - - /// The value of the attribute. - value: AttributeValue<'bump>, - - // value: &'bump str, - /// The (optional) namespace of the attribute. - /// For instance, "style" is in the "style" namespace. - ns: Option<&'bump str>, - }, - - /// Remove an attribute from a node. - RemoveAttribute { /// The ID of the node to remove. - root: u64, - - /// The name of the attribute to remove. - name: &'static str, - - /// The namespace of the attribute. - ns: Option<&'bump str>, + id: ElementId, }, - /// Manually pop a root node from the stack. - PopRoot {}, -} - -use fxhash::FxHashSet; -use DomEdit::*; - -impl<'a> Mutations<'a> { - pub(crate) fn new() -> Self { - Self { - edits: Vec::new(), - refs: Vec::new(), - dirty_scopes: Default::default(), - } - } - - // Navigation - pub(crate) fn push_root(&mut self, root: ElementId) { - let id = root.as_u64(); - self.edits.push(PushRoot { root: id }); - } - - // Navigation - pub(crate) fn pop_root(&mut self) { - self.edits.push(PopRoot {}); - } - - pub(crate) fn replace_with(&mut self, root: ElementId, m: u32) { - let root = root.as_u64(); - self.edits.push(ReplaceWith { m, root }); - } - - pub(crate) fn insert_after(&mut self, root: ElementId, n: u32) { - let root = root.as_u64(); - self.edits.push(InsertAfter { n, root }); - } - - pub(crate) fn insert_before(&mut self, root: ElementId, n: u32) { - let root = root.as_u64(); - self.edits.push(InsertBefore { n, root }); - } - - pub(crate) fn append_children(&mut self, n: u32) { - self.edits.push(AppendChildren { many: n }); - } - - // Remove Nodes from the dom - pub(crate) fn remove(&mut self, id: u64) { - self.edits.push(Remove { root: id }); - } - - // Create - pub(crate) fn create_text_node(&mut self, text: &'a str, id: ElementId) { - let id = id.as_u64(); - self.edits.push(CreateTextNode { text, root: id }); - } - - pub(crate) fn create_element( - &mut self, - tag: &'static str, - ns: Option<&'static str>, + /// Remove a particular node from the DOM + Remove { + /// The ID of the node to remove. id: ElementId, - ) { - let id = id.as_u64(); - match ns { - Some(ns) => self.edits.push(CreateElementNs { root: id, ns, tag }), - None => self.edits.push(CreateElement { root: id, tag }), - } - } - // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom - pub(crate) fn create_placeholder(&mut self, id: ElementId) { - let id = id.as_u64(); - self.edits.push(CreatePlaceholder { root: id }); - } + }, - // events - pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) { - let Listener { - event, - mounted_node, - .. - } = listener; - - let element_id = mounted_node.get().unwrap().as_u64(); - - self.edits.push(NewEventListener { - scope, - event_name: event, - root: element_id, - }); - } - pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: u64) { - self.edits.push(RemoveEventListener { event, root }); - } - - // modify - pub(crate) fn set_text(&mut self, text: &'a str, root: u64) { - self.edits.push(SetText { text, root }); - } - - pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute<'a>, root: u64) { - let Attribute { - name, - value, - namespace, - .. - } = attribute; - - self.edits.push(SetAttribute { - field: name, - value: value.clone(), - ns: *namespace, - root, - }); - } - - pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) { - let Attribute { - name, namespace, .. - } = attribute; - - self.edits.push(RemoveAttribute { - name, - ns: *namespace, - root, - }); - } - - pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) { - self.dirty_scopes.insert(scope); - } -} - -// refs are only assigned once -pub struct NodeRefMutation<'a> { - pub element: &'a mut Option>>, - pub element_id: ElementId, -} - -impl<'a> std::fmt::Debug for NodeRefMutation<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NodeRefMutation") - .field("element_id", &self.element_id) - .finish() - } -} - -impl<'a> NodeRefMutation<'a> { - pub fn downcast_ref(&self) -> Option<&T> { - self.element - .as_ref() - .and_then(|f| f.get()) - .and_then(|f| f.downcast_ref::()) - } - pub fn downcast_mut(&mut self) -> Option<&mut T> { - self.element - .as_mut() - .and_then(|f| f.get_mut()) - .and_then(|f| f.downcast_mut::()) - } + /// Push the given root node onto our stack. + PushRoot { + /// The ID of the root node to push. + id: ElementId, + }, } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 8bd91fdb1..288b15271 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,862 +1,609 @@ -//! Virtual Node Support -//! -//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers. These VNodes should be *very* -//! cheap and *very* fast to construct - building a full tree should be quick. use crate::{ - innerlude::{AttributeValue, ComponentPtr, Element, Properties, Scope, ScopeId, ScopeState}, - lazynodes::LazyNodes, - AnyEvent, Component, + any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState, }; -use bumpalo::{boxed::Box as BumpBox, Bump}; +use bumpalo::boxed::Box as BumpBox; +use bumpalo::Bump; use std::{ + any::{Any, TypeId}, cell::{Cell, RefCell}, - fmt::{Arguments, Debug, Formatter}, + fmt::Arguments, + future::Future, }; -/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM. +pub type TemplateId = &'static str; + +/// The actual state of the component's most recent computation /// -/// VNodes are designed to be lightweight and used with with a bump allocator. To create a VNode, you can use either of: +/// Because Dioxus accepts components in the form of `async fn(Scope) -> Result`, we need to support both +/// sync and async versions. /// -/// - the `rsx!` macro -/// - the [`NodeFactory`] API -pub enum VNode<'src> { - /// Text VNodes are simply bump-allocated (or static) string slices - /// - /// # Example - /// - /// ```rust, ignore - /// let mut vdom = VirtualDom::new(); - /// let node = vdom.render_vnode(rsx!( "hello" )); - /// - /// if let VNode::Text(vtext) = node { - /// assert_eq!(vtext.text, "hello"); - /// assert_eq!(vtext.dom_id.get(), None); - /// assert_eq!(vtext.is_static, true); - /// } - /// ``` - Text(&'src VText<'src>), +/// Dioxus will do its best to immediately resolve any async components into a regular Element, but as an implementor +/// 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>), - /// Element VNodes are VNodes that may contain attributes, listeners, a key, a tag, and children. - /// - /// # Example - /// - /// ```rust, ignore - /// let mut vdom = VirtualDom::new(); - /// - /// let node = vdom.render_vnode(rsx!{ - /// div { - /// key: "a", - /// onclick: |e| log::info!("clicked"), - /// hidden: "true", - /// style: { background_color: "red" }, - /// "hello" - /// } - /// }); - /// - /// if let VNode::Element(velement) = node { - /// assert_eq!(velement.tag_name, "div"); - /// assert_eq!(velement.namespace, None); - /// assert_eq!(velement.key, Some("a")); - /// } - /// ``` - Element(&'src VElement<'src>), - - /// Fragment nodes may contain many VNodes without a single root. - /// - /// # Example - /// - /// ```rust, ignore - /// rsx!{ - /// a {} - /// link {} - /// style {} - /// "asd" - /// Example {} - /// } - /// ``` - Fragment(&'src VFragment<'src>), - - /// Component nodes represent a mounted component with props, children, and a key. - /// - /// # Example - /// - /// ```rust, ignore - /// fn Example(cx: Scope) -> Element { - /// ... - /// } - /// - /// let mut vdom = VirtualDom::new(); - /// - /// let node = vdom.render_vnode(rsx!( Example {} )); - /// - /// if let VNode::Component(vcomp) = node { - /// assert_eq!(vcomp.user_fc, Example as *const ()); - /// } - /// ``` - Component(&'src VComponent<'src>), - - /// Placeholders are a type of placeholder VNode used when fragments don't contain any children. - /// - /// Placeholders cannot be directly constructed via public APIs. - /// - /// # Example - /// - /// ```rust, ignore - /// let mut vdom = VirtualDom::new(); - /// - /// let node = vdom.render_vnode(rsx!( Fragment {} )); - /// - /// if let VNode::Fragment(frag) = node { - /// let root = &frag.children[0]; - /// assert_eq!(root, VNode::Anchor); - /// } - /// ``` - Placeholder(&'src VPlaceholder), + /// An ongoing future that will resolve to a [`Element`] + Async(BumpBox<'a, dyn Future> + 'a>), } -impl<'src> VNode<'src> { - /// Get the VNode's "key" used in the keyed diffing algorithm. - pub fn key(&self) -> Option<&'src str> { - match &self { - VNode::Element(el) => el.key, - VNode::Component(c) => c.key, - VNode::Fragment(f) => f.key, - VNode::Text(_t) => None, - VNode::Placeholder(_f) => None, - } - } - - /// Get the ElementID of the mounted VNode. - /// - /// Panics if the mounted ID is None or if the VNode is not represented by a single Element. - pub fn mounted_id(&self) -> ElementId { - self.try_mounted_id().unwrap() - } - - /// Try to get the ElementID of the mounted VNode. - /// - /// Returns None if the VNode is not mounted, or if the VNode cannot be presented by a mounted ID (Fragment/Component) - pub fn try_mounted_id(&self) -> Option { - match &self { - VNode::Text(el) => el.id.get(), - VNode::Element(el) => el.id.get(), - VNode::Placeholder(el) => el.id.get(), - VNode::Fragment(_) => None, - VNode::Component(_) => None, - } - } - - // Create an "owned" version of the vnode. - pub(crate) fn decouple(&self) -> VNode<'src> { - match *self { - VNode::Text(t) => VNode::Text(t), - VNode::Element(e) => VNode::Element(e), - VNode::Component(c) => VNode::Component(c), - VNode::Placeholder(a) => VNode::Placeholder(a), - VNode::Fragment(f) => VNode::Fragment(f), - } - } -} - -impl Debug for VNode<'_> { - fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - match &self { - VNode::Element(el) => s - .debug_struct("VNode::Element") - .field("name", &el.tag) - .field("key", &el.key) - .field("attrs", &el.attributes) - .field("children", &el.children) - .field("id", &el.id) - .finish(), - VNode::Text(t) => s - .debug_struct("VNode::Text") - .field("text", &t.text) - .field("id", &t.id) - .finish(), - VNode::Placeholder(t) => s - .debug_struct("VNode::Placholder") - .field("id", &t.id) - .finish(), - VNode::Fragment(frag) => s - .debug_struct("VNode::Fragment") - .field("children", &frag.children) - .finish(), - VNode::Component(comp) => s - .debug_struct("VNode::Component") - .field("name", &comp.fn_name) - .field("fnptr", &comp.user_fc) - .field("key", &comp.key) - .field("scope", &comp.scope) - .finish(), - } - } -} - -/// An Element's unique identifier. +/// A reference to a template along with any context needed to hydrate it /// -/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is -/// unmounted, then the `ElementId` will be reused for a new component. -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ElementId(pub usize); -impl std::fmt::Display for ElementId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl ElementId { - /// Convertt the ElementId to a `u64`. - pub fn as_u64(self) -> u64 { - self.0 as u64 - } -} - -fn empty_cell() -> Cell> { - Cell::new(None) -} - -/// A placeholder node only generated when Fragments don't have any children. -pub struct VPlaceholder { - /// The [`ElementId`] of the placeholder. - pub id: Cell>, -} - -/// A bump-allocated string slice and metadata. -pub struct VText<'src> { - /// The [`ElementId`] of the VText. - pub id: Cell>, - - /// The text of the VText. - pub text: &'src str, - - /// An indiciation if this VText can be ignored during diffing - /// Is usually only when there are no strings to be formatted (so the text is &'static str) - pub is_static: bool, -} - -/// A list of VNodes with no single root. -pub struct VFragment<'src> { - /// The key of the fragment to be used during keyed diffing. - pub key: Option<&'src str>, - - /// Fragments can never have zero children. Enforced by NodeFactory. +/// The dynamic parts of the template are stored separately from the static parts. This allows faster diffing by skipping +/// static parts of the template. +#[derive(Debug, Clone)] +pub struct VNode<'a> { + /// The key given to the root of this template. /// - /// You *can* make a fragment with no children, but it's not a valid fragment and your VDom will panic. - pub children: &'src [VNode<'src>], -} - -/// An element like a "div" with children, listeners, and attributes. -pub struct VElement<'a> { - /// The [`ElementId`] of the VText. - pub id: Cell>, - - /// The key of the element to be used during keyed diffing. + /// In fragments, this is the key of the first child. In other cases, it is the key of the root. pub key: Option<&'a str>, - /// The tag name of the element. - /// - /// IE "div" - pub tag: &'static str, + /// When rendered, this template will be linked to its parent manually + pub parent: Option, - /// The namespace of the VElement - /// - /// IE "svg" - pub namespace: Option<&'static str>, + /// The static nodes and static descriptor of the template + pub template: Template<'static>, - /// The parent of the Element (if any). - /// - /// Used when bubbling events - pub parent: 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>], - /// The Listeners of the VElement. - pub listeners: &'a [Listener<'a>], + /// The dynamic parts of the template + pub dynamic_nodes: &'a [DynamicNode<'a>], - /// The attributes of the VElement. - pub attributes: &'a [Attribute<'a>], - - /// The children of the VElement. - pub children: &'a [VNode<'a>], + /// The dynamic parts of the template + pub dynamic_attrs: &'a [Attribute<'a>], } -impl Debug for VElement<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("VElement") - .field("tag_name", &self.tag) - .field("namespace", &self.namespace) - .field("key", &self.key) - .field("id", &self.id) - .field("parent", &self.parent) - .field("listeners", &self.listeners.len()) - .field("attributes", &self.attributes) - .field("children", &self.children) +impl<'a> VNode<'a> { + /// Create a template with no nodes that will be skipped over during diffing + pub fn empty() -> Element<'a> { + Ok(VNode { + key: None, + parent: None, + root_ids: &[], + dynamic_nodes: &[], + dynamic_attrs: &[], + template: Template { + name: "dioxus-empty", + roots: &[], + node_paths: &[], + attr_paths: &[], + }, + }) + } + + /// Load a dynamic root at the given index + /// + /// 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] { + 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. +/// +/// This is the core innovation in Dioxus. Most UIs are made of static nodes, yet participate in diffing like any +/// dynamic node. This struct can be created at compile time. It promises that its name is unique, allow Dioxus to use +/// its static description of the UI to skip immediately to the dynamic nodes during diffing. +/// +/// 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))] +#[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. + 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. + 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. + 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. + pub attr_paths: &'a [&'a [u8]], +} + +impl<'a> Template<'a> { + /// Is this template worth caching at all, since it's completely runtime? + /// + /// There's no point in saving templates that are completely dynamic, since they'll be recreated every time anyway. + pub fn is_completely_dynamic(&self) -> bool { + use TemplateNode::*; + self.roots + .iter() + .all(|root| matches!(root, Dynamic { .. } | DynamicText { .. })) + } +} + +/// A statically known node in a layout. +/// +/// 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"))] +pub enum TemplateNode<'a> { + /// An statically known element in the dom. + /// + /// In HTML this would be something like `

` + Element { + /// The name of the element + /// + /// IE for a div, it would be the string "div" + tag: &'a str, + + /// The namespace of the element + /// + /// In HTML, this would be a valid URI that defines a namespace for all elements below it + /// SVG is an example of this namespace + namespace: Option<&'a str>, + + /// 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"`. + attrs: &'a [TemplateAttribute<'a>], + + /// A list of template nodes that define another set of template nodes + children: &'a [TemplateNode<'a>], + }, + + /// This template node is just a piece of static text + Text { + /// The actual text + text: &'a str, + }, + + /// This template node is unknown, and needs to be created at runtime. + Dynamic { + /// The index of the dynamic node in the VNode's dynamic_nodes list + id: usize, + }, + + /// This template node is known to be some text, but needs to be created at runtime + /// + /// This is separate from the pure Dynamic variant for various optimizations + DynamicText { + /// The index of the dynamic node in the VNode's dynamic_nodes list + id: usize, + }, +} + +/// A node created at runtime +/// +/// This node's index in the DynamicNode list on VNode should match its repsective `Dynamic` index +#[derive(Debug)] +pub enum DynamicNode<'a> { + /// A component node + /// + /// Most of the time, Dioxus will actually know which component this is as compile time, but the props and + /// assigned scope are dynamic. + /// + /// The actual VComponent can be dynamic between two VNodes, though, allowing implementations to swap + /// the render function at runtime + Component(VComponent<'a>), + + /// A text node + Text(VText<'a>), + + /// A placeholder + /// + /// Used by suspense when a node isn't ready and by fragments that don't render anything + /// + /// In code, this is just an ElementId whose initial value is set to 0 upon creation + Placeholder(VPlaceholder), + + /// A list of VNodes. + /// + /// Note that this is not a list of dynamic nodes. These must be VNodes and created through conditional rendering + /// or iterators. + Fragment(&'a [VNode<'a>]), +} + +impl Default for DynamicNode<'_> { + fn default() -> Self { + Self::Placeholder(Default::default()) + } +} + +/// An instance of a child component +pub struct VComponent<'a> { + /// The name of this component + pub name: &'static str, + + /// Are the props valid for the 'static lifetime? + /// + /// Internally, this is used as a guarantee. Externally, this might be incorrect, so don't count on it. + /// + /// This flag is assumed by the [`crate::Properties`] trait which is unsafe to implement + pub static_props: bool, + + /// The assigned Scope for this component + pub scope: Cell>, + + /// The function pointer of the component, known at compile time + /// + /// It is possible that components get folded at comppile time, so these shouldn't be really used as a key + pub render_fn: *const (), + + pub(crate) props: RefCell + 'a>>>, +} + +impl<'a> std::fmt::Debug for VComponent<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VComponent") + .field("name", &self.name) + .field("static_props", &self.static_props) + .field("scope", &self.scope) .finish() } } -/// A trait for any generic Dioxus Element. -/// -/// This trait provides the ability to use custom elements in the `rsx!` macro. -/// -/// ```rust, ignore -/// struct my_element; -/// -/// impl DioxusElement for my_element { -/// const TAG_NAME: "my_element"; -/// const NAME_SPACE: None; -/// } -/// -/// let _ = rsx!{ -/// my_element {} -/// }; -/// ``` -pub trait DioxusElement { - /// The tag name of the element. - const TAG_NAME: &'static str; +/// An instance of some text, mounted to the DOM +#[derive(Debug)] +pub struct VText<'a> { + /// The actual text itself + pub value: &'a str, - /// The namespace of the element. - const NAME_SPACE: Option<&'static str>; - - /// The tag name of the element. - #[inline] - fn tag_name(&self) -> &'static str { - Self::TAG_NAME - } - - /// The namespace of the element. - #[inline] - fn namespace(&self) -> Option<&'static str> { - Self::NAME_SPACE - } + /// The ID of this node in the real DOM + pub id: Cell>, } -/// An attribute on a DOM node, such as `id="my-thing"` or -/// `href="https://example.com"`. -#[derive(Clone, Debug)] +/// A placeholder node, used by suspense and fragments +#[derive(Debug, Default)] +pub struct VPlaceholder { + /// The ID of this node in the real DOM + pub id: Cell>, +} + +/// An attribute of the TemplateNode, created at compile time +#[derive(Debug, PartialEq, Hash, Eq, PartialOrd, Ord)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] +pub enum TemplateAttribute<'a> { + /// This attribute is entirely known at compile time, enabling + Static { + /// The name of this attribute. + /// + /// For example, the `href` attribute in `href="https://example.com"`, would have the name "href" + name: &'a str, + + /// The value of this attribute, known at compile time + /// + /// Currently this only accepts &str, so values, even if they're known at compile time, are not known + value: &'a str, + + /// The namespace of this attribute. Does not exist in the HTML spec + namespace: Option<&'a str>, + }, + + /// The attribute in this position is actually determined dynamically at runtime + /// + /// This is the index into the dynamic_attributes field on the container VNode + Dynamic { + /// The index + id: usize, + }, +} + +/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"` +#[derive(Debug)] pub struct Attribute<'a> { /// The name of the attribute. - pub name: &'static str, + pub name: &'a str, - /// The value of the attribute. + /// The value of the attribute pub value: AttributeValue<'a>, - /// An indication if this attribute can be ignored during diffing - /// - /// Usually only when there are no strings to be formatted (so the value is &'static str) - pub is_static: bool, - - /// An indication of we should always try and set the attribute. - /// Used in controlled components to ensure changes are propagated. - pub is_volatile: bool, - /// The namespace of the attribute. /// - /// Doesn't exist in the html spec. - /// Used in Dioxus to denote "style" tags and other attribute groups. + /// Doesn’t exist in the html spec. Used in Dioxus to denote “style” tags and other attribute groups. pub namespace: Option<&'static str>, + + /// The element in the DOM that this attribute belongs to + pub mounted_element: Cell, + + /// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated + pub volatile: bool, } -/// An event listener. -/// IE onclick, onkeydown, etc -pub struct Listener<'bump> { - /// The ID of the node that this listener is mounted to - /// Used to generate the event listener's ID on the DOM - pub mounted_node: Cell>, +/// Any of the built-in values that the Dioxus VirtualDom supports as dynamic attributes on elements +/// +/// These are built-in to be faster during the diffing process. To use a custom value, use the [`AttributeValue::Any`] +/// variant. +pub enum AttributeValue<'a> { + /// Text attribute + Text(&'a str), - /// The type of event to listen for. + /// A float + Float(f64), + + /// Signed integer + Int(i64), + + /// Boolean + Bool(bool), + + /// A listener, like "onclick" + Listener(RefCell>>), + + /// An arbitrary value that implements PartialEq and is static + Any(BumpBox<'a, dyn AnyValue>), + + /// A "none" value, resulting in the removal of an attribute from the dom + None, +} + +type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event) + 'a>; + +impl<'a> std::fmt::Debug for AttributeValue<'a> { + 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::Listener(_) => f.debug_tuple("Listener").finish(), + Self::Any(_) => f.debug_tuple("Any").finish(), + Self::None => write!(f, "None"), + } + } +} + +impl<'a> PartialEq for AttributeValue<'a> { + 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::Listener(_), Self::Listener(_)) => true, + (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(r0.as_ref()), + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + +#[doc(hidden)] +pub trait AnyValue { + fn any_cmp(&self, other: &dyn AnyValue) -> bool; + fn our_typeid(&self) -> TypeId; +} + +impl AnyValue for T { + fn any_cmp(&self, other: &dyn AnyValue) -> bool { + if self.type_id() != other.our_typeid() { + return false; + } + + self == unsafe { &*(other as *const _ as *const T) } + } + + fn our_typeid(&self) -> TypeId { + self.type_id() + } +} + +#[doc(hidden)] +pub trait ComponentReturn<'a, A = ()> { + fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a>; +} + +impl<'a> ComponentReturn<'a> for Element<'a> { + fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> { + RenderReturn::Sync(self) + } +} + +#[doc(hidden)] +pub struct AsyncMarker; +impl<'a, F> ComponentReturn<'a, AsyncMarker> for F +where + F: Future> + 'a, +{ + 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) }) + } +} + +impl<'a> RenderReturn<'a> { + pub(crate) unsafe fn extend_lifetime_ref<'c>(&self) -> &'c RenderReturn<'c> { + unsafe { std::mem::transmute(self) } + } + pub(crate) unsafe fn extend_lifetime<'c>(self) -> RenderReturn<'c> { + unsafe { std::mem::transmute(self) } + } +} + +/// A trait that allows various items to be converted into a dynamic node for the rsx macro +pub trait IntoDynNode<'a, A = ()> { + /// Consume this item along with a scopestate and produce a DynamicNode /// - /// IE "click" - whatever the renderer needs to attach the listener by name. - pub event: &'static str, - - /// The actual callback that the user specified - pub(crate) callback: InternalHandler<'bump>, + /// You can use the bump alloactor of the scopestate to creat the dynamic node + fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>; } -pub type InternalHandler<'bump> = &'bump RefCell>>; -type InternalListenerCallback<'bump> = BumpBox<'bump, dyn FnMut(AnyEvent) + 'bump>; -type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; - -/// 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. -/// -/// -/// # Example -/// -/// ```rust, ignore -/// -/// rsx!{ -/// MyComponent { onclick: move |evt| log::info!("clicked"), } -/// } -/// -/// #[derive(Props)] -/// struct MyProps<'a> { -/// onclick: EventHandler<'a, MouseEvent>, -/// } -/// -/// fn MyComponent(cx: Scope<'a, MyProps<'a>>) -> Element { -/// cx.render(rsx!{ -/// button { -/// onclick: move |evt| cx.props.onclick.call(evt), -/// } -/// }) -/// } -/// -/// ``` -pub struct EventHandler<'bump, T = ()> { - /// The (optional) callback that the user specified - /// Uses a `RefCell` to allow for interior mutability, and FnMut closures. - pub callback: RefCell>>, +impl<'a> IntoDynNode<'a> for () { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + DynamicNode::default() + } } - -impl<'a, T> Default for EventHandler<'a, T> { - fn default() -> Self { - Self { - callback: RefCell::new(None), - } +impl<'a> IntoDynNode<'a> for VNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + DynamicNode::Fragment(_cx.bump().alloc([self])) } } -impl EventHandler<'_, T> { - /// Call this event handler with the appropriate event type - pub fn call(&self, event: T) { - if let Some(callback) = self.callback.borrow_mut().as_mut() { - callback(event); - } - } - - /// Forcibly drop the internal handler callback, releasing memory - pub fn release(&self) { - self.callback.replace(None); - } -} - -/// Virtual Components for custom user-defined components -/// Only supports the functional syntax -pub struct VComponent<'src> { - /// The key of the component to be used during keyed diffing. - pub key: Option<&'src str>, - - /// The ID of the component. - /// Will not be assigned until after the component has been initialized. - pub scope: Cell>, - - /// An indication if the component is static (can be memozied) - pub can_memoize: bool, - - /// The function pointer to the component's render function. - pub user_fc: ComponentPtr, - - /// The actual name of the component. - pub fn_name: &'static str, - - /// The props of the component. - pub props: RefCell>>, -} - -pub(crate) struct VComponentProps

{ - pub render_fn: Component

, - pub memo: unsafe fn(&P, &P) -> bool, - pub props: P, -} - -pub trait AnyProps { - fn as_ptr(&self) -> *const (); - fn render<'a>(&'a self, bump: &'a ScopeState) -> Element<'a>; - unsafe fn memoize(&self, other: &dyn AnyProps) -> bool; -} - -impl

AnyProps for VComponentProps

{ - fn as_ptr(&self) -> *const () { - &self.props as *const _ as *const () - } - - // Safety: - // this will downcast the other ptr as our swallowed type! - // you *must* make this check *before* calling this method - // if your functions are not the same, then you will downcast a pointer into a different type (UB) - unsafe fn memoize(&self, other: &dyn AnyProps) -> bool { - let real_other: &P = &*(other.as_ptr() as *const _ as *const P); - let real_us: &P = &*(self.as_ptr() as *const _ as *const P); - (self.memo)(real_us, real_other) - } - - fn render<'a>(&'a self, scope: &'a ScopeState) -> Element<'a> { - let props = unsafe { std::mem::transmute::<&P, &P>(&self.props) }; - (self.render_fn)(Scope { scope, props }) - } -} - -/// This struct provides an ergonomic API to quickly build VNodes. -/// -/// NodeFactory is used to build VNodes in the component's memory space. -/// This struct adds metadata to the final VNode about listeners, attributes, and children -#[derive(Copy, Clone)] -pub struct NodeFactory<'a> { - pub(crate) scope: &'a ScopeState, - pub(crate) bump: &'a Bump, -} - -impl<'a> NodeFactory<'a> { - /// Create a new [`NodeFactory`] from a [`Scope`] or [`ScopeState`] - pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> { - NodeFactory { - scope, - bump: &scope.wip_frame().bump, - } - } - - /// Get the custom allocator for this component - #[inline] - pub fn bump(&self) -> &'a bumpalo::Bump { - self.bump - } - - /// Directly pass in text blocks without the need to use the format_args macro. - pub fn static_text(&self, text: &'static str) -> VNode<'a> { - VNode::Text(self.bump.alloc(VText { - id: empty_cell(), - text, - is_static: true, - })) - } - - /// Parses a lazy text Arguments and returns a string and a flag indicating if the text is 'static - /// - /// Text that's static may be pointer compared, making it cheaper to diff - pub fn raw_text(&self, args: Arguments) -> (&'a str, bool) { - match args.as_str() { - Some(static_str) => (static_str, true), - None => { - use bumpalo::core_alloc::fmt::Write; - let mut str_buf = bumpalo::collections::String::new_in(self.bump); - str_buf.write_fmt(args).unwrap(); - (str_buf.into_bump_str(), false) - } - } - } - - /// Create some text that's allocated along with the other vnodes - /// - pub fn text(&self, args: Arguments) -> VNode<'a> { - let (text, is_static) = self.raw_text(args); - - VNode::Text(self.bump.alloc(VText { - text, - is_static, - id: empty_cell(), - })) - } - - /// Create a new [`VNode::Element`] - pub fn element( - &self, - el: impl DioxusElement, - listeners: &'a [Listener<'a>], - attributes: &'a [Attribute<'a>], - children: &'a [VNode<'a>], - key: Option, - ) -> VNode<'a> { - self.raw_element( - el.tag_name(), - el.namespace(), - listeners, - attributes, - children, - key, - ) - } - - /// Create a new [`VNode::Element`] without the trait bound - /// - /// IE pass in "div" instead of `div` - pub fn raw_element( - &self, - tag_name: &'static str, - namespace: Option<&'static str>, - listeners: &'a [Listener<'a>], - attributes: &'a [Attribute<'a>], - children: &'a [VNode<'a>], - key: Option, - ) -> VNode<'a> { - let key = key.map(|f| self.raw_text(f).0); - - let mut items = self.scope.items.borrow_mut(); - for listener in listeners { - let long_listener = unsafe { std::mem::transmute(listener) }; - items.listeners.push(long_listener); - } - - VNode::Element(self.bump.alloc(VElement { - tag: tag_name, - key, - namespace, - listeners, - attributes, - children, - id: empty_cell(), - parent: empty_cell(), - })) - } - - /// Create a new [`Attribute`] - pub fn attr( - &self, - name: &'static str, - val: Arguments, - namespace: Option<&'static str>, - is_volatile: bool, - ) -> Attribute<'a> { - let (value, is_static) = self.raw_text(val); - Attribute { - name, - value: AttributeValue::Text(value), - is_static, - namespace, - is_volatile, - } - } - - /// Create a new [`Attribute`] using non-arguments - pub fn custom_attr( - &self, - name: &'static str, - value: AttributeValue<'a>, - namespace: Option<&'static str>, - is_volatile: bool, - is_static: bool, - ) -> Attribute<'a> { - Attribute { - name, - value, - is_static, - namespace, - is_volatile, - } - } - - /// Create a new [`VNode::Component`] - pub fn component

( - &self, - component: fn(Scope<'a, P>) -> Element, - props: P, - key: Option, - fn_name: &'static str, - ) -> VNode<'a> - where - P: Properties + 'a, - { - let vcomp = self.bump.alloc(VComponent { - key: key.map(|f| self.raw_text(f).0), - scope: Default::default(), - can_memoize: P::IS_STATIC, - user_fc: component as ComponentPtr, - fn_name, - props: RefCell::new(Some(Box::new(VComponentProps { - props, - memo: P::memoize, // smuggle the memoization function across borders - - // i'm sorry but I just need to bludgeon the lifetimes into place here - // this is safe because we're managing all lifetimes to originate from previous calls - // the intricacies of Rust's lifetime system make it difficult to properly express - // the transformation from this specific lifetime to the for<'a> lifetime - render_fn: unsafe { std::mem::transmute(component) }, - }))), - }); - - if !P::IS_STATIC { - let vcomp = &*vcomp; - let vcomp = unsafe { std::mem::transmute(vcomp) }; - self.scope.items.borrow_mut().borrowed_props.push(vcomp); - } - - VNode::Component(vcomp) - } - - /// Create a new [`Listener`] - pub fn listener(self, event: &'static str, callback: InternalHandler<'a>) -> Listener<'a> { - Listener { - event, - mounted_node: Cell::new(None), - callback, - } - } - - /// Create a new [`VNode::Fragment`] from a root of the rsx! call - pub fn fragment_root<'b, 'c>( - self, - node_iter: impl IntoIterator + 'c> + 'b, - ) -> VNode<'a> { - let mut nodes = bumpalo::collections::Vec::new_in(self.bump); - - for node in node_iter { - nodes.push(node.into_vnode(self)); - } - - if nodes.is_empty() { - VNode::Placeholder(self.bump.alloc(VPlaceholder { id: empty_cell() })) - } else { - VNode::Fragment(self.bump.alloc(VFragment { - children: nodes.into_bump_slice(), - key: None, - })) - } - } - - /// Create a new [`VNode::Fragment`] from any iterator - pub fn fragment_from_iter<'c, I, J>( - self, - node_iter: impl IntoVNode<'a, I, J> + 'c, - ) -> VNode<'a> { - node_iter.into_vnode(self) - } - - /// Create a new [`VNode`] from any iterator of children - pub fn create_children( - self, - node_iter: impl IntoIterator>, - ) -> Element<'a> { - let mut nodes = bumpalo::collections::Vec::new_in(self.bump); - - for node in node_iter { - nodes.push(node.into_vnode(self)); - } - - if nodes.is_empty() { - Some(VNode::Placeholder( - self.bump.alloc(VPlaceholder { id: empty_cell() }), - )) - } else { - let children = nodes.into_bump_slice(); - - Some(VNode::Fragment(self.bump.alloc(VFragment { - children, - key: None, - }))) - } - } - - /// Create a new [`EventHandler`] from an [`FnMut`] - pub fn event_handler(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> { - let handler: &mut dyn FnMut(T) = self.bump.alloc(f); - let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; - let callback = RefCell::new(Some(caller)); - EventHandler { callback } - } -} - -impl Debug for NodeFactory<'_> { - fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Ok(()) - } -} - -/// Trait implementations for use in the rsx! and html! macros. -/// -/// ## Details -/// -/// This section provides convenience methods and trait implementations for converting common structs into a format accepted -/// by the macros. -/// -/// All dynamic content in the macros must flow in through `fragment_from_iter`. Everything else must be statically layed out. -/// We pipe basically everything through `fragment_from_iter`, so we expect a very specific type: -/// ```rust, ignore -/// impl IntoIterator> -/// ``` -/// -/// As such, all node creation must go through the factory, which is only available in the component context. -/// These strict requirements make it possible to manage lifetimes and state. -pub trait IntoVNode<'a, I = (), J = ()> { - /// Convert this into a [`VNode`], using the [`NodeFactory`] as a source of allocation - fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a>; -} - -// TODO: do we even need this? It almost seems better not to -// // For the case where a rendered VNode is passed into the rsx! macro through curly braces -impl<'a> IntoVNode<'a> for VNode<'a> { - fn into_vnode(self, _: NodeFactory<'a>) -> VNode<'a> { +impl<'a> IntoDynNode<'a> for DynamicNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { self } } -// Conveniently, we also support "null" (nothing) passed in -impl IntoVNode<'_> for () { - fn into_vnode(self, cx: NodeFactory) -> VNode { - VNode::Placeholder(cx.bump.alloc(VPlaceholder { id: empty_cell() })) +// An element that's an error is currently lost into the ether +impl<'a> IntoDynNode<'a> for Element<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + match self { + Ok(val) => val.into_vnode(_cx), + _ => DynamicNode::default(), + } } } -impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> { - fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { +impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + match self { + Some(val) => val.into_vnode(_cx), + None => DynamicNode::default(), + } + } +} + +impl<'a> IntoDynNode<'a> for &Element<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + match self.as_ref() { + Ok(val) => val.clone().into_vnode(_cx), + _ => DynamicNode::default(), + } + } +} + +impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { + fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { + DynamicNode::Fragment(cx.bump().alloc([self.call(cx)])) + } +} + +impl<'a> IntoDynNode<'_> for &'a str { + fn into_vnode(self, cx: &ScopeState) -> DynamicNode { + cx.text_node(format_args!("{}", self)) + } +} + +impl IntoDynNode<'_> for String { + fn into_vnode(self, cx: &ScopeState) -> DynamicNode { + cx.text_node(format_args!("{}", self)) + } +} + +impl<'b> IntoDynNode<'b> for Arguments<'_> { + fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { + cx.text_node(self) + } +} + +impl<'a> IntoDynNode<'a> for &'a VNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + DynamicNode::Fragment(_cx.bump().alloc([VNode { + parent: self.parent, + template: self.template, + root_ids: self.root_ids, + key: self.key, + dynamic_nodes: self.dynamic_nodes, + dynamic_attrs: self.dynamic_attrs, + }])) + } +} + +pub trait IntoTemplate<'a> { + fn into_template(self, _cx: &'a ScopeState) -> VNode<'a>; +} +impl<'a> IntoTemplate<'a> for VNode<'a> { + fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { + self + } +} +impl<'a> IntoTemplate<'a> for Element<'a> { + fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { + match self { + Ok(val) => val.into_template(_cx), + _ => VNode::empty().unwrap(), + } + } +} +impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> { + fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { self.call(cx) } } -impl<'b> IntoVNode<'_> for &'b str { - fn into_vnode(self, cx: NodeFactory) -> VNode { - cx.text(format_args!("{}", self)) - } -} - -impl IntoVNode<'_> for String { - fn into_vnode(self, cx: NodeFactory) -> VNode { - cx.text(format_args!("{}", self)) - } -} - -impl IntoVNode<'_> for Arguments<'_> { - fn into_vnode(self, cx: NodeFactory) -> VNode { - cx.text(self) - } -} - -impl<'a> IntoVNode<'a> for &VNode<'a> { - fn into_vnode(self, _cx: NodeFactory<'a>) -> VNode<'a> { - // borrowed nodes are strange - self.decouple() - } -} - // 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, E> IntoVNode<'a, FromNodeIterator, E> for T +impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T where - T: IntoIterator, - I: IntoVNode<'a, E>, + T: Iterator, + I: IntoTemplate<'a>, { - fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { - let mut nodes = bumpalo::collections::Vec::new_in(cx.bump); + fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { + let mut nodes = bumpalo::collections::Vec::new_in(cx.bump()); - for node in self { - nodes.push(node.into_vnode(cx)); - } + nodes.extend(self.into_iter().map(|node| node.into_template(cx))); - if nodes.is_empty() { - VNode::Placeholder(cx.bump.alloc(VPlaceholder { id: empty_cell() })) - } else { - let children = nodes.into_bump_slice(); - - if cfg!(debug_assertions) - && children.len() > 1 - && children.last().unwrap().key().is_none() - { - // todo: make the backtrace prettier or remove it altogether - log::error!( - r#" - Warning: Each child in an array or iterator should have a unique "key" prop. - Not providing a key will lead to poor performance with lists. - See docs.rs/dioxus for more information. - ------------- - {:?} - "#, - backtrace::Backtrace::new() - ); - } - - VNode::Fragment(cx.bump.alloc(VFragment { - children, - key: None, - })) + match nodes.into_bump_slice() { + children if children.is_empty() => DynamicNode::default(), + children => DynamicNode::Fragment(children), } } } + +/// A value that can be converted into an attribute value +pub trait IntoAttributeValue<'a> { + /// Convert into an attribute value + fn into_value(self, bump: &'a Bump) -> AttributeValue<'a>; +} + +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; + let mut str_buf = bumpalo::collections::String::new_in(bump); + str_buf.write_fmt(self).unwrap(); + AttributeValue::Text(str_buf.into_bump_str()) + } +} diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index e138acaa6..f8be8a0dd 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -1,100 +1,5 @@ use crate::innerlude::*; -pub struct FragmentProps<'a>(Element<'a>); -pub struct FragmentBuilder<'a, const BUILT: bool>(Element<'a>); -impl<'a> FragmentBuilder<'a, false> { - pub fn children(self, children: Element<'a>) -> FragmentBuilder<'a, true> { - FragmentBuilder(children) - } -} -impl<'a, const A: bool> FragmentBuilder<'a, A> { - pub fn build(self) -> FragmentProps<'a> { - FragmentProps(self.0) - } -} - -/// Access the children elements passed into the component -/// -/// This enables patterns where a component is passed children from its parent. -/// -/// ## Details -/// -/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions -/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure -/// on the props that takes Context. -/// -/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other -/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's -/// props are valid for the static lifetime. -/// -/// ## Example -/// -/// ```rust, ignore -/// fn App(cx: Scope) -> Element { -/// cx.render(rsx!{ -/// CustomCard { -/// h1 {} -/// p {} -/// } -/// }) -/// } -/// -/// #[derive(PartialEq, Props)] -/// struct CardProps { -/// children: Element -/// } -/// -/// fn CustomCard(cx: Scope) -> Element { -/// cx.render(rsx!{ -/// div { -/// h1 {"Title card"} -/// {cx.props.children} -/// } -/// }) -/// } -/// ``` -impl<'a> Properties for FragmentProps<'a> { - type Builder = FragmentBuilder<'a, false>; - const IS_STATIC: bool = false; - fn builder() -> Self::Builder { - FragmentBuilder(None) - } - unsafe fn memoize(&self, _other: &Self) -> bool { - false - } -} - -/// Create inline fragments using Component syntax. -/// -/// ## Details -/// -/// Fragments capture a series of children without rendering extra nodes. -/// -/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and -/// a key is needed to identify each item. -/// -/// ## Example -/// -/// ```rust, ignore -/// rsx!{ -/// Fragment { key: "abc" } -/// } -/// ``` -/// -/// ## Usage -/// -/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. -/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments. -/// -/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro. -/// -/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. -#[allow(non_upper_case_globals, non_snake_case)] -pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { - let i = cx.props.0.as_ref().map(|f| f.decouple()); - cx.render(LazyNodes::new(|f| f.fragment_from_iter(i))) -} - /// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus /// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the /// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe. @@ -165,6 +70,6 @@ impl EmptyBuilder { /// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern /// to initialize a component's props. -pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element) -> T::Builder { +pub fn fc_to_builder<'a, A, T: Properties + 'a>(_: fn(Scope<'a, T>) -> A) -> T::Builder { T::builder() } diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs new file mode 100644 index 000000000..a6442184d --- /dev/null +++ b/packages/core/src/scheduler/mod.rs @@ -0,0 +1,48 @@ +use crate::ScopeId; +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. +/// +/// These messages control how the scheduler will process updates to the UI. +#[derive(Debug)] +pub(crate) enum SchedulerMsg { + /// Immediate updates from Components that mark them as dirty + Immediate(ScopeId), + + /// A task has woken and needs to be progressed + TaskNotified(TaskId), + + /// A task has woken and needs to be progressed + SuspenseNotified(SuspenseId), +} + +use std::{cell::RefCell, rc::Rc}; + +pub(crate) struct Scheduler { + pub sender: futures_channel::mpsc::UnboundedSender, + + /// Tasks created with cx.spawn + pub tasks: RefCell>>, + + /// Async components + pub leaves: RefCell>>, +} + +impl Scheduler { + pub fn new(sender: futures_channel::mpsc::UnboundedSender) -> Rc { + Rc::new(Scheduler { + sender, + tasks: RefCell::new(Slab::new()), + leaves: RefCell::new(Slab::new()), + }) + } +} diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs new file mode 100644 index 000000000..8d7d7337e --- /dev/null +++ b/packages/core/src/scheduler/suspense.rs @@ -0,0 +1,52 @@ +use super::{waker::RcWake, SchedulerMsg}; +use crate::ElementId; +use crate::{innerlude::Mutations, Element, ScopeId}; +use std::future::Future; +use std::{ + cell::{Cell, RefCell}, + collections::HashSet, + rc::Rc, +}; + +/// An ID representing an ongoing suspended component +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub(crate) struct SuspenseId(pub usize); + +/// A boundary in the VirtualDom that captures all suspended components below it +pub struct SuspenseContext { + pub(crate) id: ScopeId, + pub(crate) waiting_on: RefCell>, + pub(crate) mutations: RefCell>, + pub(crate) placeholder: Cell>, + pub(crate) created_on_stack: Cell, +} + +impl SuspenseContext { + /// Create a new boundary for suspense + pub fn new(id: ScopeId) -> Self { + Self { + id, + waiting_on: Default::default(), + mutations: RefCell::new(Mutations::default()), + placeholder: Cell::new(None), + created_on_stack: Cell::new(0), + } + } +} + +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>, +} + +impl RcWake for SuspenseLeaf { + fn wake_by_ref(arc_self: &Rc) { + arc_self.notified.set(true); + _ = 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 new file mode 100644 index 000000000..fb4378104 --- /dev/null +++ b/packages/core/src/scheduler/task.rs @@ -0,0 +1,66 @@ +use super::{waker::RcWake, Scheduler, SchedulerMsg}; +use crate::ScopeId; +use std::cell::RefCell; +use std::future::Future; +use std::{pin::Pin, rc::Rc}; + +/// A task's unique identifier. +/// +/// `TaskId` is a `usize` that is unique across the entire VirtualDOM and across time. TaskIDs will never be reused +/// once a Task has been completed. +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct TaskId(pub usize); + +/// the task itself is the waker +pub(crate) struct LocalTask { + pub scope: ScopeId, + pub(super) task: RefCell + 'static>>>, + id: TaskId, + tx: futures_channel::mpsc::UnboundedSender, +} + +impl Scheduler { + /// Start a new future on the same thread as the rest of the VirtualDom. + /// + /// This future will not contribute to suspense resolving, so you should primarily use this for reacting to changes + /// and long running tasks. + /// + /// Whenever the component that owns this future is dropped, the future will be dropped as well. + /// + /// Spawning a future onto the root scope will cause it to be dropped when the root component is dropped - which + /// 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(), + task: RefCell::new(Box::pin(task)), + scope, + })); + + self.sender + .unbounded_send(SchedulerMsg::TaskNotified(task_id)) + .expect("Scheduler should exist"); + + task_id + } + + /// 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 + pub fn remove(&self, id: TaskId) { + self.tasks.borrow_mut().remove(id.0); + } +} + +impl RcWake for LocalTask { + fn wake_by_ref(arc_self: &Rc) { + _ = 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 new file mode 100644 index 000000000..8ae320e70 --- /dev/null +++ b/packages/core/src/scheduler/wait.rs @@ -0,0 +1,110 @@ +use futures_util::FutureExt; +use std::{ + rc::Rc, + task::{Context, Poll}, +}; + +use crate::{ + innerlude::{Mutation, Mutations, SuspenseContext}, + nodes::RenderReturn, + ScopeId, TaskId, VNode, VirtualDom, +}; + +use super::{waker::RcWake, SuspenseId}; + +impl VirtualDom { + /// Handle notifications by tasks inside the scheduler + /// + /// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the + /// queue + pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) { + let mut tasks = self.scheduler.tasks.borrow_mut(); + + let task = match tasks.get(id.0) { + Some(task) => task, + None => return, + }; + + let waker = task.waker(); + let mut cx = Context::from_waker(&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); + + // Remove it from the scheduler + tasks.remove(id.0); + } + } + + pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc { + self.scopes[id.0] + .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 scope_id = leaf.scope_id; + + // todo: cache the waker + let waker = leaf.waker(); + let mut cx = Context::from_waker(&waker); + + // Safety: the future is always pinned to the bump arena + let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; + let as_pinned_mut = &mut pinned; + + // the component finished rendering and gave us nodes + // 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 arena = scope.current_frame(); + + let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes)); + arena.node.set(ret); + + fiber.waiting_on.borrow_mut().remove(&id); + + if let RenderReturn::Sync(Ok(template)) = ret { + let mutations_ref = &mut fiber.mutations.borrow_mut(); + let mutations = &mut **mutations_ref; + let template: &VNode = unsafe { std::mem::transmute(template) }; + let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) }; + + std::mem::swap(&mut self.mutations, mutations); + + let place_holder_id = scope.placeholder.get().unwrap(); + self.scope_stack.push(scope_id); + let created = self.create(template); + self.scope_stack.pop(); + mutations.push(Mutation::ReplaceWith { + id: place_holder_id, + m: created, + }); + + for leaf in self.collected_leaves.drain(..) { + fiber.waiting_on.borrow_mut().insert(leaf); + } + + std::mem::swap(&mut self.mutations, mutations); + + if fiber.waiting_on.borrow().is_empty() { + self.finished_fibers.push(fiber.id); + } + } + } + } +} diff --git a/packages/core/src/scheduler/waker.rs b/packages/core/src/scheduler/waker.rs new file mode 100644 index 000000000..762020167 --- /dev/null +++ b/packages/core/src/scheduler/waker.rs @@ -0,0 +1,36 @@ +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 new file mode 100644 index 000000000..9c0f96db3 --- /dev/null +++ b/packages/core/src/scope_arena.rs @@ -0,0 +1,155 @@ +use crate::{ + any_props::AnyProps, + bump_frame::BumpFrame, + innerlude::DirtyScope, + innerlude::{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, + task::{Context, Poll}, +}; + +impl VirtualDom { + pub(super) fn new_scope( + &mut self, + props: Box>, + name: &'static str, + ) -> &ScopeState { + 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()); + + entry.insert(Box::new(ScopeState { + parent, + id, + height, + name, + props: Some(props), + tasks: self.scheduler.clone(), + placeholder: Default::default(), + node_arena_1: BumpFrame::new(0), + node_arena_2: BumpFrame::new(0), + spawned_tasks: Default::default(), + render_cnt: Default::default(), + hook_arena: Default::default(), + hook_list: Default::default(), + hook_idx: Default::default(), + shared_contexts: Default::default(), + borrowed_props: Default::default(), + listeners: 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 _)) + } + + pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { + // Cycle to the next frame and then reset it + // This breaks any latent references, invalidating every pointer referencing into it. + // Remove all the outdated listeners + self.ensure_drop_safety(scope_id); + + let mut new_nodes = unsafe { + let scope = self.scopes[scope_id.0].as_mut(); + + // 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(); + } + + // 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 { + let mut leaves = self.scheduler.leaves.borrow_mut(); + + let entry = leaves.vacant_entry(); + let suspense_id = SuspenseId(entry.key()); + + let leaf = Rc::new(SuspenseLeaf { + scope_id, + task: task.as_mut(), + id: suspense_id, + tx: self.scheduler.sender.clone(), + notified: Default::default(), + }); + + let waker = leaf.waker(); + let mut cx = Context::from_waker(&waker); + + // safety: the task is already pinned in the bump arena + let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) }; + + // Keep polling until either we get a value or the future is not ready + loop { + 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); + break; + } + + // If no nodes are produced but the future woke up immediately, then try polling it again + // This circumvents things like yield_now, but is important is important when rendering + // components that are just a stream of immediately ready futures + _ if leaf.notified.get() => { + leaf.notified.set(false); + continue; + } + + // If no nodes are produced, then we need to wait for the future to be woken up + // Insert the future into fiber leaves and break + _ => { + entry.insert(leaf); + self.collected_leaves.push(suspense_id); + break; + } + }; + } + }; + + let scope = &self.scopes[scope_id.0]; + + // 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); + frame.node.set(allocated); + + // And move the render generation forward by one + scope.render_cnt.set(scope.render_cnt.get() + 1); + + // remove this scope from dirty scopes + self.dirty_scopes.remove(&DirtyScope { + height: scope.height, + id: scope.id, + }); + + // rebind the lifetime now that its stored internally + unsafe { mem::transmute(allocated) } + } +} diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 2e7604d9f..14fc8c9e6 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,429 +1,53 @@ -use crate::{innerlude::*, unsafe_utils::extend_vnode}; -use bumpalo::Bump; -use futures_channel::mpsc::UnboundedSender; -use fxhash::FxHashMap; -use slab::Slab; +use crate::{ + any_props::AnyProps, + any_props::VProps, + arena::ElementId, + bump_frame::BumpFrame, + innerlude::{DynamicNode, EventHandler, VComponent, VText}, + innerlude::{Scheduler, SchedulerMsg}, + lazynodes::LazyNodes, + nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn}, + Attribute, AttributeValue, Element, Event, Properties, TaskId, +}; +use bumpalo::{boxed::Box as BumpBox, Bump}; +use rustc_hash::{FxHashMap, FxHashSet}; use std::{ any::{Any, TypeId}, - borrow::Borrow, cell::{Cell, RefCell}, - collections::{HashMap, HashSet}, + fmt::Arguments, future::Future, - pin::Pin, rc::Rc, sync::Arc, }; -/// for traceability, we use the raw fn pointer to identify the function -/// we also get the component name, but that's not necessarily unique in the app -pub(crate) type ComponentPtr = *mut std::os::raw::c_void; +/// A wrapper around the [`Scoped`] object that contains a reference to the [`ScopeState`] and properties for a given +/// component. +/// +/// The [`Scope`] is your handle to the [`crate::VirtualDom`] and the component state. Every component is given its own +/// [`ScopeState`] and merged with its properties to create a [`Scoped`]. +/// +/// The [`Scope`] handle specifically exists to provide a stable reference to these items for the lifetime of the +/// component render. +pub type Scope<'a, T = ()> = &'a Scoped<'a, T>; -pub(crate) struct Heuristic { - hook_arena_size: usize, - node_arena_size: usize, -} - -// a slab-like arena with stable references even when new scopes are allocated -// uses a bump arena as a backing +// This ScopedType exists because we want to limit the amount of monomorphization that occurs when making inner +// state type generic over props. When the state is generic, it causes every method to be monomorphized for every +// instance of Scope in the codebase. // -// has an internal heuristics engine to pre-allocate arenas to the right size -pub(crate) struct ScopeArena { - pub scope_gen: Cell, - pub bump: Bump, - pub scopes: RefCell>, - pub heuristics: RefCell>, - pub free_scopes: RefCell>, - pub nodes: RefCell>>, - pub tasks: Rc, -} - -impl ScopeArena { - pub(crate) fn new(sender: UnboundedSender) -> Self { - let bump = Bump::new(); - - // allocate a container for the root element - // this will *never* show up in the diffing process - // todo: figure out why this is necessary. i forgot. whoops. - let el = bump.alloc(VElement { - tag: "root", - namespace: None, - key: None, - id: Cell::new(Some(ElementId(0))), - parent: Default::default(), - listeners: &[], - attributes: &[], - children: &[], - }); - - let node = bump.alloc(VNode::Element(el)); - let mut nodes = Slab::new(); - let root_id = nodes.insert(unsafe { std::mem::transmute(node as *const _) }); - - debug_assert_eq!(root_id, 0); - - Self { - scope_gen: Cell::new(0), - bump, - scopes: RefCell::new(FxHashMap::default()), - heuristics: RefCell::new(FxHashMap::default()), - free_scopes: RefCell::new(Vec::new()), - nodes: RefCell::new(nodes), - tasks: Rc::new(TaskQueue { - tasks: RefCell::new(FxHashMap::default()), - task_map: RefCell::new(FxHashMap::default()), - gen: Cell::new(0), - sender, - }), - } - } - - /// Safety: - /// - Obtaining a mutable reference to any Scope is unsafe - /// - Scopes use interior mutability when sharing data into components - pub(crate) fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { - unsafe { self.scopes.borrow().get(&id).map(|f| &**f) } - } - - pub(crate) fn get_scope_raw(&self, id: ScopeId) -> Option<*mut ScopeState> { - self.scopes.borrow().get(&id).copied() - } - - pub(crate) fn new_with_key( - &self, - fc_ptr: ComponentPtr, - vcomp: Box, - parent_scope: Option, - container: ElementId, - subtree: u32, - ) -> ScopeId { - // Increment the ScopeId system. ScopeIDs are never reused - let new_scope_id = ScopeId(self.scope_gen.get()); - self.scope_gen.set(self.scope_gen.get() + 1); - - // Get the height of the scope - let height = parent_scope - .and_then(|id| self.get_scope(id).map(|scope| scope.height + 1)) - .unwrap_or_default(); - - let parent_scope = parent_scope.and_then(|f| self.get_scope_raw(f)); - - /* - This scopearena aggressively reuses old scopes when possible. - We try to minimize the new allocations for props/arenas. - - However, this will probably lead to some sort of fragmentation. - I'm not exactly sure how to improve this today. - */ - if let Some(old_scope) = self.free_scopes.borrow_mut().pop() { - // reuse the old scope - let scope = unsafe { &mut *old_scope }; - - scope.container = container; - scope.our_arena_idx = new_scope_id; - scope.parent_scope = parent_scope; - scope.height = height; - scope.fnptr = fc_ptr; - scope.props.get_mut().replace(vcomp); - scope.subtree.set(subtree); - scope.frames[0].reset(); - scope.frames[1].reset(); - scope.shared_contexts.get_mut().clear(); - scope.items.get_mut().listeners.clear(); - scope.items.get_mut().borrowed_props.clear(); - scope.hook_idx.set(0); - scope.hook_vals.get_mut().clear(); - - let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope); - debug_assert!(any_item.is_none()); - } else { - // else create a new scope - let (node_capacity, hook_capacity) = self - .heuristics - .borrow() - .get(&fc_ptr) - .map(|h| (h.node_arena_size, h.hook_arena_size)) - .unwrap_or_default(); - - self.scopes.borrow_mut().insert( - new_scope_id, - self.bump.alloc(ScopeState { - container, - our_arena_idx: new_scope_id, - parent_scope, - height, - fnptr: fc_ptr, - props: RefCell::new(Some(vcomp)), - frames: [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)], - - // todo: subtrees - subtree: Cell::new(0), - is_subtree_root: Cell::default(), - - generation: 0.into(), - - tasks: self.tasks.clone(), - shared_contexts: RefCell::default(), - - items: RefCell::new(SelfReferentialItems { - listeners: Vec::default(), - borrowed_props: Vec::default(), - }), - - hook_arena: Bump::new(), - hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)), - hook_idx: Cell::default(), - }), - ); - } - - new_scope_id - } - - // Removes a scope and its descendents from the arena - pub fn try_remove(&self, id: ScopeId) { - self.ensure_drop_safety(id); - - // Dispose of any ongoing tasks - let mut tasks = self.tasks.tasks.borrow_mut(); - let mut task_map = self.tasks.task_map.borrow_mut(); - if let Some(cur_tasks) = task_map.remove(&id) { - for task in cur_tasks { - tasks.remove(&task); - } - } - - // Safety: - // - ensure_drop_safety ensures that no references to this scope are in use - // - this raw pointer is removed from the map - let scope = unsafe { &mut *self.scopes.borrow_mut().remove(&id).unwrap() }; - scope.reset(); - - self.free_scopes.borrow_mut().push(scope); - } - - pub fn reserve_node<'a>(&self, node: &'a VNode<'a>) -> ElementId { - let mut els = self.nodes.borrow_mut(); - let entry = els.vacant_entry(); - let key = entry.key(); - let id = ElementId(key); - let node = unsafe { extend_vnode(node) }; - entry.insert(node as *const _); - id - } - - pub fn update_node<'a>(&self, node: &'a VNode<'a>, id: ElementId) { - let node = unsafe { extend_vnode(node) }; - *self.nodes.borrow_mut().get_mut(id.0).unwrap() = node; - } - - pub fn collect_garbage(&self, id: ElementId) { - self.nodes.borrow_mut().remove(id.0); - } - - /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from - /// causing UB in our tree. +// +/// A wrapper around a component's [`ScopeState`] and properties. The [`ScopeState`] provides the majority of methods +/// for the VirtualDom and component state. +pub struct Scoped<'a, T = ()> { + /// The component's state and handle to the scheduler. /// - /// This works by cleaning up our references from the bottom of the tree to the top. The directed graph of components - /// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove - /// any possible references to the data in the hook list. - /// - /// References to hook data can only be stored in listeners and component props. During diffing, we make sure to log - /// all listeners and borrowed props so we can clear them here. - /// - /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will - /// be dropped. - pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { - if let Some(scope) = self.get_scope(scope_id) { - let mut items = scope.items.borrow_mut(); - - // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we - // run the hooks (which hold an &mut Reference) - // recursively call ensure_drop_safety on all children - items.borrowed_props.drain(..).for_each(|comp| { - if let Some(scope_id) = comp.scope.get() { - self.ensure_drop_safety(scope_id); - } - drop(comp.props.take()); - }); - - // Now that all the references are gone, we can safely drop our own references in our listeners. - items - .listeners - .drain(..) - .for_each(|listener| drop(listener.callback.borrow_mut().take())); - } - } - - pub(crate) fn run_scope(&self, id: ScopeId) { - // Cycle to the next frame and then reset it - // This breaks any latent references, invalidating every pointer referencing into it. - // Remove all the outdated listeners - self.ensure_drop_safety(id); - - // todo: we *know* that this is aliased by the contents of the scope itself - let scope = unsafe { &mut *self.get_scope_raw(id).expect("could not find scope") }; - - log::trace!("running scope {:?} symbol: {:?}", id, scope.fnptr); - - // Safety: - // - We dropped the listeners, so no more &mut T can be used while these are held - // - All children nodes that rely on &mut T are replaced with a new reference - scope.hook_idx.set(0); - - { - // Safety: - // - We've dropped all references to the wip bump frame with "ensure_drop_safety" - unsafe { scope.reset_wip_frame() }; - - let items = scope.items.borrow(); - - // guarantee that we haven't screwed up - there should be no latent references anywhere - debug_assert!(items.listeners.is_empty()); - debug_assert!(items.borrowed_props.is_empty()); - } - - /* - If the component returns None, then we fill in a placeholder node. This will wipe what was there. - An alternate approach is to leave the Real Dom the same, but that can lead to safety issues and a lot more checks. - - Instead, we just treat the `None` as a shortcut to placeholder. - If the developer wants to prevent a scope from updating, they should control its memoization instead. - - Also, the way we implement hooks allows us to cut rendering short before the next hook is recalled. - I'm not sure if React lets you abort the component early, but we let you do that. - */ - - let props = scope.props.borrow(); - let render = props.as_ref().unwrap(); - if let Some(node) = render.render(scope) { - let frame = scope.wip_frame(); - let node = frame.bump.alloc(node); - frame.node.set(unsafe { extend_vnode(node) }); - } else { - let frame = scope.wip_frame(); - let node = frame - .bump - .alloc(VNode::Placeholder(frame.bump.alloc(VPlaceholder { - id: Cell::default(), - }))); - frame.node.set(unsafe { extend_vnode(node) }); - } - - // make the "wip frame" contents the "finished frame" - // any future dipping into completed nodes after "render" will go through "fin head" - scope.cycle_frame(); - } - - pub fn call_listener_with_bubbling(&self, event: &UserEvent, element: ElementId) { - let nodes = self.nodes.borrow(); - let mut cur_el = Some(element); - - let state = Rc::new(BubbleState::new()); - - while let Some(id) = cur_el.take() { - if let Some(el) = nodes.get(id.0) { - let real_el = unsafe { &**el }; - - if let VNode::Element(real_el) = real_el { - for listener in real_el.listeners.borrow().iter() { - if listener.event == event.name { - if state.canceled.get() { - // stop bubbling if canceled - return; - } - - let mut cb = listener.callback.borrow_mut(); - if let Some(cb) = cb.as_mut() { - // todo: arcs are pretty heavy to clone - // we really want to convert arc to rc - // unfortunately, the SchedulerMsg must be send/sync to be sent across threads - // we could convert arc to rc internally or something - (cb)(AnyEvent { - bubble_state: state.clone(), - data: event.data.clone(), - }); - } - - if !event.bubbles { - return; - } - } - } - - cur_el = real_el.parent.get(); - } - } - } - } - - // The head of the bumpframe is the first linked NodeLink - pub fn wip_head(&self, id: ScopeId) -> &VNode { - let scope = self.get_scope(id).unwrap(); - let frame = scope.wip_frame(); - let node = unsafe { &*frame.node.get() }; - unsafe { extend_vnode(node) } - } - - // The head of the bumpframe is the first linked NodeLink - pub fn fin_head(&self, id: ScopeId) -> &VNode { - let scope = self.get_scope(id).unwrap(); - let frame = scope.fin_frame(); - let node = unsafe { &*frame.node.get() }; - unsafe { extend_vnode(node) } - } - - pub fn root_node(&self, id: ScopeId) -> &VNode { - self.fin_head(id) - } - - // this is totally okay since all our nodes are always in a valid state - pub fn get_element(&self, id: ElementId) -> Option<&VNode> { - self.nodes - .borrow() - .get(id.0) - .copied() - .map(|ptr| unsafe { extend_vnode(&*ptr) }) - } -} - -/// Components in Dioxus use the "Context" object to interact with their lifecycle. -/// -/// This lets components access props, schedule updates, integrate hooks, and expose shared state. -/// -/// For the most part, the only method you should be using regularly is `render`. -/// -/// ## Example -/// -/// ```ignore -/// #[derive(Props)] -/// struct ExampleProps { -/// name: String -/// } -/// -/// fn Example(cx: Scope) -> Element { -/// cx.render(rsx!{ div {"Hello, {cx.props.name}"} }) -/// } -/// ``` -pub struct Scope<'a, P = ()> { - /// The internal ScopeState for this component + /// Stores things like the custom bump arena, spawn functions, hooks, and the scheduler. pub scope: &'a ScopeState, - /// The props for this component - pub props: &'a P, + /// The component's properties. + pub props: &'a T, } -impl

Copy for Scope<'_, P> {} -impl

Clone for Scope<'_, P> { - fn clone(&self) -> Self { - Self { - scope: self.scope, - props: self.props, - } - } -} - -impl<'a, P> std::ops::Deref for Scope<'a, P> { - // rust will auto deref again to the original 'a lifetime at the call site +impl<'a, T> std::ops::Deref for Scoped<'a, T> { type Target = &'a ScopeState; fn deref(&self) -> &Self::Target { &self.scope @@ -432,115 +56,119 @@ impl<'a, P> std::ops::Deref for Scope<'a, P> { /// A component's unique identifier. /// -/// `ScopeId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`ScopeID`]s will never be reused -/// once a component has been unmounted. +/// `ScopeId` is a `usize` that acts a key for the internal slab of Scopes. This means that the key is not unqiue across +/// time. We do try and guarantee that between calls to `wait_for_work`, no ScopeIds will be recycled in order to give +/// time for any logic that relies on these IDs to properly update. #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] pub struct ScopeId(pub usize); -/// A task's unique identifier. +/// A component's state separate from its props. /// -/// `TaskId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`TaskID`]s will never be reused -/// once a Task has been completed. -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct TaskId { - /// The global ID of the task - pub id: usize, - - /// The original scope that this task was scheduled in - pub scope: ScopeId, -} - -/// Every component in Dioxus is represented by a `ScopeState`. -/// -/// Scopes contain the state for hooks, the component's props, and other lifecycle information. -/// -/// Scopes are allocated in a generational arena. As components are mounted/unmounted, they will replace slots of dead components. -/// The actual contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently. -/// -/// We expose the `Scope` type so downstream users can traverse the Dioxus [`VirtualDom`] for whatever -/// use case they might have. +/// This struct exists to provide a common interface for all scopes without relying on generics. pub struct ScopeState { - pub(crate) parent_scope: Option<*mut ScopeState>, - pub(crate) container: ElementId, - pub(crate) our_arena_idx: ScopeId, + pub(crate) render_cnt: Cell, + pub(crate) name: &'static str, + + pub(crate) node_arena_1: BumpFrame, + pub(crate) node_arena_2: BumpFrame, + + pub(crate) parent: Option<*mut ScopeState>, + pub(crate) id: ScopeId, + pub(crate) height: u32, - pub(crate) fnptr: ComponentPtr, - // todo: subtrees - pub(crate) is_subtree_root: Cell, - pub(crate) subtree: Cell, - pub(crate) props: RefCell>>, - - // nodes, items - pub(crate) frames: [BumpFrame; 2], - pub(crate) generation: Cell, - pub(crate) items: RefCell>, - - // hooks pub(crate) hook_arena: Bump, - pub(crate) hook_vals: RefCell>, + pub(crate) hook_list: RefCell>, pub(crate) hook_idx: Cell, - // shared state -> todo: move this out of scopestate - pub(crate) shared_contexts: RefCell>>, - pub(crate) tasks: Rc, + pub(crate) shared_contexts: RefCell>>, + + pub(crate) tasks: Rc, + pub(crate) spawned_tasks: FxHashSet, + + pub(crate) borrowed_props: RefCell>>, + pub(crate) listeners: RefCell>>, + + pub(crate) props: Option>>, + pub(crate) placeholder: Cell>, } -pub struct SelfReferentialItems<'a> { - pub(crate) listeners: Vec<&'a Listener<'a>>, - pub(crate) borrowed_props: Vec<&'a VComponent<'a>>, -} - -// Public methods exposed to libraries and components -impl ScopeState { - /// Get the subtree ID that this scope belongs to. - /// - /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route - /// the mutations to the correct window/portal/subtree. - /// - /// - /// # Example - /// - /// ```rust, ignore - /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} })); - /// dom.rebuild(); - /// - /// let base = dom.base_scope(); - /// - /// assert_eq!(base.subtree(), 0); - /// ``` - /// - /// todo: enable - pub(crate) fn _subtree(&self) -> u32 { - self.subtree.get() +impl<'src> ScopeState { + pub(crate) fn current_frame(&self) -> &BumpFrame { + match self.render_cnt.get() % 2 { + 0 => &self.node_arena_1, + 1 => &self.node_arena_2, + _ => unreachable!(), + } } - /// Create a new subtree with this scope as the root of the subtree. - /// - /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route - /// the mutations to the correct window/portal/subtree. - /// - /// This method - /// - /// # Example - /// - /// ```rust, ignore - /// fn App(cx: Scope) -> Element { - /// render!(div { "Subtree {id}"}) - /// }; - /// ``` - /// - /// todo: enable subtree - pub(crate) fn _create_subtree(&self) -> Option { - if self.is_subtree_root.get() { - None - } else { - todo!() + pub(crate) fn previous_frame(&self) -> &BumpFrame { + match self.render_cnt.get() % 2 { + 1 => &self.node_arena_1, + 0 => &self.node_arena_2, + _ => unreachable!(), } } + 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 + } + + /// Get the current render since the inception of this component + /// + /// This can be used as a helpful diagnostic when debugging hooks/renders, etc + pub fn generation(&self) -> usize { + self.render_cnt.get() + } + + /// Get a handle to the currently active bump arena for this Scope + /// + /// This is a bump memory allocator. Be careful using this directly since the contents will be wiped on the next render. + /// It's easy to leak memory here since the drop implementation will not be called for any objects allocated in this arena. + /// + /// 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 + } + + /// Get a handle to the currently active head node arena for this Scope + /// + /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR. + /// + /// Panics if the tree has not been built yet. + pub fn root_node(&self) -> &RenderReturn { + self.try_root_node() + .expect("The tree has not been built yet. Make sure to call rebuild on the tree before accessing its nodes.") + } + + /// Try to get a handle to the currently active head node arena for this Scope + /// + /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR. + /// + /// Returns [`None`] if the tree has not been built yet. + pub fn try_root_node(&self) -> Option<&RenderReturn> { + let ptr = self.current_frame().node.get(); + + if ptr.is_null() { + return None; + } + + let r: &RenderReturn = unsafe { &*ptr }; + + unsafe { std::mem::transmute(r) } + } + /// Get the height of this Scope - IE the number of scopes above it. /// /// A Scope with a height of `0` is the root scope - there are no other scopes above it. @@ -559,9 +187,9 @@ impl ScopeState { self.height } - /// Get the Parent of this [`Scope`] within this Dioxus [`VirtualDom`]. + /// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`]. /// - /// This ID is not unique across Dioxus [`VirtualDom`]s or across time. IDs will be reused when components are unmounted. + /// This ID is not unique across Dioxus [`crate::VirtualDom`]s or across time. IDs will be reused when components are unmounted. /// /// The base component will not have a parent, and will return `None`. /// @@ -577,12 +205,12 @@ impl ScopeState { /// ``` pub fn parent(&self) -> Option { // safety: the pointer to our parent is *always* valid thanks to the bump arena - self.parent_scope.map(|p| unsafe { &*p }.our_arena_idx) + self.parent.map(|p| unsafe { &*p }.id) } - /// Get the ID of this Scope within this Dioxus [`VirtualDom`]. + /// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`]. /// - /// This ID is not unique across Dioxus [`VirtualDom`]s or across time. IDs will be reused when components are unmounted. + /// This ID is not unique across Dioxus [`crate::VirtualDom`]s or across time. IDs will be reused when components are unmounted. /// /// # Example /// @@ -594,17 +222,12 @@ impl ScopeState { /// assert_eq!(base.scope_id(), 0); /// ``` pub fn scope_id(&self) -> ScopeId { - self.our_arena_idx - } - - /// Get a handle to the raw update scheduler channel - pub fn scheduler_channel(&self) -> UnboundedSender { - self.tasks.sender.clone() + self.id } /// Create a subscription that schedules a future render for the reference component /// - /// ## Notice: you should prefer using [`schedule_update_any`] and [`scope_id`] + /// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`] pub fn schedule_update(&self) -> Arc { let (chan, id) = (self.tasks.sender.clone(), self.scope_id()); Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id)))) @@ -617,19 +240,19 @@ impl ScopeState { /// This method should be used when you want to schedule an update for a component pub fn schedule_update_any(&self) -> Arc { let chan = self.tasks.sender.clone(); - Arc::new(move |id| drop(chan.unbounded_send(SchedulerMsg::Immediate(id)))) + Arc::new(move |id| { + chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap(); + }) } - /// Get the [`ScopeId`] of a mounted component. - /// - /// `ScopeId` is not unique for the lifetime of the [`VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted. + /// Mark this scope as dirty, and schedule a render for it. pub fn needs_update(&self) { self.needs_update_any(self.scope_id()); } /// Get the [`ScopeId`] of a mounted component. /// - /// `ScopeId` is not unique for the lifetime of the [`VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted. + /// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted. pub fn needs_update_any(&self, id: ScopeId) { self.tasks .sender @@ -637,20 +260,45 @@ impl ScopeState { .expect("Scheduler to exist if scope exists"); } - /// Get the Root Node of this scope - pub fn root_node(&self) -> &VNode { - let node = unsafe { &*self.fin_frame().node.get() }; - unsafe { std::mem::transmute(node) } + /// Return any context of type T if it exists on this scope + pub fn has_context(&self) -> Option { + self.shared_contexts + .borrow() + .get(&TypeId::of::())? + .downcast_ref::() + .cloned() } - /// This method enables the ability to expose state to children further down the [`VirtualDom`] Tree. + /// Try to retrieve a shared state with type `T` from any parent scope. + /// + /// Clones the state if it exists. + pub fn consume_context(&self) -> Option { + if let Some(this_ctx) = self.has_context() { + return Some(this_ctx); + } + + let mut search_parent = self.parent; + while let Some(parent_ptr) = search_parent { + // safety: all parent pointers are valid thanks to the bump arena + let parent = unsafe { &*parent_ptr }; + if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::()) { + return shared.downcast_ref::().cloned(); + } + search_parent = parent.parent; + } + None + } + + /// Expose state to children further down the [`crate::VirtualDom`] Tree. Does not require `clone` on the context, + /// though we do recommend it. /// /// 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_consume_context` instead. + /// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead. /// - /// When the component is dropped, so is the context. Be aware of this behavior when consuming - /// the context via Rc/Weak. + /// 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 /// @@ -668,102 +316,18 @@ impl ScopeState { /// } /// ``` pub fn provide_context(&self, value: T) -> T { + let value2 = value.clone(); + self.shared_contexts .borrow_mut() - .insert(TypeId::of::(), Box::new(value.clone())) - .and_then(|f| f.downcast::().ok()); - value - } + .insert(TypeId::of::(), Box::new(value)); - /// Provide a context for the root component from anywhere in your app. - /// - /// - /// # Example - /// - /// ```rust, ignore - /// struct SharedState(&'static str); - /// - /// static App: Component = |cx| { - /// cx.use_hook(|| cx.provide_root_context(SharedState("world"))); - /// render!(Child {}) - /// } - /// - /// static Child: Component = |cx| { - /// let state = cx.consume_state::(); - /// render!(div { "hello {state.0}" }) - /// } - /// ``` - pub fn provide_root_context(&self, value: T) -> T { - // if we *are* the root component, then we can just provide the context directly - if self.scope_id() == ScopeId(0) { - self.shared_contexts - .borrow_mut() - .insert(TypeId::of::(), Box::new(value.clone())) - .and_then(|f| f.downcast::().ok()); - return value; - } - - let mut search_parent = self.parent_scope; - - while let Some(parent) = search_parent.take() { - let parent = unsafe { &*parent }; - - if parent.scope_id() == ScopeId(0) { - let exists = parent - .shared_contexts - .borrow_mut() - .insert(TypeId::of::(), Box::new(value.clone())); - - if exists.is_some() { - log::warn!("Context already provided to parent scope - replacing it"); - } - return value; - } - - search_parent = parent.parent_scope; - } - - unreachable!("all apps have a root scope") - } - - /// Try to retrieve a shared state with type T from the any parent Scope. - pub fn consume_context(&self) -> Option { - if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::()) { - Some( - (*shared - .downcast_ref::() - .expect("Context of type T should exist")) - .clone(), - ) - } else { - let mut search_parent = self.parent_scope; - - while let Some(parent_ptr) = search_parent { - // safety: all parent pointers are valid thanks to the bump arena - let parent = unsafe { &*parent_ptr }; - if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::()) { - return Some( - shared - .downcast_ref::() - .expect("Context of type T should exist") - .clone(), - ); - } - search_parent = parent.parent_scope; - } - None - } + value2 } /// Pushes the future onto the poll queue to be polled after the component renders. pub fn push_future(&self, fut: impl Future + 'static) -> TaskId { - // wake up the scheduler if it is sleeping - self.tasks - .sender - .unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx)) - .expect("Scheduler should exist"); - - self.tasks.spawn(self.our_arena_idx, fut) + self.tasks.spawn(self.id, fut) } /// Spawns the future but does not return the [`TaskId`] @@ -771,27 +335,30 @@ impl ScopeState { self.push_future(fut); } - /// Spawn a future that Dioxus will never clean up + /// Spawn a future that Dioxus won't clean up when this component is unmounted /// /// This is good for tasks that need to be run after the component has been dropped. pub fn spawn_forever(&self, fut: impl Future + 'static) -> TaskId { + // The root scope will never be unmounted so we can just add the task at the top of the app + let id = self.tasks.spawn(ScopeId(0), fut); + // wake up the scheduler if it is sleeping self.tasks .sender - .unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx)) + .unbounded_send(SchedulerMsg::TaskNotified(id)) .expect("Scheduler should exist"); - // The root scope will never be unmounted so we can just add the task at the top of the app - self.tasks.spawn(ScopeId(0), fut) + id } - /// Informs the scheduler that this task is no longer needed and should be removed - /// on next poll. + /// Informs the scheduler that this task is no longer needed and should be removed. + /// + /// This drops the task immediately. pub fn remove_future(&self, id: TaskId) { self.tasks.remove(id); } - /// Take a lazy [`VNode`] structure and actually build it with the context of the Vdoms efficient [`VNode`] allocator. + /// Take a lazy [`crate::VNode`] structure and actually build it with the context of the efficient [`bumpalo::Bump`] allocator. /// /// ## Example /// @@ -804,11 +371,142 @@ impl ScopeState { /// cx.render(lazy_tree) /// } ///``` - pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Option> { - Some(rsx.call(NodeFactory { - scope: self, - bump: &self.wip_frame().bump, - })) + pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> { + let element = rsx.call(self); + + let mut listeners = self.listeners.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); + } + } + + 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); + } + } + + Ok(element) + } + + /// Create a dynamic text node using [`Arguments`] and the [`ScopeState`]'s internal [`Bump`] allocator + pub fn text_node(&'src self, args: Arguments) -> DynamicNode<'src> { + DynamicNode::Text(VText { + value: self.raw_text(args), + id: Default::default(), + }) + } + + /// Allocate some text inside the [`ScopeState`] from [`Arguments`] + /// + /// Uses the currently active [`Bump`] allocator + pub fn raw_text(&'src self, args: Arguments) -> &'src str { + args.as_str().unwrap_or_else(|| { + use bumpalo::core_alloc::fmt::Write; + let mut str_buf = bumpalo::collections::String::new_in(self.bump()); + str_buf.write_fmt(args).unwrap(); + str_buf.into_bump_str() + }) + } + + /// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] using the internal [`Bump`] allocator + pub fn make_node<'c, I>(&'src self, into: impl IntoDynNode<'src, I> + 'c) -> DynamicNode { + into.into_vnode(self) + } + + /// Create a new [`Attribute`] from a name, value, namespace, and volatile bool + /// + /// "Volatile" referes to whether or not Dioxus should always override the value. This helps prevent the UI in + /// some renderers stay in sync with the VirtualDom's understanding of the world + pub fn attr( + &'src self, + name: &'static str, + value: impl IntoAttributeValue<'src>, + namespace: Option<&'static str>, + volatile: bool, + ) -> Attribute<'src> { + Attribute { + name, + namespace, + volatile, + mounted_element: Default::default(), + value: value.into_value(self.bump()), + } + } + + /// Create a new [`DynamicNode::Component`] variant + /// + /// + /// The given component can be any of four signatures. Remember that an [`Element`] is really a [`Result`]. + /// + /// ```rust, ignore + /// // Without explicit props + /// fn(Scope) -> Element; + /// async fn(Scope<'_>) -> Element; + /// + /// // With explicit props + /// fn(Scope) -> Element; + /// async fn(Scope>) -> Element; + /// ``` + pub fn component>( + &'src self, + component: fn(Scope<'src, P>) -> F, + props: P, + fn_name: &'static str, + ) -> DynamicNode<'src> + where + P: Properties + 'src, + { + let vcomp = VProps::new(component, P::memoize, props); + + // cast off the lifetime of the render return + let as_dyn: Box + '_> = Box::new(vcomp); + let extended: Box + 'src> = unsafe { std::mem::transmute(as_dyn) }; + + DynamicNode::Component(VComponent { + name: fn_name, + render_fn: component as *const (), + static_props: P::IS_STATIC, + props: RefCell::new(Some(extended)), + scope: Cell::new(None), + }) + } + + /// Create a new [`EventHandler`] from an [`FnMut`] + pub fn event_handler(&'src self, f: impl FnMut(T) + 'src) -> EventHandler<'src, T> { + let handler: &mut dyn FnMut(T) = self.bump().alloc(f); + let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; + let callback = RefCell::new(Some(caller)); + EventHandler { callback } + } + + /// Create a new [`AttributeValue`] with the listener variant from a callback + /// + /// The callback must be confined to the lifetime of the ScopeState + pub fn listener( + &'src self, + mut callback: impl FnMut(Event) + 'src, + ) -> 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 FnMut(_) + 'src> = unsafe { + BumpBox::from_raw(self.bump().alloc(move |event: Event| { + if let Ok(data) = event.data.downcast::() { + callback(Event { + propogates: event.propogates, + data, + }) + } + })) + }; + + AttributeValue::Listener(RefCell::new(Some(boxed))) } /// Store a value between renders. The foundational hook for all other hooks. @@ -829,19 +527,17 @@ impl ScopeState { /// ``` #[allow(clippy::mut_from_ref)] pub fn use_hook(&self, initializer: impl FnOnce() -> State) -> &mut State { - let mut vals = self.hook_vals.borrow_mut(); + let cur_hook = self.hook_idx.get(); + let mut hook_list = self.hook_list.borrow_mut(); - let hook_len = vals.len(); - let cur_idx = self.hook_idx.get(); - - if cur_idx >= hook_len { - vals.push(self.hook_arena.alloc(initializer())); + if cur_hook >= hook_list.len() { + hook_list.push(self.hook_arena.alloc(initializer())); } - vals - .get(cur_idx) + hook_list + .get(cur_hook) .and_then(|inn| { - self.hook_idx.set(cur_idx + 1); + self.hook_idx.set(cur_hook + 1); let raw_box = unsafe { &mut **inn }; raw_box.downcast_mut::() }) @@ -855,159 +551,4 @@ impl ScopeState { "###, ) } - - /// The "work in progress frame" represents the frame that is currently being worked on. - pub(crate) fn wip_frame(&self) -> &BumpFrame { - match self.generation.get() & 1 { - 0 => &self.frames[0], - _ => &self.frames[1], - } - } - - /// Mutable access to the "work in progress frame" - used to clear it - pub(crate) fn wip_frame_mut(&mut self) -> &mut BumpFrame { - match self.generation.get() & 1 { - 0 => &mut self.frames[0], - _ => &mut self.frames[1], - } - } - - /// Access to the frame where finalized nodes existed - pub(crate) fn fin_frame(&self) -> &BumpFrame { - match self.generation.get() & 1 { - 1 => &self.frames[0], - _ => &self.frames[1], - } - } - - /// Reset this component's frame - /// - /// # Safety: - /// - /// This method breaks every reference of every [`VNode`] in the current frame. - /// - /// Calling reset itself is not usually a big deal, but we consider it important - /// due to the complex safety guarantees we need to uphold. - pub(crate) unsafe fn reset_wip_frame(&mut self) { - self.wip_frame_mut().bump.reset(); - } - - /// Cycle to the next generation - pub(crate) fn cycle_frame(&self) { - self.generation.set(self.generation.get() + 1); - } - - // todo: disable bookkeeping on drop (unncessary) - pub(crate) fn reset(&mut self) { - // first: book keaping - self.hook_idx.set(0); - self.parent_scope = None; - self.generation.set(0); - self.is_subtree_root.set(false); - self.subtree.set(0); - - // next: shared context data - self.shared_contexts.get_mut().clear(); - - // next: reset the node data - let SelfReferentialItems { - borrowed_props, - listeners, - } = self.items.get_mut(); - borrowed_props.clear(); - listeners.clear(); - self.frames[0].reset(); - self.frames[1].reset(); - - // Free up the hook values - self.hook_vals.get_mut().drain(..).for_each(|state| { - let as_mut = unsafe { &mut *state }; - let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) }; - drop(boxed); - }); - - // Finally, clear the hook arena - self.hook_arena.reset(); - } -} - -pub(crate) struct BumpFrame { - pub bump: Bump, - pub node: Cell<*const VNode<'static>>, -} -impl BumpFrame { - pub(crate) fn new(capacity: usize) -> Self { - let bump = Bump::with_capacity(capacity); - let node = bump.alloc(VText { - text: "placeholdertext", - id: Cell::default(), - is_static: false, - }); - let node = bump.alloc(VNode::Text(unsafe { - &*(node as *mut VText as *const VText) - })); - let nodes = Cell::new(node as *const _); - Self { bump, node: nodes } - } - - pub(crate) fn reset(&mut self) { - self.bump.reset(); - let node = self.bump.alloc(VText { - text: "placeholdertext", - id: Cell::default(), - is_static: false, - }); - let node = self.bump.alloc(VNode::Text(unsafe { - &*(node as *mut VText as *const VText) - })); - self.node.set(node as *const _); - } -} - -pub(crate) struct TaskQueue { - pub(crate) tasks: RefCell>, - pub(crate) task_map: RefCell>>, - gen: Cell, - sender: UnboundedSender, -} - -pub(crate) type InnerTask = Pin>>; -impl TaskQueue { - fn spawn(&self, scope: ScopeId, task: impl Future + 'static) -> TaskId { - let pinned = Box::pin(task); - let id = self.gen.get(); - self.gen.set(id + 1); - let tid = TaskId { id, scope }; - - self.tasks.borrow_mut().insert(tid, pinned); - - // also add to the task map - // when the component is unmounted we know to remove it from the map - self.task_map - .borrow_mut() - .entry(scope) - .or_default() - .insert(tid); - - tid - } - - fn remove(&self, id: TaskId) { - if let Ok(mut tasks) = self.tasks.try_borrow_mut() { - tasks.remove(&id); - if let Some(task_map) = self.task_map.borrow_mut().get_mut(&id.scope) { - task_map.remove(&id); - } - } - // the task map is still around, but it'll be removed when the scope is unmounted - } - - pub(crate) fn has_tasks(&self) -> bool { - !self.tasks.borrow().is_empty() - } -} - -#[test] -fn sizeof() { - dbg!(std::mem::size_of::()); } diff --git a/packages/core/src/subtree.rs b/packages/core/src/subtree.rs new file mode 100644 index 000000000..57d09efe2 --- /dev/null +++ b/packages/core/src/subtree.rs @@ -0,0 +1,30 @@ +/* +This is a WIP module + +Subtrees allow the virtualdom to split up the mutation stream into smaller chunks which can be directed to different parts of the dom. +It's core to implementing multiwindow desktop support, portals, and alternative inline renderers like react-three-fiber. + +The primary idea is to give each renderer a linear element tree managed by Dioxus to maximize performance and minimize memory usage. +This can't be done if two renderers need to share the same native tree. +With subtrees, we have an entirely different slab of elements + +*/ + +use std::borrow::Cow; + +use slab::Slab; + +use crate::{ElementPath, ScopeId}; + +/// A collection of elements confined to a single scope under a chunk of the tree +/// +/// All elements in this collection are guaranteed to be in the same scope and share the same numbering +/// +/// This unit can be multithreaded +/// Whenever multiple subtrees are present, we can perform **parallel diffing** +pub struct Subtree { + id: usize, + namespace: Cow<'static, str>, + root: ScopeId, + elements: Slab, +} diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs deleted file mode 100644 index 134675243..000000000 --- a/packages/core/src/util.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::innerlude::{VNode, VirtualDom}; - -/// An iterator that only yields "real" [`Element`]s. IE only Elements that are -/// not [`VNode::Component`] or [`VNode::Fragment`], . -pub struct ElementIdIterator<'a> { - vdom: &'a VirtualDom, - - // Heuristically we should never bleed into 5 completely nested fragments/components - // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane - stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>, -} - -impl<'a> ElementIdIterator<'a> { - /// Create a new iterator from the given [`VirtualDom`] and [`VNode`] - /// - /// This will allow you to iterate through all the real childrne of the [`VNode`]. - pub fn new(vdom: &'a VirtualDom, node: &'a VNode<'a>) -> Self { - Self { - vdom, - stack: smallvec::smallvec![(0, node)], - } - } -} - -impl<'a> Iterator for ElementIdIterator<'a> { - type Item = &'a VNode<'a>; - - fn next(&mut self) -> Option<&'a VNode<'a>> { - let mut should_pop = false; - let mut returned_node = None; - let mut should_push = None; - - while returned_node.is_none() { - if let Some((count, node)) = self.stack.last_mut() { - match node { - // We can only exit our looping when we get "real" nodes - VNode::Element(_) | VNode::Text(_) | VNode::Placeholder(_) => { - // We've recursed INTO an element/text - // We need to recurse *out* of it and move forward to the next - // println!("Found element! Returning it!"); - should_pop = true; - returned_node = Some(&**node); - } - - // If we get a fragment we push the next child - VNode::Fragment(frag) => { - let count = *count as usize; - if count >= frag.children.len() { - should_pop = true; - } else { - should_push = Some(&frag.children[count]); - } - } - - // For components, we load their root and push them onto the stack - VNode::Component(sc) => { - let scope = self.vdom.get_scope(sc.scope.get().unwrap()).unwrap(); - // Simply swap the current node on the stack with the root of the component - *node = scope.root_node(); - } - } - } else { - // If there's no more items on the stack, we're done! - return None; - } - - if should_pop { - self.stack.pop(); - if let Some((id, _)) = self.stack.last_mut() { - *id += 1; - } - should_pop = false; - } - - if let Some(push) = should_push { - self.stack.push((0, push)); - should_push = None; - } - } - - returned_node - } -} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 794eaa3f6..33f652d52 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -2,17 +2,24 @@ //! //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. -use crate::diff::DiffState; -use crate::innerlude::*; -use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures_util::{future::poll_fn, StreamExt}; -use fxhash::FxHashSet; -use indexmap::IndexSet; -use std::{collections::VecDeque, iter::FromIterator, task::Poll}; +use crate::{ + any_props::VProps, + arena::{ElementId, ElementRef}, + innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg}, + mutations::Mutation, + nodes::RenderReturn, + nodes::{Template, TemplateId}, + scheduler::SuspenseId, + scopes::{ScopeId, ScopeState}, + AttributeValue, Element, Event, Scope, SuspenseContext, +}; +use futures_util::{pin_mut, StreamExt}; +use rustc_hash::FxHashMap; +use slab::Slab; +use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; /// A virtual node system that progresses user events and diffs UI trees. /// -/// /// ## Guide /// /// Components are defined as simple functions that take [`Scope`] and return an [`Element`]. @@ -50,89 +57,112 @@ use std::{collections::VecDeque, iter::FromIterator, task::Poll}; /// let edits = vdom.rebuild(); /// ``` /// -/// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler. +/// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data. /// /// ```rust, ignore -/// let channel = vdom.get_scheduler_channel(); -/// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent { -/// // ... -/// })) +/// vdom.handle_event(event); /// ``` /// -/// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom. +/// While no events are ready, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom. /// /// ```rust, ignore /// vdom.wait_for_work().await; /// ``` /// -/// Once work is ready, call [`VirtualDom::work_with_deadline`] to compute the differences between the previous and +/// Once work is ready, call [`VirtualDom::render_with_deadline`] to compute the differences between the previous and /// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be /// handled by the renderer. /// /// ```rust, ignore -/// let mutations = vdom.work_with_deadline(|| false); -/// for edit in mutations { -/// apply(edit); +/// let mutations = vdom.work_with_deadline(tokio::time::sleep(Duration::from_millis(100))); +/// +/// for edit in mutations.edits { +/// real_dom.apply(edit); /// } /// ``` /// +/// To not wait for suspense while diffing the VirtualDom, call [`VirtualDom::render_immediate`] or pass an immediately +/// ready future to [`VirtualDom::render_with_deadline`]. +/// +/// /// ## Building an event loop around Dioxus: /// /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above. -/// /// ```rust, ignore -/// fn App(cx: Scope) -> Element { -/// cx.render(rsx!{ +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx! { /// div { "Hello World" } /// }) /// } /// -/// async fn main() { -/// let mut dom = VirtualDom::new(App); +/// let dom = VirtualDom::new(app); /// -/// let mut inital_edits = dom.rebuild(); -/// apply_edits(inital_edits); +/// real_dom.apply(dom.rebuild()); /// -/// loop { -/// dom.wait_for_work().await; -/// let frame_timeout = TimeoutFuture::new(Duration::from_millis(16)); -/// let deadline = || (&mut frame_timeout).now_or_never(); -/// let edits = dom.run_with_deadline(deadline).await; -/// apply_edits(edits); +/// loop { +/// select! { +/// _ = dom.wait_for_work() => {} +/// evt = real_dom.wait_for_event() => dom.handle_event(evt), /// } +/// +/// real_dom.apply(dom.render_immediate()); +/// } +/// ``` +/// +/// ## Waiting for suspense +/// +/// Because Dioxus supports suspense, you can use it for server-side rendering, static site generation, and other usecases +/// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the +/// [`VirtualDom::render_with_deadline`] method: +/// +/// ```rust, ignore +/// let dom = VirtualDom::new(app); +/// +/// let deadline = tokio::time::sleep(Duration::from_millis(100)); +/// let edits = dom.render_with_deadline(deadline).await; +/// ``` +/// +/// ## Use with streaming +/// +/// If not all rendering is done by the deadline, it might be worthwhile to stream the rest later. To do this, we +/// suggest rendering with a deadline, and then looping between [`VirtualDom::wait_for_work`] and render_immediate until +/// no suspended work is left. +/// +/// ```rust, ignore +/// let dom = VirtualDom::new(app); +/// +/// let deadline = tokio::time::sleep(Duration::from_millis(20)); +/// let edits = dom.render_with_deadline(deadline).await; +/// +/// real_dom.apply(edits); +/// +/// while dom.has_suspended_work() { +/// dom.wait_for_work().await; +/// real_dom.apply(dom.render_immediate()); /// } /// ``` pub struct VirtualDom { - scopes: ScopeArena, + pub(crate) templates: FxHashMap>, + pub(crate) scopes: Slab>, + pub(crate) dirty_scopes: BTreeSet, + pub(crate) scheduler: Rc, - pending_messages: VecDeque, - dirty_scopes: IndexSet, + // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template + pub(crate) elements: Slab, - channel: ( - UnboundedSender, - UnboundedReceiver, - ), + // While diffing we need some sort of way of breaking off a stream of suspended mutations. + pub(crate) scope_stack: Vec, + pub(crate) collected_leaves: Vec, + + // Whenever a suspense tree is finished, we push its boundary onto this stack. + // When "render_with_deadline" is called, we pop the stack and return the mutations + pub(crate) finished_fibers: Vec, + + pub(crate) rx: futures_channel::mpsc::UnboundedReceiver, + + pub(crate) mutations: Mutations<'static>, } -/// The type of message that can be sent to the scheduler. -/// -/// These messages control how the scheduler will process updates to the UI. -#[derive(Debug)] -pub enum SchedulerMsg { - /// Events from the Renderer - Event(UserEvent), - - /// Immediate updates from Components that mark them as dirty - Immediate(ScopeId), - - /// Mark all components as dirty and update them - DirtyAll, - - /// New tasks from components that should be polled when the next poll is ready - NewTask(ScopeId), -} - -// Methods to create the VirtualDom impl VirtualDom { /// Create a new VirtualDom with a component that does not have special props. /// @@ -154,8 +184,8 @@ impl VirtualDom { /// ``` /// /// Note: the VirtualDom is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it. - pub fn new(root: Component) -> Self { - Self::new_with_props(root, ()) + pub fn new(app: fn(Scope) -> Element) -> Self { + Self::new_with_props(app, ()) } /// Create a new VirtualDom with the given properties for the root component. @@ -188,139 +218,193 @@ impl VirtualDom { /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" }); /// let mutations = dom.rebuild(); /// ``` - pub fn new_with_props

(root: Component

, root_props: P) -> Self - where - P: 'static, - { - Self::new_with_props_and_scheduler( - root, - root_props, - futures_channel::mpsc::unbounded::(), - ) - } + pub fn new_with_props(root: fn(Scope

) -> Element, root_props: P) -> Self { + let (tx, rx) = futures_channel::mpsc::unbounded(); + let mut dom = Self { + rx, + scheduler: Scheduler::new(tx), + templates: Default::default(), + scopes: Slab::default(), + elements: Default::default(), + scope_stack: Vec::new(), + dirty_scopes: BTreeSet::new(), + collected_leaves: Vec::new(), + finished_fibers: Vec::new(), + mutations: Mutations::default(), + }; - /// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler - /// - /// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the - /// VirtualDom to be created just to retrieve its channel receiver. - /// - /// ```rust, ignore - /// let channel = futures_channel::mpsc::unbounded(); - /// let dom = VirtualDom::new_with_scheduler(Example, (), channel); - /// ``` - pub fn new_with_props_and_scheduler( - root: Component

, - root_props: P, - channel: ( - UnboundedSender, - UnboundedReceiver, - ), - ) -> Self { - let scopes = ScopeArena::new(channel.0.clone()); - - scopes.new_with_key( - root as ComponentPtr, - Box::new(VComponentProps { - props: root_props, - memo: |_a, _b| unreachable!("memo on root will neve be run"), - render_fn: root, - }), - None, - ElementId(0), - 0, + let root = dom.new_scope( + Box::new(VProps::new(root, |_, _| unreachable!(), root_props)), + "app", ); - Self { - scopes, - channel, - dirty_scopes: IndexSet::from_iter([ScopeId(0)]), - pending_messages: VecDeque::new(), - } + // The root component is always a suspense boundary for any async children + // This could be unexpected, so we might rethink this behavior later + // + // We *could* just panic if the suspense boundary is not found + root.provide_context(Rc::new(SuspenseContext::new(ScopeId(0)))); + + // Unlike react, we provide a default error boundary that just renders the error as a string + 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 } - /// Get the [`Scope`] for the root component. - /// - /// This is useful for traversing the tree from the root for heuristics or alternative renderers that use Dioxus - /// directly. - /// - /// This method is equivalent to calling `get_scope(ScopeId(0))` - /// - /// # Example - /// - /// ```rust, ignore - /// let mut dom = VirtualDom::new(example); - /// dom.rebuild(); - /// - /// - /// ``` - pub fn base_scope(&self) -> &ScopeState { - self.get_scope(ScopeId(0)).unwrap() - } - - /// Get the [`ScopeState`] for a component given its [`ScopeId`] - /// - /// # Example - /// - /// + /// Get the state for any scope given its ID /// + /// 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_scope(id) + self.scopes.get(id.0).map(|f| f.as_ref()) } - /// Get an [`UnboundedSender`] handle to the channel used by the scheduler. + /// Get the single scope at the top of the VirtualDom tree that will always be around /// - /// # Example - /// - /// ```rust, ignore - /// let dom = VirtualDom::new(App); - /// let sender = dom.get_scheduler_channel(); - /// ``` - pub fn get_scheduler_channel(&self) -> UnboundedSender { - self.channel.0.clone() + /// 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() } - /// Try to get an element from its ElementId - pub fn get_element(&self, id: ElementId) -> Option<&VNode> { - self.scopes.get_element(id) + /// Build the virtualdom with a global context inserted into the base scope + /// + /// This is useful for what is essentially dependency injection when building the app + pub fn with_root_context(self, context: T) -> Self { + self.base_scope().provide_context(context); + self } - /// Add a new message to the scheduler queue directly. + /// Manually mark a scope as requiring a re-render /// - /// - /// This method makes it possible to send messages to the scheduler from outside the VirtualDom without having to - /// call `get_schedule_channel` and then `send`. - /// - /// # Example - /// ```rust, ignore - /// let dom = VirtualDom::new(App); - /// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - /// ``` - pub fn handle_message(&mut self, msg: SchedulerMsg) { - if self.channel.0.unbounded_send(msg).is_ok() { - self.process_all_messages(); + /// 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) { + let height = scope.height; + self.dirty_scopes.insert(DirtyScope { height, id }); } } - /// Check if the [`VirtualDom`] has any pending updates or work to be done. + /// Determine whether or not a scope is currently in a suspended state /// - /// # Example + /// 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] + .consume_context::>() + .unwrap() + .waiting_on + .borrow() + .is_empty() + } + + /// Determine if the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is + /// ready to be rendered. + pub fn has_suspended_work(&self) -> bool { + !self.scheduler.leaves.borrow().is_empty() + } + + /// Call a listener inside the VirtualDom with data from outside the VirtualDom. /// - /// ```rust, ignore - /// let dom = VirtualDom::new(App); + /// This method will identify the appropriate element. The data must match up with the listener delcared. Note that + /// this method does not give any indication as to the success of the listener call. If the listener is not found, + /// nothing will happen. /// - /// // the dom is "dirty" when it is started and must be rebuilt to get the first render - /// assert!(dom.has_any_work()); - /// ``` - pub fn has_work(&self) -> bool { - !(self.dirty_scopes.is_empty() && self.pending_messages.is_empty()) + /// It is up to the listeners themselves to mark nodes as dirty. + /// + /// If you have multiple events, you can call this method multiple times before calling "render_with_deadline" + pub fn handle_event( + &mut self, + name: &str, + data: Rc, + element: ElementId, + bubbles: bool, + ) { + /* + ------------------------ + The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when + we find the target path. + + With the target path, we try and move up to the parent until there is no parent. + Due to how bubbling works, we call the listeners before walking to the parent. + + If we wanted to do capturing, then we would accumulate all the listeners and call them in reverse order. + ---------------------- + + For a visual demonstration, here we present a tree on the left and whether or not a listener is collected on the + right. + + | <-- yes (is ascendant) + | | | <-- no (is not direct ascendant) + | | <-- yes (is ascendant) + | | | | | <--- target element, break early, don't check other listeners + | | | <-- no, broke early + | <-- no, broke early + */ + let mut parent_path = self.elements.get(element.0); + let mut listeners = vec![]; + + // 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)), + 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 target_path = el_ref.path; + + for (idx, attr) in template.dynamic_attrs.iter().enumerate() { + let this_path = template.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) { + listeners.push(&attr.value); + + // Break if the event doesn't bubble anyways + if !bubbles { + break; + } + + // Break if this is the exact target element. + // This means we won't call two listeners with the same name on the same element. This should be + // documented, or be rejected from the rsx! macro outright + if target_path == this_path { + break; + } + } + } + + // Now that we've accumulated all the parent attributes for the target element, call them in reverse order + // We check the bubble state between each call to see if the event has been stopped from bubbling + for listener in listeners.drain(..).rev() { + if let AttributeValue::Listener(listener) = listener { + if let Some(cb) = listener.borrow_mut().as_deref_mut() { + cb(uievent.clone()); + } + + if !uievent.propogates.get() { + return; + } + } + } + + parent_path = template.parent.and_then(|id| self.elements.get(id.0)); + } } /// Wait for the scheduler to have any work. /// - /// This method polls the internal future queue *and* the scheduler channel. - /// To add work to the VirtualDom, insert a message via the scheduler channel. + /// This method polls the internal future queue, waiting for suspense nodes, tasks, or other work. This completes when + /// any work is ready. If multiple scopes are marked dirty from a task or a suspense tree is finished, this method + /// will exit. /// - /// This lets us poll async tasks during idle periods without blocking the main thread. + /// This method is cancel-safe, so you're fine to discard the future in a select block. + /// + /// This lets us poll async tasks and suspended trees during idle periods without blocking the main thread. /// /// # Example /// @@ -329,420 +413,218 @@ impl VirtualDom { /// let sender = dom.get_scheduler_channel(); /// ``` pub async fn wait_for_work(&mut self) { + let mut some_msg = None; + loop { - if !self.dirty_scopes.is_empty() && self.pending_messages.is_empty() { - break; - } + match some_msg.take() { + // If a bunch of messages are ready in a sequence, try to pop them off synchronously + Some(msg) => match msg { + SchedulerMsg::Immediate(id) => self.mark_dirty(id), + SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), + SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), + }, - if self.pending_messages.is_empty() { - if self.scopes.tasks.has_tasks() { - use futures_util::future::{select, Either}; + // If they're not ready, then we should wait for them to be ready + None => { + match self.rx.try_next() { + Ok(Some(val)) => some_msg = Some(val), + Ok(None) => return, + Err(_) => { + // If we have any dirty scopes, or finished fiber trees then we should exit + if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() { + return; + } - let scopes = &mut self.scopes; - let task_poll = poll_fn(|cx| { - let mut tasks = scopes.tasks.tasks.borrow_mut(); - tasks.retain(|_, task| task.as_mut().poll(cx).is_pending()); - - match tasks.is_empty() { - true => Poll::Ready(()), - false => Poll::Pending, + some_msg = self.rx.next().await } - }); - - match select(task_poll, self.channel.1.next()).await { - Either::Left((_, _)) => {} - Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()), } - } else { - self.pending_messages - .push_front(self.channel.1.next().await.unwrap()); - } - } - - // Move all the messages into the queue - self.process_all_messages(); - } - } - - /// Manually kick the VirtualDom to process any - pub fn process_all_messages(&mut self) { - // clear out the scheduler queue - while let Ok(Some(msg)) = self.channel.1.try_next() { - self.pending_messages.push_front(msg); - } - - // process all the messages pulled from the queue - while let Some(msg) = self.pending_messages.pop_back() { - self.process_message(msg); - } - } - - /// Handle an individual message for the scheduler. - /// - /// This will either call an event listener or mark a component as dirty. - pub fn process_message(&mut self, msg: SchedulerMsg) { - match msg { - SchedulerMsg::NewTask(_id) => { - // uh, not sure? I think end up re-polling it anyways - } - SchedulerMsg::Event(event) => { - if let Some(element) = event.element { - self.scopes.call_listener_with_bubbling(&event, element); - } - } - SchedulerMsg::Immediate(s) => { - self.dirty_scopes.insert(s); - } - SchedulerMsg::DirtyAll => { - for id in self.scopes.scopes.borrow().keys() { - self.dirty_scopes.insert(*id); } } } } - /// Run the virtualdom with a deadline. - /// - /// This method will perform any outstanding diffing work and try to return as many mutations as possible before the - /// deadline is reached. This method accepts a closure that returns `true` if the deadline has been reached. To wrap - /// your future into a deadline, consider the `now_or_never` method from `future_utils`. - /// - /// ```rust, ignore - /// let mut vdom = VirtualDom::new(App); - /// - /// let timeout = TimeoutFuture::from_ms(16); - /// let deadline = || (&mut timeout).now_or_never(); - /// - /// let mutations = vdom.work_with_deadline(deadline); - /// ``` - /// - /// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent - /// "jank". It will try to finish whatever work it has by the deadline to free up time for other work. - /// - /// If the work is not finished by the deadline, Dioxus will store it for later and return when work_with_deadline - /// is called again. This means you can ensure some level of free time on the VirtualDom's thread during the work phase. - /// - /// For use in the web, it is expected that this method will be called to be executed during "idle times" and the - /// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft - /// entirely jank-free applications that perform a ton of work. - /// - /// In general use, Dioxus is plenty fast enough to not need to worry about this. - /// - /// # Example - /// - /// ```rust, ignore - /// fn App(cx: Scope) -> Element { - /// cx.render(rsx!( div {"hello"} )) - /// } - /// - /// let mut dom = VirtualDom::new(App); - /// - /// loop { - /// let mut timeout = TimeoutFuture::from_ms(16); - /// let deadline = move || (&mut timeout).now_or_never(); - /// - /// let mutations = dom.run_with_deadline(deadline).await; - /// - /// apply_mutations(mutations); - /// } - /// ``` - #[allow(unused)] - pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec { - let mut committed_mutations = vec![]; - - while !self.dirty_scopes.is_empty() { - let scopes = &self.scopes; - let mut diff_state = DiffState::new(scopes); - - let mut ran_scopes = FxHashSet::default(); - - // Sort the scopes by height. Theoretically, we'll de-duplicate scopes by height - self.dirty_scopes - .retain(|id| scopes.get_scope(*id).is_some()); - - self.dirty_scopes.sort_by(|a, b| { - let h1 = scopes.get_scope(*a).unwrap().height; - let h2 = scopes.get_scope(*b).unwrap().height; - h1.cmp(&h2).reverse() - }); - - if let Some(scopeid) = self.dirty_scopes.pop() { - if !ran_scopes.contains(&scopeid) { - ran_scopes.insert(scopeid); - - self.scopes.run_scope(scopeid); - - diff_state.diff_scope(scopeid); - - let DiffState { mutations, .. } = diff_state; - - for scope in &mutations.dirty_scopes { - self.dirty_scopes.remove(scope); - } - - if !mutations.edits.is_empty() { - committed_mutations.push(mutations); - } - - // todo: pause the diff machine - // if diff_state.work(&mut deadline) { - // let DiffState { mutations, .. } = diff_state; - // for scope in &mutations.dirty_scopes { - // self.dirty_scopes.remove(scope); - // } - // committed_mutations.push(mutations); - // } else { - // // leave the work in an incomplete state - // // - // // todo: we should store the edits and re-apply them later - // // for now, we just dump the work completely (threadsafe) - // return committed_mutations; - // } - } + /// Process all events in the queue until there are no more left + pub fn process_events(&mut self) { + while let Ok(Some(msg)) = self.rx.try_next() { + match msg { + SchedulerMsg::Immediate(id) => self.mark_dirty(id), + SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), + SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), } } - - committed_mutations } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch. /// - /// The diff machine expects the RealDom's stack to be the root of the application. + /// The mutations item expects the RealDom's stack to be the root of the application. /// /// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the /// root component will be ran once and then diffed. All updates will flow out as mutations. /// /// All state stored in components will be completely wiped away. /// + /// Any templates previously registered will remain. + /// /// # Example /// ```rust, ignore /// static App: Component = |cx| cx.render(rsx!{ "hello world" }); + /// /// let mut dom = VirtualDom::new(); /// let edits = dom.rebuild(); /// /// apply_edits(edits); /// ``` pub fn rebuild(&mut self) -> Mutations { - let scope_id = ScopeId(0); - let mut diff_state = DiffState::new(&self.scopes); + match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { + // Rebuilding implies we append the created elements to the root + RenderReturn::Sync(Ok(node)) => { + let m = self.create_scope(ScopeId(0), node); + self.mutations.edits.push(Mutation::AppendChildren { + id: ElementId(0), + m, + }); + } + // If an error occurs, we should try to render the default error component and context where the error occured + RenderReturn::Sync(Err(e)) => panic!("Cannot catch errors during rebuild {:?}", e), + RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"), + } - self.scopes.run_scope(scope_id); - - diff_state.element_stack.push(ElementId(0)); - diff_state.scope_stack.push(scope_id); - - let node = self.scopes.fin_head(scope_id); - let created = diff_state.create_node(node); - - diff_state.mutations.append_children(created as u32); - - self.dirty_scopes.clear(); - assert!(self.dirty_scopes.is_empty()); - - diff_state.mutations + self.finalize() } - /// Compute a manual diff of the VirtualDom between states. - /// - /// This can be useful when state inside the DOM is remotely changed from the outside, but not propagated as an event. - /// - /// In this case, every component will be diffed, even if their props are memoized. This method is intended to be used - /// to force an update of the DOM when the state of the app is changed outside of the app. - /// - /// To force a reflow of the entire VirtualDom, use `ScopeId(0)` as the scope_id. - /// - /// # Example - /// ```rust, ignore - /// #[derive(PartialEq, Props)] - /// struct AppProps { - /// value: Shared<&'static str>, - /// } - /// - /// static App: Component = |cx| { - /// let val = cx.value.borrow(); - /// cx.render(rsx! { div { "{val}" } }) - /// }; - /// - /// let value = Rc::new(RefCell::new("Hello")); - /// let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone(), }); - /// - /// let _ = dom.rebuild(); - /// - /// *value.borrow_mut() = "goodbye"; - /// - /// let edits = dom.hard_diff(ScopeId(0)); - /// ``` - pub fn hard_diff(&mut self, scope_id: ScopeId) -> Mutations { - let mut diff_machine = DiffState::new(&self.scopes); - self.scopes.run_scope(scope_id); + /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress + /// suspended subtrees. + pub fn render_immediate(&mut self) -> Mutations { + // Build a waker that won't wake up since our deadline is already expired when it's polled + let waker = futures_util::task::noop_waker(); + let mut cx = std::task::Context::from_waker(&waker); - let (old, new) = ( - diff_machine.scopes.wip_head(scope_id), - diff_machine.scopes.fin_head(scope_id), - ); + // Now run render with deadline but dont even try to poll any async tasks + let fut = self.render_with_deadline(std::future::ready(())); + pin_mut!(fut); - diff_machine.force_diff = true; - diff_machine.scope_stack.push(scope_id); - let scope = diff_machine.scopes.get_scope(scope_id).unwrap(); - diff_machine.element_stack.push(scope.container); - - diff_machine.diff_node(old, new); - - diff_machine.mutations + // The root component is not allowed to be async + match fut.poll(&mut cx) { + std::task::Poll::Ready(mutations) => mutations, + std::task::Poll::Pending => panic!("render_immediate should never return pending"), + } } - /// Renders an `rsx` call into the Base Scope's allocator. + /// Render what you can given the timeline and then move on /// - /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + /// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues. /// - /// ```rust, ignore - /// fn Base(cx: Scope) -> Element { - /// render!(div {}) - /// } - /// - /// let dom = VirtualDom::new(Base); - /// let nodes = dom.render_nodes(rsx!("div")); - /// ``` - pub fn render_vnodes<'a>(&'a self, lazy_nodes: LazyNodes<'a, '_>) -> &'a VNode<'a> { - let scope = self.scopes.get_scope(ScopeId(0)).unwrap(); - let frame = scope.wip_frame(); - let factory = NodeFactory::new(scope); - let node = lazy_nodes.call(factory); - frame.bump.alloc(node) + /// If no suspense trees are present + pub async fn render_with_deadline(&mut self, deadline: impl Future) -> Mutations { + pin_mut!(deadline); + + self.process_events(); + + loop { + // first, unload any complete suspense trees + for finished_fiber in self.finished_fibers.drain(..) { + let scope = &mut self.scopes[finished_fiber.0]; + let context = scope.has_context::>().unwrap(); + + self.mutations + .templates + .append(&mut context.mutations.borrow_mut().templates); + + self.mutations + .edits + .append(&mut context.mutations.borrow_mut().edits); + + // TODO: count how many nodes are on the stack? + self.mutations.push(Mutation::ReplaceWith { + id: context.placeholder.get().unwrap(), + m: 1, + }) + } + + // Next, diff any dirty scopes + // We choose not to poll the deadline since we complete pretty quickly anyways + if let Some(dirty) = self.dirty_scopes.iter().next().cloned() { + 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) { + continue; + } + + // if the scope is currently suspended, then we should skip it, ignoring any tasks calling for an update + if self.is_scope_suspended(dirty.id) { + continue; + } + + // Save the current mutations length so we can split them into boundary + let mutations_to_this_point = self.mutations.edits.len(); + + // Run the scope and get the mutations + self.run_scope(dirty.id); + self.diff_scope(dirty.id); + + // 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] + .consume_context::>() + .unwrap(); + + let boundary_mut = boundary.borrow_mut(); + + // Attach mutations + boundary_mut + .mutations + .borrow_mut() + .edits + .extend(self.mutations.edits.split_off(mutations_to_this_point)); + + // Attach suspended leaves + boundary + .waiting_on + .borrow_mut() + .extend(self.collected_leaves.drain(..)); + } + } + + // If there's more work, then just continue, plenty of work to do + if !self.dirty_scopes.is_empty() { + continue; + } + + // If there's no pending suspense, then we have no reason to wait for anything + if self.scheduler.leaves.borrow().is_empty() { + return self.finalize(); + } + + // Poll the suspense leaves in the meantime + let mut work = self.wait_for_work(); + + // safety: this is okay since we don't touch the original future + let pinned = unsafe { std::pin::Pin::new_unchecked(&mut work) }; + + // If the deadline is exceded (left) then we should return the mutations we have + use futures_util::future::{select, Either}; + if let Either::Left((_, _)) = select(&mut deadline, pinned).await { + // release the borrowed + drop(work); + return self.finalize(); + } + } } - /// Renders an `rsx` call into the Base Scope's allocator. - /// - /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. - /// - /// ```rust, ignore - /// fn Base(cx: Scope) -> Element { - /// render!(div {}) - /// } - /// - /// let dom = VirtualDom::new(Base); - /// let nodes = dom.render_nodes(rsx!("div")); - /// ``` - pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { - let mut machine = DiffState::new(&self.scopes); - machine.element_stack.push(ElementId(0)); - machine.scope_stack.push(ScopeId(0)); - machine.diff_node(old, new); - - machine.mutations - } - - /// Renders an `rsx` call into the Base Scope's allocator. - /// - /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. - /// - /// - /// ```rust, ignore - /// fn Base(cx: Scope) -> Element { - /// render!(div {}) - /// } - /// - /// let dom = VirtualDom::new(Base); - /// let nodes = dom.render_nodes(rsx!("div")); - /// ``` - pub fn create_vnodes<'a>(&'a self, nodes: LazyNodes<'a, '_>) -> Mutations<'a> { - let mut machine = DiffState::new(&self.scopes); - machine.scope_stack.push(ScopeId(0)); - machine.element_stack.push(ElementId(0)); - let node = self.render_vnodes(nodes); - let created = machine.create_node(node); - machine.mutations.append_children(created as u32); - machine.mutations - } - - /// Renders an `rsx` call into the Base Scopes's arena. - /// - /// Useful when needing to diff two rsx! calls from outside the VirtualDom, such as in a test. - /// - /// - /// ```rust, ignore - /// fn Base(cx: Scope) -> Element { - /// render!(div {}) - /// } - /// - /// let dom = VirtualDom::new(Base); - /// let nodes = dom.render_nodes(rsx!("div")); - /// ``` - pub fn diff_lazynodes<'a>( - &'a self, - left: LazyNodes<'a, '_>, - right: LazyNodes<'a, '_>, - ) -> (Mutations<'a>, Mutations<'a>) { - let (old, new) = (self.render_vnodes(left), self.render_vnodes(right)); - - let mut create = DiffState::new(&self.scopes); - create.scope_stack.push(ScopeId(0)); - create.element_stack.push(ElementId(0)); - let created = create.create_node(old); - create.mutations.append_children(created as u32); - - let mut edit = DiffState::new(&self.scopes); - edit.scope_stack.push(ScopeId(0)); - edit.element_stack.push(ElementId(0)); - edit.diff_node(old, new); - - (create.mutations, edit.mutations) + /// 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 } } -/* -Scopes and ScopeArenas are never dropped internally. -An app will always occupy as much memory as its biggest form. - -This means we need to handle all specifics of drop *here*. It's easier -to reason about centralizing all the drop logic in one spot rather than scattered in each module. - -Broadly speaking, we want to use the remove_nodes method to clean up *everything* -This will drop listeners, borrowed props, and hooks for all components. -We need to do this in the correct order - nodes at the very bottom must be dropped first to release -the borrow chain. - -Once the contents of the tree have been cleaned up, we can finally clean up the -memory used by ScopeState itself. - -questions: -should we build a vcomponent for the root? -- probably - yes? -- store the vcomponent in the root dom - -- 1: Use remove_nodes to use the ensure_drop_safety pathway to safely drop the tree -- 2: Drop the ScopeState itself -*/ impl Drop for VirtualDom { fn drop(&mut self) { - // the best way to drop the dom is to replace the root scope with a dud - // the diff infrastructure will then finish the rest - let scope = self.scopes.get_scope(ScopeId(0)).unwrap(); - - // todo: move the remove nodes method onto scopearena - // this will clear *all* scopes *except* the root scope - let mut machine = DiffState::new(&self.scopes); - machine.remove_nodes([scope.root_node()], false); - - // Now, clean up the root scope - // safety: there are no more references to the root scope - let scope = unsafe { &mut *self.scopes.get_scope_raw(ScopeId(0)).unwrap() }; - scope.reset(); - - // make sure there are no "live" components - for (_, scopeptr) in self.scopes.scopes.get_mut().drain() { - // safety: all scopes were made in the bump's allocator - // They are never dropped until now. The only way to drop is through Box. - let scope = unsafe { bumpalo::boxed::Box::from_raw(scopeptr) }; - drop(scope); - } - - for scopeptr in self.scopes.free_scopes.get_mut().drain(..) { - // safety: all scopes were made in the bump's allocator - // They are never dropped until now. The only way to drop is through Box. - let mut scope = unsafe { bumpalo::boxed::Box::from_raw(scopeptr) }; - scope.reset(); - drop(scope); - } + // Simply drop this scope which drops all of its children + self.drop_scope(ScopeId(0)); } } diff --git a/packages/core/tests/.rustfmt.toml b/packages/core/tests/.rustfmt.toml new file mode 100644 index 000000000..01c43e05f --- /dev/null +++ b/packages/core/tests/.rustfmt.toml @@ -0,0 +1,2 @@ +struct_variant_width = 100 +struct_lit_width = 80 diff --git a/packages/dioxus/tests/README.md b/packages/core/tests/README.md similarity index 90% rename from packages/dioxus/tests/README.md rename to packages/core/tests/README.md index 63293c741..eea7cec3a 100644 --- a/packages/dioxus/tests/README.md +++ b/packages/core/tests/README.md @@ -1,9 +1,6 @@ # Testing of Dioxus core -NodeFactory -- [] rsx, html, NodeFactory generate the same structures - Diffing - [x] create elements - [x] create text @@ -19,15 +16,13 @@ Diffing - [x] keyed diffing - [x] keyed diffing out of order - [x] keyed diffing with prefix/suffix -- [x] suspended nodes work +- [x] suspended nodes work Lifecycle - [] Components mount properly - [] Components create new child components - [] Replaced components unmount old components and mount new - [] Post-render effects are called -- [] - Shared Context - [] Shared context propagates downwards @@ -37,7 +32,7 @@ Suspense - [] use_suspense generates suspended nodes -Hooks +Hooks - [] Drop order is maintained - [] Shared hook state is okay - [] use_hook works diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs new file mode 100644 index 000000000..8889f77c1 --- /dev/null +++ b/packages/core/tests/attr_cleanup.rs @@ -0,0 +1,75 @@ +//! dynamic attributes in dioxus necessitate an allocated node ID. +//! +//! This tests to ensure we clean it up + +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +#[test] +fn attrs_cycle() { + let mut dom = VirtualDom::new(|cx| { + let id = cx.generation(); + match cx.generation() % 2 { + 0 => cx.render(rsx! { + div {} + }), + 1 => cx.render(rsx! { + div { + h1 { class: "{id}", id: "{id}" } + } + }), + _ => unreachable!(), + } + }); + + assert_eq!( + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + AppendChildren { m: 1, id: ElementId(0) }, + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + 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 }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + ReplaceWith { id: ElementId(2), m: 1 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + 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 }, + ReplaceWith { id: ElementId(1), m: 1 } + ] + ); + + // we take the node taken by attributes since we reused it + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + ReplaceWith { id: ElementId(2), m: 1 } + ] + ); +} diff --git a/packages/core/tests/boolattrs.rs b/packages/core/tests/boolattrs.rs new file mode 100644 index 000000000..5cd2ad1ad --- /dev/null +++ b/packages/core/tests/boolattrs.rs @@ -0,0 +1,15 @@ +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +#[test] +fn bool_test() { + let mut app = VirtualDom::new(|cx| cx.render(rsx!(div { hidden: false }))); + assert_eq!( + app.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + SetBoolAttribute { name: "hidden", value: false, id: ElementId(1,) }, + AppendChildren { m: 1, id: ElementId(0) }, + ] + ) +} diff --git a/packages/core/tests/borrowedstate.rs b/packages/core/tests/borrowedstate.rs new file mode 100644 index 000000000..0aace9c97 --- /dev/null +++ b/packages/core/tests/borrowedstate.rs @@ -0,0 +1,55 @@ +#![allow(non_snake_case)] + +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +#[test] +fn test_borrowed_state() { + let mut dom = VirtualDom::new(Parent); + + 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,) }, + HydrateText { path: &[0,], value: "Hello w1!", id: ElementId(4,) }, + ReplacePlaceholder { path: &[1,], m: 1 }, + ReplacePlaceholder { path: &[0,], m: 1 }, + AppendChildren { m: 1, id: ElementId(0) }, + ] + ) +} + +fn Parent(cx: Scope) -> Element { + let w1 = cx.use_hook(|| String::from("w1")); + + cx.render(rsx! { + div { + Child { name: w1 } + } + }) +} + +#[derive(Props)] +struct ChildProps<'a> { + name: &'a str, +} + +fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { + cx.render(rsx! { + div { + h1 { "it's nested" } + Child2 { name: cx.props.name } + } + }) +} + +#[derive(Props)] +struct Grandchild<'a> { + name: &'a str, +} + +fn Child2<'a>(cx: Scope<'a, Grandchild<'a>>) -> Element { + cx.render(rsx!(div { "Hello {cx.props.name}!" })) +} diff --git a/packages/core/tests/bubble_error.rs b/packages/core/tests/bubble_error.rs new file mode 100644 index 000000000..3dbfe80a9 --- /dev/null +++ b/packages/core/tests/bubble_error.rs @@ -0,0 +1,28 @@ +//! we should properly bubble up errors from components + +use dioxus::prelude::*; + +fn app(cx: Scope) -> Element { + let raw = match cx.generation() % 2 { + 0 => "123.123", + 1 => "123.123.123", + _ => unreachable!(), + }; + + let value = raw.parse::()?; + + cx.render(rsx! { + div { "hello {value}" } + }) +} + +#[test] +fn bubbles_error() { + let mut dom = VirtualDom::new(app); + + let _edits = dom.rebuild().santize(); + + dom.mark_dirty(ScopeId(0)); + + _ = dom.render_immediate(); +} diff --git a/packages/core/tests/context_api.rs b/packages/core/tests/context_api.rs new file mode 100644 index 000000000..696810cb0 --- /dev/null +++ b/packages/core/tests/context_api.rs @@ -0,0 +1,51 @@ +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +#[test] +fn state_shares() { + fn app(cx: Scope) -> Element { + cx.provide_context(cx.generation() as i32); + + cx.render(rsx!(child_1 {})) + } + + fn child_1(cx: Scope) -> Element { + cx.render(rsx!(child_2 {})) + } + + fn child_2(cx: Scope) -> Element { + let value = cx.consume_context::().unwrap(); + cx.render(rsx!("Value is {value}")) + } + + let mut dom = VirtualDom::new(app); + assert_eq!( + dom.rebuild().santize().edits, + [ + CreateTextNode { value: "Value is 0", id: ElementId(1,) }, + AppendChildren { m: 1, id: ElementId(0) }, + ] + ); + + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + assert_eq!(dom.base_scope().consume_context::().unwrap(), 1); + + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + assert_eq!(dom.base_scope().consume_context::().unwrap(), 2); + + dom.mark_dirty(ScopeId(2)); + assert_eq!( + dom.render_immediate().santize().edits, + [SetText { value: "Value is 2", id: ElementId(1,) },] + ); + + dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId(2)); + let edits = dom.render_immediate(); + assert_eq!( + edits.santize().edits, + [SetText { value: "Value is 3", id: ElementId(1,) },] + ); +} diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs new file mode 100644 index 000000000..5e5005d3f --- /dev/null +++ b/packages/core/tests/create_dom.rs @@ -0,0 +1,197 @@ +#![allow(unused, non_upper_case_globals, non_snake_case)] + +//! Prove that the dom works normally through virtualdom methods. +//! +//! This methods all use "rebuild" which completely bypasses the scheduler. +//! Hard rebuilds don't consume any events from the event queue. + +use dioxus::core::Mutation::*; +use dioxus::prelude::*; +use dioxus_core::ElementId; + +#[test] +fn test_original_diff() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + div { + div { + "Hello, world!" + } + } + }) + }); + + let edits = dom.rebuild().santize(); + + assert_eq!( + edits.edits, + [ + // add to root + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + AppendChildren { m: 1, id: ElementId(0) } + ] + ) +} + +#[test] +fn create() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + div { + div { + "Hello, world!" + div { + div { + Fragment { + "hello" + "world" + } + } + } + } + } + }) + }); + + let _edits = dom.rebuild().santize(); + + // todo: we don't test template mutations anymore since the templates are passed along + + // assert_eq!( + // edits.templates, + // [ + // // create template + // CreateElement { name: "div" }, + // CreateElement { name: "div" }, + // CreateStaticText { value: "Hello, world!" }, + // CreateElement { name: "div" }, + // CreateElement { name: "div" }, + // CreateStaticPlaceholder {}, + // AppendChildren { m: 1 }, + // AppendChildren { m: 1 }, + // AppendChildren { m: 2 }, + // AppendChildren { m: 1 }, + // SaveTemplate { name: "template", m: 1 }, + // // The fragment child template + // CreateStaticText { value: "hello" }, + // CreateStaticText { value: "world" }, + // SaveTemplate { name: "template", m: 2 }, + // ] + // ); +} + +#[test] +fn create_list() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + (0..3).map(|f| rsx!( div { "hello" } )) + }) + }); + + let _edits = dom.rebuild().santize(); + + // note: we dont test template edits anymore + // assert_eq!( + // edits.templates, + // [ + // // create template + // CreateElement { name: "div" }, + // CreateStaticText { value: "hello" }, + // AppendChildren { m: 1 }, + // SaveTemplate { name: "template", m: 1 } + // ] + // ); +} + +#[test] +fn create_simple() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + div {} + div {} + div {} + div {} + }) + }); + + let edits = dom.rebuild().santize(); + + // note: we dont test template edits anymore + // assert_eq!( + // edits.templates, + // [ + // // create template + // CreateElement { name: "div" }, + // CreateElement { name: "div" }, + // CreateElement { name: "div" }, + // CreateElement { name: "div" }, + // // add to root + // SaveTemplate { name: "template", m: 4 } + // ] + // ); +} +#[test] +fn create_components() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + Child { "abc1" } + Child { "abc2" } + Child { "abc3" } + }) + }); + + #[derive(Props)] + struct ChildProps<'a> { + children: Element<'a>, + } + + fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { + cx.render(rsx! { + h1 {} + div { &cx.props.children } + p {} + }) + } + + let _edits = dom.rebuild().santize(); + + // todo: test this +} + +#[test] +fn anchors() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + if true { + rsx!( div { "hello" } ) + } + if false { + rsx!( div { "goodbye" } ) + } + }) + }); + + // note that the template under "false" doesn't show up since it's not loaded + let edits = dom.rebuild().santize(); + + // note: we dont test template edits anymore + // assert_eq!( + // edits.templates, + // [ + // // create each template + // CreateElement { name: "div" }, + // CreateStaticText { value: "hello" }, + // AppendChildren { m: 1 }, + // SaveTemplate { m: 1, name: "template" }, + // ] + // ); + + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + CreatePlaceholder { id: ElementId(2) }, + AppendChildren { m: 2, id: ElementId(0) } + ] + ) +} diff --git a/packages/core/tests/create_element.rs b/packages/core/tests/create_element.rs new file mode 100644 index 000000000..a4497ba74 --- /dev/null +++ b/packages/core/tests/create_element.rs @@ -0,0 +1,32 @@ +// use dioxus::core::Mutation::*; +use dioxus::prelude::*; + +#[test] +fn multiroot() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + div { "Hello a" } + div { "Hello b" } + div { "Hello c" } + }) + }); + + // note: we dont test template edits anymore + let _templates = dom.rebuild().santize().templates; + + // assert_eq!( + // dom.rebuild().santize().templates, + // [ + // CreateElement { name: "div" }, + // CreateStaticText { value: "Hello a" }, + // AppendChildren { m: 1 }, + // CreateElement { name: "div" }, + // CreateStaticText { value: "Hello b" }, + // AppendChildren { m: 1 }, + // CreateElement { name: "div" }, + // CreateStaticText { value: "Hello c" }, + // AppendChildren { m: 1 }, + // SaveTemplate { name: "template", m: 3 } + // ] + // ) +} diff --git a/packages/core/tests/create_fragments.rs b/packages/core/tests/create_fragments.rs new file mode 100644 index 000000000..ee312f34b --- /dev/null +++ b/packages/core/tests/create_fragments.rs @@ -0,0 +1,104 @@ +//! Do we create fragments properly across complex boundaries? + +use dioxus::core::Mutation::*; +use dioxus::prelude::*; +use dioxus_core::ElementId; + +#[test] +fn empty_fragment_creates_nothing() { + fn app(cx: Scope) -> Element { + cx.render(rsx!(())) + } + + let mut vdom = VirtualDom::new(app); + let edits = vdom.rebuild(); + + assert_eq!( + edits.edits, + [ + CreatePlaceholder { id: ElementId(1) }, + AppendChildren { id: ElementId(0), m: 1 } + ] + ); +} + +#[test] +fn root_fragments_work() { + let mut vdom = VirtualDom::new(|cx| { + cx.render(rsx!( + div { "hello" } + div { "goodbye" } + )) + }); + + assert_eq!( + vdom.rebuild().edits.last().unwrap(), + &AppendChildren { id: ElementId(0), m: 2 } + ); +} + +#[test] +fn fragments_nested() { + let mut vdom = VirtualDom::new(|cx| { + cx.render(rsx!( + div { "hello" } + div { "goodbye" } + rsx! { + div { "hello" } + div { "goodbye" } + rsx! { + div { "hello" } + div { "goodbye" } + rsx! { + div { "hello" } + div { "goodbye" } + } + } + } + )) + }); + + assert_eq!( + vdom.rebuild().edits.last().unwrap(), + &AppendChildren { id: ElementId(0), m: 8 } + ); +} + +#[test] +fn fragments_across_components() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + demo_child {} + demo_child {} + demo_child {} + demo_child {} + }) + } + + fn demo_child(cx: Scope) -> Element { + let world = "world"; + cx.render(rsx! { + "hellO!" + world + }) + } + + assert_eq!( + VirtualDom::new(app).rebuild().edits.last().unwrap(), + &AppendChildren { id: ElementId(0), m: 8 } + ); +} + +#[test] +fn list_fragments() { + fn app(cx: Scope) -> Element { + cx.render(rsx!( + h1 {"hello"} + (0..6).map(|f| rsx!( span { "{f}" })) + )) + } + assert_eq!( + VirtualDom::new(app).rebuild().edits.last().unwrap(), + &AppendChildren { id: ElementId(0), m: 7 } + ); +} diff --git a/packages/core/tests/create_lists.rs b/packages/core/tests/create_lists.rs new file mode 100644 index 000000000..ec3e8ecf2 --- /dev/null +++ b/packages/core/tests/create_lists.rs @@ -0,0 +1,72 @@ +use dioxus::core::Mutation::*; +use dioxus::prelude::*; +use dioxus_core::ElementId; + +// A real-world usecase of templates at peak performance +// In react, this would be a lot of node creation. +// +// In Dioxus, we memoize the rsx! body and simplify it down to a few template loads +// +// Also note that the IDs increase linearly. This lets us drive a vec on the renderer for O(1) re-indexing +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + (0..3).map(|i| rsx! { + div { + h1 { "hello world! "} + p { "{i}" } + } + }) + } + }) +} + +#[test] +fn list_renders() { + let mut dom = VirtualDom::new(app); + + let edits = dom.rebuild().santize(); + + // note: we dont test template edits anymore + // assert_eq!( + // edits.templates, + // [ + // // Create the outer div + // CreateElement { name: "div" }, + // // todo: since this is the only child, we should just use + // // append when modify the values (IE no need for a placeholder) + // CreateStaticPlaceholder, + // AppendChildren { m: 1 }, + // SaveTemplate { name: "template", m: 1 }, + // // Create the inner template div + // CreateElement { name: "div" }, + // CreateElement { name: "h1" }, + // CreateStaticText { value: "hello world! " }, + // AppendChildren { m: 1 }, + // CreateElement { name: "p" }, + // CreateTextPlaceholder, + // AppendChildren { m: 1 }, + // AppendChildren { m: 2 }, + // SaveTemplate { name: "template", m: 1 } + // ], + // ); + + assert_eq!( + edits.edits, + [ + // Load the outer div + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + // Load each template one-by-one, rehydrating it + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + HydrateText { path: &[1, 0], value: "0", id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + HydrateText { path: &[1, 0], value: "1", id: ElementId(5) }, + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + HydrateText { path: &[1, 0], value: "2", id: ElementId(7) }, + // Replace the 0th childn on the div with the 3 templates on the stack + ReplacePlaceholder { m: 3, path: &[0] }, + // Append the container div to the dom + AppendChildren { m: 1, id: ElementId(0) } + ], + ) +} diff --git a/packages/core/tests/create_passthru.rs b/packages/core/tests/create_passthru.rs new file mode 100644 index 000000000..b329fe51a --- /dev/null +++ b/packages/core/tests/create_passthru.rs @@ -0,0 +1,106 @@ +use dioxus::core::Mutation::*; +use dioxus::prelude::*; +use dioxus_core::ElementId; + +/// Should push the text node onto the stack and modify it +#[test] +fn nested_passthru_creates() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + pass_thru { + pass_thru { + pass_thru { + div { "hi" } + } + } + } + }) + } + + #[inline_props] + fn pass_thru<'a>(cx: Scope<'a>, children: Element<'a>) -> Element { + cx.render(rsx!(children)) + } + + let mut dom = VirtualDom::new(app); + let edits = dom.rebuild().santize(); + + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + AppendChildren { m: 1, id: ElementId(0) }, + ] + ) +} + +/// Should load all the templates and append them +/// +/// Take note on how we don't spit out the template for child_comp since it's entirely dynamic +#[test] +fn nested_passthru_creates_add() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + child_comp { + "1" + child_comp { + "2" + child_comp { + "3" + div { + "hi" + } + } + } + } + }) + } + + #[inline_props] + fn child_comp<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { children }) + } + + let mut dom = VirtualDom::new(app); + + assert_eq!( + dom.rebuild().santize().edits, + [ + // load 1 + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + // load 2 + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + // load 3 + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + // load div that contains 4 + LoadTemplate { name: "template", index: 1, id: ElementId(4) }, + AppendChildren { id: ElementId(0), m: 4 }, + ] + ); +} + +/// note that the template is all dynamic roots - so it doesn't actually get cached as a template +#[test] +fn dynamic_node_as_root() { + fn app(cx: Scope) -> Element { + let a = 123; + let b = 456; + cx.render(rsx! { "{a}" "{b}" }) + } + + let mut dom = VirtualDom::new(app); + let edits = dom.rebuild().santize(); + + // Since the roots were all dynamic, they should not cause any template muations + assert!(edits.templates.is_empty()); + + // The root node is text, so we just create it on the spot + assert_eq!( + edits.edits, + [ + CreateTextNode { value: "123", id: ElementId(1) }, + CreateTextNode { value: "456", id: ElementId(2) }, + AppendChildren { id: ElementId(0), m: 2 } + ] + ) +} diff --git a/packages/core/tests/cycle.rs b/packages/core/tests/cycle.rs new file mode 100644 index 000000000..133b04658 --- /dev/null +++ b/packages/core/tests/cycle.rs @@ -0,0 +1,51 @@ +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +/// As we clean up old templates, the ID for the node should cycle +#[test] +fn cycling_elements() { + let mut dom = VirtualDom::new(|cx| { + cx.render(match cx.generation() % 2 { + 0 => rsx! { div { "wasd" } }, + 1 => rsx! { div { "abcd" } }, + _ => unreachable!(), + }) + }); + + 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!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); + + // notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); +} diff --git a/packages/core/tests/diff_component.rs b/packages/core/tests/diff_component.rs new file mode 100644 index 000000000..2b74f8302 --- /dev/null +++ b/packages/core/tests/diff_component.rs @@ -0,0 +1,102 @@ +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +/// When returning sets of components, we do a light diff of the contents to preserve some react-like functionality +/// +/// This means that nav_bar should never get re-created and that we should only be swapping out +/// different pointers +#[test] +fn component_swap() { + fn app(cx: Scope) -> Element { + let render_phase = cx.use_hook(|| 0); + + *render_phase += 1; + + cx.render(match *render_phase { + 0 => rsx! { + nav_bar {} + dash_board {} + }, + 1 => rsx! { + nav_bar {} + dash_results {} + }, + 2 => rsx! { + nav_bar {} + dash_board {} + }, + 3 => rsx! { + nav_bar {} + dash_results {} + }, + 4 => rsx! { + nav_bar {} + dash_board {} + }, + _ => rsx!("blah"), + }) + } + + fn nav_bar(cx: Scope) -> Element { + cx.render(rsx! { + h1 { + "NavBar" + (0..3).map(|_| rsx!(nav_link {})) + } + }) + } + + fn nav_link(cx: Scope) -> Element { + cx.render(rsx!( h1 { "nav_link" } )) + } + + fn dash_board(cx: Scope) -> Element { + cx.render(rsx!( div { "dashboard" } )) + } + + fn dash_results(cx: Scope) -> Element { + cx.render(rsx!( div { "results" } )) + } + + 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) } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + ReplaceWith { id: ElementId(5), m: 1 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + ReplaceWith { id: ElementId(6), m: 1 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + ReplaceWith { id: ElementId(5), m: 1 } + ] + ); +} diff --git a/packages/core/tests/diff_element.rs b/packages/core/tests/diff_element.rs new file mode 100644 index 000000000..3ab9b284a --- /dev/null +++ b/packages/core/tests/diff_element.rs @@ -0,0 +1,84 @@ +use dioxus::core::Mutation::*; +use dioxus::prelude::*; +use dioxus_core::ElementId; + +#[test] +fn text_diff() { + fn app(cx: Scope) -> Element { + let gen = cx.generation(); + cx.render(rsx!( h1 { "hello {gen}" } )) + } + + let mut vdom = VirtualDom::new(app); + _ = vdom.rebuild(); + + vdom.mark_dirty(ScopeId(0)); + assert_eq!( + vdom.render_immediate().edits, + [SetText { value: "hello 1", id: ElementId(2) }] + ); + + vdom.mark_dirty(ScopeId(0)); + assert_eq!( + vdom.render_immediate().edits, + [SetText { value: "hello 2", id: ElementId(2) }] + ); + + vdom.mark_dirty(ScopeId(0)); + assert_eq!( + vdom.render_immediate().edits, + [SetText { value: "hello 3", id: ElementId(2) }] + ); +} + +#[test] +fn element_swap() { + fn app(cx: Scope) -> Element { + let gen = cx.generation(); + + match gen % 2 { + 0 => cx.render(rsx!( h1 { "hello 1" } )), + 1 => cx.render(rsx!( h2 { "hello 2" } )), + _ => unreachable!(), + } + } + + let mut vdom = VirtualDom::new(app); + _ = vdom.rebuild(); + + vdom.mark_dirty(ScopeId(0)); + assert_eq!( + vdom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); + + vdom.mark_dirty(ScopeId(0)); + assert_eq!( + vdom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, + ] + ); + + vdom.mark_dirty(ScopeId(0)); + assert_eq!( + vdom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + ReplaceWith { id: ElementId(1,), m: 1 }, + ] + ); + + vdom.mark_dirty(ScopeId(0)); + assert_eq!( + vdom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, + ] + ); +} diff --git a/packages/core/tests/diff_keyed_list.rs b/packages/core/tests/diff_keyed_list.rs new file mode 100644 index 000000000..81013849d --- /dev/null +++ b/packages/core/tests/diff_keyed_list.rs @@ -0,0 +1,356 @@ +//! Diffing Tests +//! +//! These tests only verify that the diffing algorithm works properly for single components. +//! +//! It does not validated that component lifecycles work properly. This is done in another test file. + +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +/// Should result in moves, but not removals or additions +#[test] +fn keyed_diffing_out_of_order() { + let mut dom = VirtualDom::new(|cx| { + let order = match cx.generation() % 2 { + 0 => &[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9], + 1 => &[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9], + _ => unreachable!(), + }; + + 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) }, + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().edits, + [ + PushRoot { id: ElementId(7,) }, + InsertBefore { id: ElementId(5,), m: 1 }, + ] + ) +} + +/// Should result in moves only +#[test] +fn keyed_diffing_out_of_order_adds() { + let mut dom = VirtualDom::new(|cx| { + let order = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7, 8 /**/], + 1 => &[/**/ 8, 7, 4, 5, 6 /**/], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().edits, + [ + PushRoot { id: ElementId(5,) }, + PushRoot { id: ElementId(4,) }, + InsertBefore { id: ElementId(1,), m: 2 }, + ] + ) +} + +/// Should result in moves only +#[test] +fn keyed_diffing_out_of_order_adds_3() { + let mut dom = VirtualDom::new(|cx| { + let order = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7, 8 /**/], + 1 => &[/**/ 4, 8, 7, 5, 6 /**/], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().edits, + [ + PushRoot { id: ElementId(5,) }, + PushRoot { id: ElementId(4,) }, + InsertBefore { id: ElementId(2,), m: 2 }, + ] + ); +} + +/// Should result in moves onl +#[test] +fn keyed_diffing_out_of_order_adds_4() { + let mut dom = VirtualDom::new(|cx| { + let order = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7, 8 /**/], + 1 => &[/**/ 4, 5, 8, 7, 6 /**/], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().edits, + [ + PushRoot { id: ElementId(5,) }, + PushRoot { id: ElementId(4,) }, + InsertBefore { id: ElementId(3,), m: 2 }, + ] + ); +} + +/// Should result in moves onl +#[test] +fn keyed_diffing_out_of_order_adds_5() { + let mut dom = VirtualDom::new(|cx| { + let order = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7, 8 /**/], + 1 => &[/**/ 4, 5, 6, 8, 7 /**/], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().edits, + [ + PushRoot { id: ElementId(5,) }, + InsertBefore { id: ElementId(4,), m: 1 }, + ] + ); +} + +/// Should result in moves onl +#[test] +fn keyed_diffing_additions() { + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7, 8 /**/], + 1 => &[/**/ 4, 5, 6, 7, 8, 9, 10 /**/], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + LoadTemplate { name: "template", index: 0, id: ElementId(7) }, + InsertAfter { id: ElementId(5), m: 2 } + ] + ); +} + +#[test] +fn keyed_diffing_additions_and_moves_on_ends() { + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[/**/ 4, 5, 6, 7 /**/], + 1 => &[/**/ 7, 4, 5, 6, 11, 12 /**/], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + // create 11, 12 + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + InsertAfter { id: ElementId(3), m: 2 }, + // move 7 to the front + PushRoot { id: ElementId(4) }, + InsertBefore { id: ElementId(1), m: 1 } + ] + ); +} + +#[test] +fn keyed_diffing_additions_and_moves_in_middle() { + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[/**/ 1, 2, 3, 4 /**/], + 1 => &[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + // LIS: 4, 5, 6 + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + // create 5, 6 + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + InsertBefore { id: ElementId(3), m: 2 }, + // create 7, 8 + LoadTemplate { name: "template", index: 0, id: ElementId(7) }, + LoadTemplate { name: "template", index: 0, id: ElementId(8) }, + InsertBefore { id: ElementId(2), m: 2 }, + // move 7 + PushRoot { id: ElementId(4) }, + InsertBefore { id: ElementId(1), m: 1 } + ] + ); +} + +#[test] +fn controlled_keyed_diffing_out_of_order() { + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[4, 5, 6, 7], + 1 => &[0, 5, 9, 6, 4], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + // LIS: 5, 6 + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + // remove 7 + Remove { id: ElementId(4,) }, + // move 4 to after 6 + PushRoot { id: ElementId(1) }, + InsertAfter { id: ElementId(3,), m: 1 }, + // create 9 and insert before 6 + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + InsertBefore { id: ElementId(3,), m: 1 }, + // create 0 and insert before 5 + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + InsertBefore { id: ElementId(2,), m: 1 }, + ] + ); +} + +#[test] +fn controlled_keyed_diffing_out_of_order_max_test() { + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[0, 1, 2, 3, 4], + 1 => &[3, 0, 1, 10, 2], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + Remove { id: ElementId(5,) }, + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + InsertBefore { id: ElementId(3,), m: 1 }, + PushRoot { id: ElementId(4) }, + InsertBefore { id: ElementId(1,), m: 1 }, + ] + ); +} + +// noticed some weird behavior in the desktop interpreter +// just making sure it doesnt happen in the core implementation +#[test] +fn remove_list() { + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[9, 8, 7, 6, 5], + 1 => &[9, 8], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + Remove { id: ElementId(5) }, + Remove { id: ElementId(4) }, + Remove { id: ElementId(3) }, + ] + ); +} + +#[test] +fn no_common_keys() { + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[1, 2, 3], + 1 => &[4, 5, 6], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + Remove { id: ElementId(3) }, + Remove { id: ElementId(2) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + ReplaceWith { id: ElementId(1), m: 3 } + ] + ); +} diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs new file mode 100644 index 000000000..09bf3b215 --- /dev/null +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -0,0 +1,380 @@ +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; +use pretty_assertions::assert_eq; + +#[test] +fn list_creates_one_by_one() { + let mut dom = VirtualDom::new(|cx| { + let gen = cx.generation(); + + cx.render(rsx! { + div { + (0..gen).map(|i| rsx! { + div { "{i}" } + }) + } + }) + }); + + // load the div and then assign the empty fragment as a placeholder + assert_eq!( + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + AssignId { path: &[0], id: ElementId(2,) }, + AppendChildren { id: ElementId(0), m: 1 }, + ] + ); + + // Rendering the first item should replace the placeholder with an element + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(3,) }, + HydrateText { path: &[0], value: "0", id: ElementId(4,) }, + ReplaceWith { id: ElementId(2,), m: 1 }, + ] + ); + + // Rendering the next item should insert after the previous + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, + HydrateText { path: &[0], value: "1", id: ElementId(5,) }, + InsertAfter { id: ElementId(3,), m: 1 }, + ] + ); + + // ... and again! + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(6,) }, + HydrateText { path: &[0], value: "2", id: ElementId(7,) }, + InsertAfter { id: ElementId(2,), m: 1 }, + ] + ); + + // once more + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(8,) }, + HydrateText { path: &[0], value: "3", id: ElementId(9,) }, + InsertAfter { id: ElementId(6,), m: 1 }, + ] + ); +} + +#[test] +fn removes_one_by_one() { + let mut dom = VirtualDom::new(|cx| { + let gen = 3 - cx.generation() % 4; + + cx.render(rsx! { + div { + (0..gen).map(|i| rsx! { + div { "{i}" } + }) + } + }) + }); + + // load the div and then assign the empty fragment as a placeholder + assert_eq!( + dom.rebuild().santize().edits, + [ + // The container + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + // each list item + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + HydrateText { path: &[0], value: "0", id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + HydrateText { path: &[0], value: "1", id: ElementId(5) }, + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + HydrateText { path: &[0], value: "2", id: ElementId(7) }, + // replace the placeholder in the template with the 3 templates on the stack + ReplacePlaceholder { m: 3, path: &[0] }, + // Mount the div + AppendChildren { id: ElementId(0), m: 1 } + ] + ); + + // Remove div(3) + // Rendering the first item should replace the placeholder with an element + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [Remove { id: ElementId(6) }] + ); + + // Remove div(2) + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [Remove { id: ElementId(4) }] + ); + + // Remove div(1) and replace with a placeholder + // todo: this should just be a remove with no placeholder + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + CreatePlaceholder { id: ElementId(4) }, + ReplaceWith { id: ElementId(2), m: 1 } + ] + ); + + // load the 3 and replace the placeholder + // todo: this should actually be append to, but replace placeholder is fine for now + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + HydrateText { path: &[0], value: "0", id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + HydrateText { path: &[0], value: "1", id: ElementId(6) }, + LoadTemplate { name: "template", index: 0, id: ElementId(7) }, + HydrateText { path: &[0], value: "2", id: ElementId(8) }, + ReplaceWith { id: ElementId(4), m: 3 } + ] + ); +} + +#[test] +fn list_shrink_multiroot() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + div { + (0..cx.generation()).map(|i| rsx! { + div { "{i}" } + div { "{i}" } + }) + } + }) + }); + + assert_eq!( + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1,) }, + AssignId { path: &[0,], id: ElementId(2,) }, + AppendChildren { id: ElementId(0), m: 1 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + HydrateText { path: &[0], value: "0", id: ElementId(4) }, + LoadTemplate { name: "template", index: 1, id: ElementId(5) }, + HydrateText { path: &[0], value: "0", id: ElementId(6) }, + ReplaceWith { id: ElementId(2), m: 2 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + HydrateText { path: &[0], value: "1", id: ElementId(7) }, + LoadTemplate { name: "template", index: 1, id: ElementId(8) }, + HydrateText { path: &[0], value: "1", id: ElementId(9) }, + InsertAfter { id: ElementId(5), m: 2 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(10) }, + HydrateText { path: &[0], value: "2", id: ElementId(11) }, + LoadTemplate { name: "template", index: 1, id: ElementId(12) }, + HydrateText { path: &[0], value: "2", id: ElementId(13) }, + InsertAfter { id: ElementId(8), m: 2 } + ] + ); +} + +#[test] +fn removes_one_by_one_multiroot() { + let mut dom = VirtualDom::new(|cx| { + let gen = 3 - cx.generation() % 4; + + cx.render(rsx! { + div { + (0..gen).map(|i| rsx! { + div { "{i}" } + div { "{i}" } + }) + } + }) + }); + + // load the div and then assign the empty fragment as a placeholder + assert_eq!( + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + // + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + HydrateText { path: &[0], value: "0", id: ElementId(3) }, + LoadTemplate { name: "template", index: 1, id: ElementId(4) }, + HydrateText { path: &[0], value: "0", id: ElementId(5) }, + // + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + HydrateText { path: &[0], value: "1", id: ElementId(7) }, + LoadTemplate { name: "template", index: 1, id: ElementId(8) }, + HydrateText { path: &[0], value: "1", id: ElementId(9) }, + // + LoadTemplate { name: "template", index: 0, id: ElementId(10) }, + HydrateText { path: &[0], value: "2", id: ElementId(11) }, + LoadTemplate { name: "template", index: 1, id: ElementId(12) }, + HydrateText { path: &[0], value: "2", id: ElementId(13) }, + // + ReplacePlaceholder { path: &[0], m: 6 }, + // + AppendChildren { id: ElementId(0), m: 1 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [Remove { id: ElementId(10) }, Remove { id: ElementId(12) }] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [Remove { id: ElementId(6) }, Remove { id: ElementId(8) }] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + CreatePlaceholder { id: ElementId(8) }, + Remove { id: ElementId(2) }, + ReplaceWith { id: ElementId(4), m: 1 } + ] + ); +} + +#[test] +fn two_equal_fragments_are_equal_static() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + (0..5).map(|_| rsx! { + div { "hello" } + }) + }) + }); + + _ = dom.rebuild(); + assert!(dom.render_immediate().edits.is_empty()); +} + +#[test] +fn two_equal_fragments_are_equal() { + let mut dom = VirtualDom::new(|cx| { + cx.render(rsx! { + (0..5).map(|i| rsx! { + div { "hello {i}" } + }) + }) + }); + + _ = dom.rebuild(); + assert!(dom.render_immediate().edits.is_empty()); +} + +#[test] +fn remove_many() { + let mut dom = VirtualDom::new(|cx| { + let num = match cx.generation() % 3 { + 0 => 0, + 1 => 1, + 2 => 5, + _ => unreachable!(), + }; + + cx.render(rsx! { + (0..num).map(|i| rsx! { div { "hello {i}" } }) + }) + }); + + 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(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, + [ + 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/hotreloading.rs b/packages/core/tests/hotreloading.rs new file mode 100644 index 000000000..8e9f3400b --- /dev/null +++ b/packages/core/tests/hotreloading.rs @@ -0,0 +1 @@ +//! It should be possible to swap out templates at runtime, enabling hotreloading diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs new file mode 100644 index 000000000..fca6fcbaa --- /dev/null +++ b/packages/core/tests/kitchen_sink.rs @@ -0,0 +1,42 @@ +use dioxus::core::{ElementId, Mutation}; +use dioxus::prelude::*; + +fn basic_syntax_is_a_template(cx: Scope) -> Element { + let asd = 123; + let var = 123; + + cx.render(rsx! { + div { key: "12345", + class: "asd", + class: "{asd}", + onclick: move |_| {}, + div { "{var}" } + div { + h1 { "var" } + p { "you're great!" } + div { background_color: "red", + h1 { "var" } + div { b { "asd" } "not great" } + } + p { "you're great!" } + } + } + }) +} +#[test] +fn dual_stream() { + let mut dom = VirtualDom::new(basic_syntax_is_a_template); + let edits = dom.rebuild().santize(); + + use Mutation::*; + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + SetAttribute { name: "class", value: "123", 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 } + ], + ); +} diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs new file mode 100644 index 000000000..04f6bc3cf --- /dev/null +++ b/packages/core/tests/lifecycle.rs @@ -0,0 +1,168 @@ +#![allow(unused, non_upper_case_globals)] +#![allow(non_snake_case)] + +//! Tests for the lifecycle of components. +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; + +type Shared = Arc>; + +#[test] +fn manual_diffing() { + struct AppProps { + value: Shared<&'static str>, + } + + fn app(cx: Scope) -> Element { + let val = cx.props.value.lock().unwrap(); + cx.render(rsx! { div { "{val}" } }) + }; + + let value = Arc::new(Mutex::new("Hello")); + let mut dom = VirtualDom::new_with_props(app, AppProps { value: value.clone() }); + + let _ = dom.rebuild(); + + *value.lock().unwrap() = "goodbye"; + + assert_eq!( + dom.rebuild().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + HydrateText { path: &[0], value: "goodbye", id: ElementId(4) }, + AppendChildren { m: 1, id: ElementId(0) } + ] + ); +} + +#[test] +fn events_generate() { + fn app(cx: Scope) -> Element { + let count = cx.use_hook(|| 0); + + match *count { + 0 => cx.render(rsx! { + div { onclick: move |_| *count += 1, + div { "nested" } + "Click me!" + } + }), + _ => cx.render(rsx!(())), + } + }; + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true); + + dom.mark_dirty(ScopeId(0)); + let edits = dom.render_immediate(); + + assert_eq!( + edits.edits, + [ + CreatePlaceholder { id: ElementId(2) }, + ReplaceWith { id: ElementId(1), m: 1 } + ] + ) +} + +// #[test] +// fn components_generate() { +// fn app(cx: Scope) -> Element { +// let render_phase = cx.use_hook(|| 0); +// *render_phase += 1; + +// cx.render(match *render_phase { +// 1 => rsx_without_templates!("Text0"), +// 2 => rsx_without_templates!(div {}), +// 3 => rsx_without_templates!("Text2"), +// 4 => rsx_without_templates!(Child {}), +// 5 => rsx_without_templates!({ None as Option<()> }), +// 6 => rsx_without_templates!("text 3"), +// 7 => rsx_without_templates!({ (0..2).map(|f| rsx_without_templates!("text {f}")) }), +// 8 => rsx_without_templates!(Child {}), +// _ => todo!(), +// }) +// }; + +// fn Child(cx: Scope) -> Element { +// println!("Running child"); +// cx.render(rsx_without_templates! { +// h1 {} +// }) +// } + +// let mut dom = VirtualDom::new(app); +// let edits = dom.rebuild(); +// assert_eq!( +// edits.edits, +// [ +// CreateTextNode { root: Some(1), text: "Text0" }, +// AppendChildren { root: Some(0), children: vec![1] } +// ] +// ); + +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateElement { root: Some(2), tag: "div", children: 0 }, +// ReplaceWith { root: Some(1), nodes: vec![2] } +// ] +// ); + +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateTextNode { root: Some(1), text: "Text2" }, +// ReplaceWith { root: Some(2), nodes: vec![1] } +// ] +// ); + +// // child {} +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateElement { root: Some(2), tag: "h1", children: 0 }, +// ReplaceWith { root: Some(1), nodes: vec![2] } +// ] +// ); + +// // placeholder +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreatePlaceholder { root: Some(1) }, +// ReplaceWith { root: Some(2), nodes: vec![1] } +// ] +// ); + +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateTextNode { root: Some(2), text: "text 3" }, +// ReplaceWith { root: Some(1), nodes: vec![2] } +// ] +// ); + +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateTextNode { text: "text 0", root: Some(1) }, +// CreateTextNode { text: "text 1", root: Some(3) }, +// ReplaceWith { root: Some(2), nodes: vec![1, 3] }, +// ] +// ); + +// assert_eq!( +// dom.hard_diff(ScopeId(0)).edits, +// [ +// CreateElement { tag: "h1", root: Some(2), children: 0 }, +// ReplaceWith { root: Some(1), nodes: vec![2] }, +// Remove { root: Some(3) }, +// ] +// ); +// } diff --git a/packages/core/tests/miri_full_app.rs b/packages/core/tests/miri_full_app.rs new file mode 100644 index 000000000..c1c6d52cc --- /dev/null +++ b/packages/core/tests/miri_full_app.rs @@ -0,0 +1,49 @@ +use dioxus::prelude::*; +use dioxus_core::ElementId; +use std::rc::Rc; + +#[test] +fn miri_rollover() { + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + + for _ in 0..3 { + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + dom.process_events(); + _ = dom.render_immediate(); + } +} + +fn app(cx: Scope) -> Element { + let mut idx = use_state(cx, || 0); + let onhover = |_| println!("go!"); + + cx.render(rsx! { + div { + button { + onclick: move |_| { + idx += 1; + println!("Clicked"); + }, + "+" + } + button { onclick: move |_| idx -= 1, "-" } + ul { + (0..**idx).map(|i| rsx! { + child_example { i: i, onhover: onhover } + }) + } + } + }) +} + +#[inline_props] +fn child_example<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element { + cx.render(rsx! { + li { + onmouseover: move |e| onhover.call(e), + "{i}" + } + }) +} diff --git a/packages/core/tests/miri_simple.rs b/packages/core/tests/miri_simple.rs new file mode 100644 index 000000000..6da2c4bf4 --- /dev/null +++ b/packages/core/tests/miri_simple.rs @@ -0,0 +1,127 @@ +use dioxus::prelude::*; + +#[test] +fn app_drops() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + div {} + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn hooks_drop() { + fn app(cx: Scope) -> Element { + cx.use_hook(|| String::from("asd")); + cx.use_hook(|| String::from("asd")); + cx.use_hook(|| String::from("asd")); + cx.use_hook(|| String::from("asd")); + + cx.render(rsx! { + div {} + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn contexts_drop() { + fn app(cx: Scope) -> Element { + cx.provide_context(String::from("asd")); + + cx.render(rsx! { + div { + child_comp {} + } + }) + } + + fn child_comp(cx: Scope) -> Element { + let el = cx.consume_context::().unwrap(); + + cx.render(rsx! { + div { "hello {el}" } + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn tasks_drop() { + fn app(cx: Scope) -> Element { + cx.spawn(async { + // tokio::time::sleep(std::time::Duration::from_millis(100000)).await; + }); + + cx.render(rsx! { + div { } + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn root_props_drop() { + struct RootProps(String); + + let mut dom = VirtualDom::new_with_props( + |cx| cx.render(rsx!( div { "{cx.props.0}" } )), + RootProps("asdasd".to_string()), + ); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn diffing_drops_old() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + match cx.generation() % 2 { + 0 => rsx!( child_comp1 { name: "asdasd".to_string() }), + 1 => rsx!( child_comp2 { name: "asdasd".to_string() }), + _ => todo!() + } + } + }) + } + + #[inline_props] + fn child_comp1(cx: Scope, name: String) -> Element { + cx.render(rsx! { "Hello {name}" }) + } + + #[inline_props] + fn child_comp2(cx: Scope, name: String) -> Element { + cx.render(rsx! { "Goodbye {name}" }) + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + + _ = dom.render_immediate(); +} diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs new file mode 100644 index 000000000..160baa25d --- /dev/null +++ b/packages/core/tests/miri_stress.rs @@ -0,0 +1,349 @@ +#![allow(non_snake_case)] + +use std::rc::Rc; + +use dioxus::prelude::*; + +/// This test checks that we should release all memory used by the virtualdom when it exits. +/// +/// When miri runs, it'll let us know if we leaked or aliased. +#[test] +fn test_memory_leak() { + fn app(cx: Scope) -> Element { + let val = cx.generation(); + + cx.spawn(async {}); + + if val == 2 || val == 4 { + return cx.render(rsx!(())); + } + + let name = cx.use_hook(|| String::from("numbers: ")); + + name.push_str("123 "); + + cx.render(rsx!( + div { "Hello, world!" } + Child {} + Child {} + Child {} + Child {} + Child {} + Child {} + BorrowedChild { name: name } + BorrowedChild { name: name } + BorrowedChild { name: name } + BorrowedChild { name: name } + BorrowedChild { name: name } + )) + } + + #[derive(Props)] + struct BorrowedProps<'a> { + name: &'a str, + } + + fn BorrowedChild<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element { + cx.render(rsx! { + div { + "goodbye {cx.props.name}" + Child {} + Child {} + } + }) + } + + fn Child(cx: Scope) -> Element { + render!(div { "goodbye world" }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + + for _ in 0..5 { + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + } +} + +#[test] +fn memo_works_properly() { + fn app(cx: Scope) -> Element { + let val = cx.generation(); + + if val == 2 || val == 4 { + return cx.render(rsx!(())); + } + + let name = cx.use_hook(|| String::from("asd")); + + cx.render(rsx!( + div { "Hello, world! {name}" } + Child { na: "asdfg".to_string() } + )) + } + + #[derive(PartialEq, Props)] + struct ChildProps { + na: String, + } + + fn Child(cx: Scope) -> Element { + render!(div { "goodbye world" }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + // todo!() + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); +} + +#[test] +fn free_works_on_root_hooks() { + /* + On Drop, scopearena drops all the hook contents. and props + */ + #[derive(PartialEq, Clone, Props)] + struct AppProps { + inner: Rc, + } + + fn app(cx: Scope) -> Element { + let name: &AppProps = cx.use_hook(|| cx.props.clone()); + render!(child_component { inner: name.inner.clone() }) + } + + fn child_component(cx: Scope) -> Element { + render!(div { "{cx.props.inner}" }) + } + + let ptr = Rc::new("asdasd".to_string()); + let mut dom = VirtualDom::new_with_props(app, AppProps { inner: ptr.clone() }); + let _ = dom.rebuild(); + + // ptr gets cloned into props and then into the hook + assert_eq!(Rc::strong_count(&ptr), 4); + + drop(dom); + + 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; + +// if *cnt == 1 { +// render!(div { Child { a: "abcdef".to_string() } }) +// } else { +// 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); + +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); + +// dbg!("forcing update to child"); + +// 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); +// } +// } diff --git a/packages/core/tests/safety.rs b/packages/core/tests/safety.rs new file mode 100644 index 000000000..8c125f4c6 --- /dev/null +++ b/packages/core/tests/safety.rs @@ -0,0 +1,24 @@ +//! Tests related to safety of the library. + +use std::rc::Rc; + +use dioxus::prelude::*; +use dioxus_core::SuspenseContext; + +/// Ensure no issues with not calling rebuild +#[test] +fn root_node_isnt_null() { + let dom = VirtualDom::new(|cx| render!("Hello world!")); + + let scope = dom.base_scope(); + + // We haven't built the tree, so trying to get out the root node should fail + assert!(scope.try_root_node().is_none()); + + // The height should be 0 + assert_eq!(scope.height(), 0); + + // There should be a default suspense context + // todo: there should also be a default error boundary + assert!(scope.has_context::>().is_some()); +} diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs new file mode 100644 index 000000000..9fac13937 --- /dev/null +++ b/packages/core/tests/suspense.rs @@ -0,0 +1,92 @@ +use dioxus::core::ElementId; +use dioxus::core::{Mutation::*, SuspenseContext}; +use dioxus::prelude::*; +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) }, + ] + ); + + // wait just a moment, not enough time for the boundary to resolve + + dom.wait_for_work().await; +} + +fn app(cx: Scope) -> Element { + cx.render(rsx!( + div { + "Waiting for child..." + suspense_boundary {} + } + )) +} + +fn suspense_boundary(cx: Scope) -> Element { + cx.use_hook(|| { + cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id()))); + }); + + // Ensure the right types are found + cx.has_context::>().unwrap(); + + cx.render(rsx!(async_child {})) +} + +async fn async_child(cx: Scope<'_>) -> Element { + use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await; + cx.render(rsx!(async_text {})) +} + +async fn async_text(cx: Scope<'_>) -> Element { + let username = use_future!(cx, || async { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + "async child 1" + }); + + let age = use_future!(cx, || async { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + 1234 + }); + + let (_user, _age) = use_future!(cx, || async { + tokio::join!( + tokio::time::sleep(std::time::Duration::from_secs(1)), + tokio::time::sleep(std::time::Duration::from_secs(2)) + ); + ("async child 1", 1234) + }) + .await; + + let (username, age) = tokio::join!(username.into_future(), age.into_future()); + + cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } )) +} diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs new file mode 100644 index 000000000..2704c5eb2 --- /dev/null +++ b/packages/core/tests/task.rs @@ -0,0 +1,42 @@ +//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely + +use dioxus::prelude::*; +use std::time::Duration; + +static mut POLL_COUNT: usize = 0; + +#[tokio::test] +async fn it_works() { + let mut dom = VirtualDom::new(app); + + let _ = dom.rebuild(); + + tokio::select! { + _ = dom.wait_for_work() => {} + _ = tokio::time::sleep(Duration::from_millis(500)) => {} + }; + + // By the time the tasks are finished, we should've accumulated ticks from two tasks + // Be warned that by setting the delay to too short, tokio might not schedule in the tasks + assert_eq!(unsafe { POLL_COUNT }, 135); +} + +fn app(cx: Scope) -> Element { + cx.use_hook(|| { + cx.spawn(async { + for x in 0..10 { + tokio::time::sleep(Duration::from_micros(50)).await; + unsafe { POLL_COUNT += x } + } + }); + + cx.spawn(async { + for x in 0..10 { + tokio::time::sleep(Duration::from_micros(25)).await; + unsafe { POLL_COUNT += x * 2 } + } + }); + }); + + cx.render(rsx!(())) +} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index f17593e33..4aaae6c64 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -15,36 +15,41 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] 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-rsx-interpreter = { path = "../rsx_interpreter", optional = true } serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" log = "0.4.14" -wry = { version = "0.20.2" } +wry = { version = "0.23.4" } futures-channel = "0.3.21" tokio = { version = "1.16.1", features = [ "sync", "rt-multi-thread", "rt", "time", + "macros", ], optional = true, default-features = false } -webbrowser = "0.7.1" -infer = "0.9.0" +webbrowser = "0.8.0" +infer = "0.11.0" dunce = "1.0.2" -interprocess = { version = "1.1.1", optional = true } +interprocess = { version = "1.1.1", optional = true} +futures-util = "0.3.25" + +[target.'cfg(target_os = "ios")'.dependencies] +objc = "0.2.7" +objc_id = "0.1.1" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9.3" [features] -default = ["tokio_runtime"] +default = ["tokio_runtime", "hot-reload"] tokio_runtime = ["tokio"] fullscreen = ["wry/fullscreen"] transparent = ["wry/transparent"] tray = ["wry/tray"] -hot-reload = ["dioxus-rsx-interpreter", "interprocess"] +hot-reload = ["interprocess"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 85be2727f..4456eb2d7 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -21,13 +21,14 @@ pub struct Config { pub(crate) resource_dir: Option, pub(crate) custom_head: Option, pub(crate) custom_index: Option, + pub(crate) root_name: String, } type DropHandler = Box bool>; pub(crate) type WryProtocol = ( String, - Box WryResult + 'static>, + Box>) -> WryResult>> + 'static>, ); impl Config { @@ -46,6 +47,7 @@ impl Config { resource_dir: None, custom_head: None, custom_index: None, + root_name: "main".to_string(), } } @@ -96,7 +98,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 @@ -126,6 +128,14 @@ impl Config { self.custom_index = Some(index); self } + + /// 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. + pub fn with_root_name(mut self, name: impl Into) -> Self { + self.root_name = name.into(); + self + } } impl Default for Config { diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 5153133b9..9037189b1 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -1,10 +1,16 @@ use crate::desktop_context::{DesktopContext, UserWindowEvent}; - +use crate::events::{decode_event, EventMessage}; use dioxus_core::*; +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, @@ -14,10 +20,15 @@ use wry::{ pub(super) struct DesktopController { pub(super) webviews: HashMap, - pub(super) sender: futures_channel::mpsc::UnboundedSender, + 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 { @@ -29,64 +40,65 @@ impl DesktopController { proxy: EventLoopProxy, ) -> Self { let edit_queue = Arc::new(Mutex::new(Vec::new())); - let (sender, receiver) = futures_channel::mpsc::unbounded::(); + let (event_tx, mut event_rx) = unbounded(); + let proxy2 = proxy.clone(); let pending_edits = edit_queue.clone(); - let return_sender = sender.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 "spawn" onto multiple threads + // 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_and_scheduler(root, props, (sender, receiver)); - - let window_context = DesktopContext::new(desktop_context_proxy); - - dom.base_scope().provide_context(window_context); - - // allow other proccesses to send the new rsx text to the @dioxusin ipc channel and recieve erros on the @dioxusout channel - #[cfg(feature = "hot-reload")] - crate::hot_reload::init(&dom); - - let edits = dom.rebuild(); - - edit_queue - .lock() - .unwrap() - .push(serde_json::to_string(&edits.edits).unwrap()); - - // Make sure the window is ready for any new updates - proxy.send_event(UserWindowEvent::Update).unwrap(); + 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 { - dom.wait_for_work().await; - - let muts = dom.work_with_deadline(|| false); - - for edit in muts { - edit_queue - .lock() - .unwrap() - .push(serde_json::to_string(&edit.edits).unwrap()); + tokio::select! { + _ = dom.wait_for_work() => {} + Some(json_value) = event_rx.next() => { + if let Ok(value) = serde_json::from_value::(json_value) { + let name = value.event.clone(); + let el_id = ElementId(value.mounted_dom_id); + if let Some(evt) = decode_event(value) { + dom.handle_event(&name, evt, el_id, dioxus_html::events::event_bubbles(&name)); + } + } + } } - let _ = proxy.send_event(UserWindowEvent::Update); + 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, - sender: return_sender, + 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![], } } @@ -100,13 +112,23 @@ impl DesktopController { pub(super) fn try_load_ready_webviews(&mut self) { if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { - let mut queue = self.pending_edits.lock().unwrap(); + 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 queue.drain(..) { + 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 df6de3092..756814de0 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,7 +1,16 @@ +use std::rc::Rc; + use crate::controller::DesktopController; use dioxus_core::ScopeState; +use serde::de::Error; +use serde_json::Value; +use std::future::Future; +use std::future::IntoFuture; +use std::pin::Pin; use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; +#[cfg(target_os = "ios")] +use wry::application::platform::ios::WindowExtIOS; use wry::application::window::Fullscreen as WryFullscreen; use UserWindowEvent::*; @@ -31,11 +40,18 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { pub struct DesktopContext { /// The wry/tao proxy to the current window pub proxy: ProxyType, + pub(super) eval_reciever: Rc>>, } impl DesktopContext { - pub(crate) fn new(proxy: ProxyType) -> Self { - Self { proxy } + pub(crate) fn new( + proxy: ProxyType, + eval_reciever: tokio::sync::mpsc::UnboundedReceiver, + ) -> Self { + Self { + proxy, + eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)), + } } /// trigger the drag-window event @@ -134,16 +150,34 @@ impl DesktopContext { pub fn eval(&self, script: impl std::string::ToString) { let _ = self.proxy.send_event(Eval(script.to_string())); } + + /// Push view + #[cfg(target_os = "ios")] + pub fn push_view(&self, view: objc_id::ShareId) { + let _ = self.proxy.send_event(PushView(view)); + } + + /// Push view + #[cfg(target_os = "ios")] + pub fn pop_view(&self) { + let _ = self.proxy.send_event(PopView); + } } #[derive(Debug)] pub enum UserWindowEvent { - Update, + 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), @@ -164,73 +198,155 @@ pub enum UserWindowEvent { DevTool, Eval(String), + + #[cfg(target_os = "ios")] + PushView(objc_id::ShareId), + #[cfg(target_os = "ios")] + PopView, } -pub(super) fn handler( - user_event: UserWindowEvent, - desktop: &mut DesktopController, - control_flow: &mut ControlFlow, -) { - // currently dioxus-desktop supports a single window only, - // so we can grab the only webview from the map; - let webview = desktop.webviews.values().next().unwrap(); - let window = webview.window(); +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; + }; - match user_event { - Update => desktop.try_load_ready_webviews(), - 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(|| WryFullscreen::Borderless(Some(handle)))); + 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()); } - } - FocusWindow => window.set_focus(), - Resizable(state) => window.set_resizable(state), - AlwaysOnTop(state) => window.set_always_on_top(state), - - 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}"); + 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)))); + } } - } - DevTool => { - #[cfg(debug_assertions)] - webview.open_devtools(); - #[cfg(not(debug_assertions))] - log::warn!("Devtools are disabled in release builds"); - } + FocusWindow => window.set_focus(), + Resizable(state) => window.set_resizable(state), + AlwaysOnTop(state) => window.set_always_on_top(state), - Eval(code) => { - if let Err(e) = webview.evaluate_script(code.as_str()) { - // we can't panic this error. - log::warn!("Eval script error: {e}"); + 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 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) { +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 } + } + }) +} - cx.use_hook(|| move |script| desktop.eval(script)) +/// 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>>> + } +} + +#[cfg(target_os = "ios")] +fn is_main_thread() -> bool { + use objc::runtime::{Class, BOOL, NO}; + use objc::*; + + let cls = Class::get("NSThread").unwrap(); + let result: BOOL = unsafe { msg_send![cls, isMainThread] }; + result != NO } diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 37c491a05..b82d6f749 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -1,12 +1,10 @@ //! Convert a serialized event to an event trigger -use std::any::Any; -use std::sync::Arc; - -use dioxus_core::{ElementId, EventPriority, UserEvent}; -use dioxus_html::event_bubbles; -use dioxus_html::on::*; +use dioxus_html::events::*; use serde::{Deserialize, Serialize}; +use serde_json::from_value; +use std::any::Any; +use std::rc::Rc; #[derive(Deserialize, Serialize)] pub(crate) struct IpcMessage { @@ -34,187 +32,60 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option { } } -#[derive(Deserialize, Serialize)] -struct ImEvent { - event: String, - mounted_dom_id: u64, - contents: serde_json::Value, +macro_rules! match_data { + ( + $m:ident; + $name:ident; + $( + $tip:ty => $($mname:literal)|* ; + )* + ) => { + match $name { + $( $($mname)|* => { + let val: $tip = from_value::<$tip>($m).ok()?; + Rc::new(val) as Rc + })* + _ => return None, + } + }; } -pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { - let ImEvent { - event, - mounted_dom_id, - contents, - } = serde_json::from_value(val).unwrap(); - - let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); - let name = event_name_from_type(&event); - - let event = make_synthetic_event(&event, contents); - - UserEvent { - name, - priority: EventPriority::Low, - scope_id: None, - element: mounted_dom_id, - bubbles: event_bubbles(name), - data: event, - } +#[derive(Deserialize)] +pub struct EventMessage { + pub contents: serde_json::Value, + pub event: String, + pub mounted_dom_id: usize, } -fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc { - match name { - "copy" | "cut" | "paste" => { - // - Arc::new(ClipboardData {}) - } - "compositionend" | "compositionstart" | "compositionupdate" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "keydown" | "keypress" | "keyup" => { - let evt = serde_json::from_value::(val).unwrap(); - Arc::new(evt) - } - "focus" | "blur" | "focusout" | "focusin" => { - // - Arc::new(FocusData {}) - } +pub fn decode_event(value: EventMessage) -> Option> { + let val = value.contents; + let name = value.event.as_str(); + type DragData = MouseData; - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - "change" | "input" | "invalid" | "reset" | "submit" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } + let evt = match_data! { val; name; + MouseData => "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup"; + ClipboardData => "copy" | "cut" | "paste"; + CompositionData => "compositionend" | "compositionstart" | "compositionupdate"; + KeyboardData => "keydown" | "keypress" | "keyup"; + FocusData => "blur" | "focus" | "focusin" | "focusout"; + FormData => "change" | "input" | "invalid" | "reset" | "submit"; + DragData => "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop"; + PointerData => "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture"; + SelectionData => "selectstart" | "selectionchange" | "select"; + TouchData => "touchcancel" | "touchend" | "touchmove" | "touchstart"; + ScrollData => "scroll"; + WheelData => "wheel"; + MediaData => "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" + | "encrypted" | "ended" | "interruptbegin" | "interruptend" | "loadeddata" + | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress" + | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate" + | "volumechange" | "waiting" | "error" | "load" | "loadend" | "timeout"; + AnimationData => "animationstart" | "animationend" | "animationiteration"; + TransitionData => "transitionend"; + ToggleData => "toggle"; + // ImageData => "load" | "error"; + // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange"; + }; - "click" | "contextmenu" | "dblclick" | "doubleclick" | "drag" | "dragend" | "dragenter" - | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" - | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "select" => { - // - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "touchcancel" | "touchend" | "touchmove" | "touchstart" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "scroll" => Arc::new(()), - - "wheel" => Arc::new(serde_json::from_value::(val).unwrap()), - - "animationstart" | "animationend" | "animationiteration" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "transitionend" => Arc::new(serde_json::from_value::(val).unwrap()), - - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" - | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" - | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => { - // - Arc::new(MediaData {}) - } - - "toggle" => Arc::new(ToggleData {}), - - _ => Arc::new(()), - } -} - -fn event_name_from_type(typ: &str) -> &'static str { - match typ { - "copy" => "copy", - "cut" => "cut", - "paste" => "paste", - "compositionend" => "compositionend", - "compositionstart" => "compositionstart", - "compositionupdate" => "compositionupdate", - "keydown" => "keydown", - "keypress" => "keypress", - "keyup" => "keyup", - "focus" => "focus", - "focusout" => "focusout", - "focusin" => "focusin", - "blur" => "blur", - "change" => "change", - "input" => "input", - "invalid" => "invalid", - "reset" => "reset", - "submit" => "submit", - "click" => "click", - "contextmenu" => "contextmenu", - "doubleclick" => "doubleclick", - "dblclick" => "dblclick", - "drag" => "drag", - "dragend" => "dragend", - "dragenter" => "dragenter", - "dragexit" => "dragexit", - "dragleave" => "dragleave", - "dragover" => "dragover", - "dragstart" => "dragstart", - "drop" => "drop", - "mousedown" => "mousedown", - "mouseenter" => "mouseenter", - "mouseleave" => "mouseleave", - "mousemove" => "mousemove", - "mouseout" => "mouseout", - "mouseover" => "mouseover", - "mouseup" => "mouseup", - "pointerdown" => "pointerdown", - "pointermove" => "pointermove", - "pointerup" => "pointerup", - "pointercancel" => "pointercancel", - "gotpointercapture" => "gotpointercapture", - "lostpointercapture" => "lostpointercapture", - "pointerenter" => "pointerenter", - "pointerleave" => "pointerleave", - "pointerover" => "pointerover", - "pointerout" => "pointerout", - "select" => "select", - "touchcancel" => "touchcancel", - "touchend" => "touchend", - "touchmove" => "touchmove", - "touchstart" => "touchstart", - "scroll" => "scroll", - "wheel" => "wheel", - "animationstart" => "animationstart", - "animationend" => "animationend", - "animationiteration" => "animationiteration", - "transitionend" => "transitionend", - "abort" => "abort", - "canplay" => "canplay", - "canplaythrough" => "canplaythrough", - "durationchange" => "durationchange", - "emptied" => "emptied", - "encrypted" => "encrypted", - "ended" => "ended", - "error" => "error", - "loadeddata" => "loadeddata", - "loadedmetadata" => "loadedmetadata", - "loadstart" => "loadstart", - "pause" => "pause", - "play" => "play", - "playing" => "playing", - "progress" => "progress", - "ratechange" => "ratechange", - "seeked" => "seeked", - "seeking" => "seeking", - "stalled" => "stalled", - "suspend" => "suspend", - "timeupdate" => "timeupdate", - "volumechange" => "volumechange", - "waiting" => "waiting", - "toggle" => "toggle", - a => { - panic!("unsupported event type {:?}", a); - } - } + Some(evt) } diff --git a/packages/desktop/src/hot_reload.rs b/packages/desktop/src/hot_reload.rs index 2f63162c1..e2628cdd4 100644 --- a/packages/desktop/src/hot_reload.rs +++ b/packages/desktop/src/hot_reload.rs @@ -1,7 +1,9 @@ +#![allow(dead_code)] + use dioxus_core::VirtualDom; -use dioxus_rsx_interpreter::{error::Error, ErrorHandler, SetManyRsxMessage, RSX_CONTEXT}; + use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; -use std::io::{BufRead, BufReader, Write}; +use std::io::{BufRead, BufReader}; use std::time::Duration; use std::{sync::Arc, sync::Mutex}; @@ -11,60 +13,34 @@ fn handle_error(connection: std::io::Result) -> Option>>> = Arc::new(Mutex::new(None)); + let latest_in_connection_handle = latest_in_connection.clone(); - let latest_out_connection: Arc>>> = - Arc::new(Mutex::new(None)); - let latest_out_connection_handle = latest_out_connection.clone(); - - struct DesktopErrorHandler { - latest_connection: Arc>>>, - } - impl ErrorHandler for DesktopErrorHandler { - fn handle_error(&self, err: Error) { - if let Some(conn) = &mut *self.latest_connection.lock().unwrap() { - conn.get_mut() - .write_all((serde_json::to_string(&err).unwrap() + "\n").as_bytes()) - .unwrap(); - } else { - panic!("{}", err); - } - } - } - - RSX_CONTEXT.set_error_handler(DesktopErrorHandler { - latest_connection: latest_out_connection_handle, - }); - RSX_CONTEXT.provide_scheduler_channel(dom.get_scheduler_channel()); // connect to processes for incoming data std::thread::spawn(move || { - if let Ok(listener) = LocalSocketListener::bind("@dioxusin") { + 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)); } } }); - // connect to processes for outgoing errors - std::thread::spawn(move || { - if let Ok(listener) = LocalSocketListener::bind("@dioxusout") { - for conn in listener.incoming().filter_map(handle_error) { - *latest_out_connection.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(_) => { - let msgs: SetManyRsxMessage = serde_json::from_str(&buf).unwrap(); - RSX_CONTEXT.extend(msgs); + 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 { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 20086a48d..a37ad88f8 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -8,16 +8,20 @@ mod controller; mod desktop_context; mod escape; mod events; -#[cfg(feature = "hot-reload")] -mod hot_reload; 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}; +pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult}; +use futures_channel::mpsc::UnboundedSender; pub use wry; pub use wry::application as tao; -use crate::events::trigger_from_serialized; pub use cfg::Config; use controller::DesktopController; use dioxus_core::*; @@ -100,99 +104,141 @@ 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()); - let proxy = event_loop.create_proxy(); - // We assume that if the icon is None, then the user just didnt set it + 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::WindowEvent { + event, window_id, .. + } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow), + _ => {} + }, + + 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() { - cfg.window.window.window_icon = Some( + window.set_window_icon(Some( tao::window::Icon::from_rgba( include_bytes!("./assets/default_icon.bin").to_vec(), 460, 460, ) .expect("image parse failed"), - ); + )); } - event_loop.run(move |window_event, event_loop, control_flow| { - *control_flow = ControlFlow::Wait; - - match window_event { - Event::NewEvents(StartCause::Init) => { - let builder = cfg.window.clone(); - - let window = builder.build(event_loop).unwrap(); - let window_id = window.id(); - - let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone()); - - let proxy = proxy.clone(); - - 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 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| { - parse_ipc_message(&payload) - .map(|message| match message.method() { - "user_event" => { - let event = trigger_from_serialized(message.params()); - log::trace!("User event: {:?}", event); - sender.unbounded_send(SchedulerMsg::Event(event)).unwrap(); + 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| { + parse_ipc_message(&payload) + .map(|message| match message.method() { + "eval_result" => { + let result = message.params(); + eval_sender.send(result).unwrap(); + } + "user_event" => { + _ = event_tx.unbounded_send(message.params()); + } + "initialize" => { + is_ready.store(true, std::sync::atomic::Ordering::Relaxed); + let _ = proxy.send_event(UserWindowEvent::EditsReady); + } + "browser_open" => { + let data = message.params(); + log::trace!("Open browser: {:?}", data); + if let Some(temp) = data.as_object() { + if temp.contains_key("href") { + let url = temp.get("href").unwrap().as_str().unwrap(); + if let Err(e) = webbrowser::open(url) { + log::error!("Open Browser error: {:?}", e); } - "initialize" => { - is_ready.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = proxy.send_event(UserWindowEvent::Update); - } - "browser_open" => { - let data = message.params(); - log::trace!("Open browser: {:?}", data); - if let Some(temp) = data.as_object() { - if temp.contains_key("href") { - let url = temp.get("href").unwrap().as_str().unwrap(); - if let Err(e) = webbrowser::open(url) { - log::error!("Open Browser error: {:?}", e); - } - } - } - } - _ => (), - }) - .unwrap_or_else(|| { - log::warn!("invalid IPC message received"); - }); - }) - .with_custom_protocol(String::from("dioxus"), move |r| { - protocol::desktop_handler( - r, - resource_dir.clone(), - custom_head.clone(), - index_file.clone(), - ) - }) - .with_file_drop_handler(move |window, evet| { - file_handler - .as_ref() - .map(|handler| handler(window, evet)) - .unwrap_or_default() - }); + } + } + } + _ => (), + }) + .unwrap_or_else(|| { + log::warn!("invalid IPC message received"); + }); + }) + .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) - } + 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 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(); @@ -203,32 +249,11 @@ pub fn launch_with_props(root: Component

, props: P, mut cf }); } "#, - ) - } else { - // in debug, we are okay with the reload menu showing and dev tool - webview = webview.with_devtools(true); - } + ) + } else { + // in debug, we are okay with the reload menu showing and dev tool + webview = webview.with_devtools(true); + } - desktop.webviews.insert(window_id, webview.build().unwrap()); - } - - Event::WindowEvent { - event, window_id, .. - } => match event { - WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow), - _ => {} - }, - - Event::UserEvent(user_event) => { - desktop_context::handler(user_event, &mut desktop, control_flow) - } - Event::MainEventsCleared => {} - Event::Resumed => {} - Event::Suspended => {} - Event::LoopDestroyed => {} - Event::RedrawRequested(_id) => {} - _ => {} - } - }) + webview.build().unwrap() } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 1ee89dad0..625e0abbc 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,26 +1,32 @@ use std::path::{Path, PathBuf}; use wry::{ - http::{status::StatusCode, Request, Response, ResponseBuilder}, + http::{status::StatusCode, Request, Response}, Result, }; -const MODULE_LOADER: &str = r#" +fn module_loader(root_name: &str) -> String { + format!( + " -"#; +", + root_name + ) +} pub(super) fn desktop_handler( - request: &Request, + request: &Request>, asset_root: Option, custom_head: Option, custom_index: Option, -) -> Result { + 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().replace("dioxus://", ""); + let path = request.uri().to_string().replace("dioxus://", ""); // all assets should be called from index.html let trimmed = path.trim_start_matches("index.html/"); @@ -30,25 +36,30 @@ pub(super) fn desktop_handler( // 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 - .replace("", &format!("{}", MODULE_LOADER)) + .replace("", &format!("{}", module_loader(root_name))) .into_bytes(); - ResponseBuilder::new().mimetype("text/html").body(rendered) + 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); } - template = template.replace("", MODULE_LOADER); + template = template.replace("", &module_loader(root_name)); - ResponseBuilder::new() - .mimetype("text/html") + Response::builder() + .header("Content-Type", "text/html") .body(template.into_bytes()) + .map_err(From::from) } } else if trimmed == "index.js" { - ResponseBuilder::new() - .mimetype("text/javascript") + 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())) @@ -57,20 +68,23 @@ pub(super) fn desktop_handler( let asset = asset_root.join(trimmed).canonicalize()?; if !asset.starts_with(asset_root) { - return ResponseBuilder::new() + return Response::builder() .status(StatusCode::FORBIDDEN) - .body(String::from("Forbidden").into_bytes()); + .body(String::from("Forbidden").into_bytes()) + .map_err(From::from); } if !asset.exists() { - return ResponseBuilder::new() + return Response::builder() .status(StatusCode::NOT_FOUND) - .body(String::from("Not Found").into_bytes()); + .body(String::from("Not Found").into_bytes()) + .map_err(From::from); } - ResponseBuilder::new() - .mimetype(get_mime_from_path(trimmed)?) + Response::builder() + .header("Content-Type", get_mime_from_path(trimmed)?) .body(std::fs::read(asset)?) + .map_err(From::from) } } @@ -96,10 +110,10 @@ fn get_asset_root() -> Option { #[cfg(target_os = "macos")] { let bundle = core_foundation::bundle::CFBundle::main_bundle(); - let bundle_path = dbg!(bundle.path()?); - let resources_path = dbg!(bundle.resources_path()?); - let absolute_resources_root = dbg!(bundle_path.join(resources_path)); - let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?); + let bundle_path = bundle.path()?; + let resources_path = bundle.resources_path()?; + let absolute_resources_root = bundle_path.join(resources_path); + let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?; return Some(canonical_resources_root); } @@ -113,7 +127,7 @@ fn get_mime_from_path(trimmed: &str) -> Result<&str> { return Ok("image/svg+xml"); } - let res = match infer::get_from_path(&trimmed)?.map(|f| f.mime_type()) { + let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) { Some(t) if t == "text/plain" => get_mime_by_ext(trimmed), Some(f) => f, None => get_mime_by_ext(trimmed), diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 9a376140e..801ef7ac5 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -17,22 +17,13 @@ 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-rsx-interpreter = { path = "../rsx_interpreter", optional = true } - - -# dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.2.1", optional = true } -# dioxus-native-core = { path = "./packages/native-core", version = "^0.2.0", optional = true } -# dioxus-native-core-macro = { path = "./packages/native-core-macro", version = "^0.2.0", optional = true } [features] default = ["macro", "hooks", "html"] macro = ["dioxus-core-macro", "dioxus-rsx"] html = ["dioxus-html"] hooks = ["dioxus-hooks"] -hot-reload = [ - "dioxus-core-macro/hot-reload", - "dioxus-rsx-interpreter", -] + [dev-dependencies] futures-util = "0.3.21" @@ -41,10 +32,9 @@ rand = { version = "0.8.4", features = ["small_rng"] } criterion = "0.3.5" thiserror = "1.0.30" env_logger = "0.9.0" +tokio = { version = "1.21.2", features = ["full"] } +# dioxus-edit-stream = { path = "../edit-stream" } -[[bench]] -name = "create" -harness = false [[bench]] name = "jsframework" diff --git a/packages/dioxus/benches/create.rs b/packages/dioxus/benches/create.rs deleted file mode 100644 index f328e4d9d..000000000 --- a/packages/dioxus/benches/create.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/packages/dioxus/benches/jsframework.rs b/packages/dioxus/benches/jsframework.rs index 3a7198012..24ca2fe69 100644 --- a/packages/dioxus/benches/jsframework.rs +++ b/packages/dioxus/benches/jsframework.rs @@ -5,9 +5,14 @@ //! to be made, but the change application phase will be just as performant as the vanilla wasm_bindgen code. In essence, //! we are measuring the overhead of Dioxus, not the performance of the "apply" phase. //! -//! On my MBP 2019: -//! - Dioxus takes 3ms to create 1_000 rows -//! - Dioxus takes 30ms to create 10_000 rows +//! +//! Pre-templates (Mac M1): +//! - 3ms to create 1_000 rows +//! - 30ms to create 10_000 rows +//! +//! Post-templates +//! - 580us to create 1_000 rows +//! - 6.2ms to create 10_000 rows //! //! As pure "overhead", these are amazing good numbers, mostly slowed down by hitting the global allocator. //! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster. @@ -20,25 +25,26 @@ criterion_group!(mbenches, create_rows); criterion_main!(mbenches); fn create_rows(c: &mut Criterion) { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let mut rng = SmallRng::from_entropy(); - render!(table { - tbody { - (0..10_000_usize).map(|f| { - let label = Label::new(&mut rng); - rsx!(Row { - row_id: f, - label: label + render!( + table { + tbody { + (0..10_000_usize).map(|f| { + let label = Label::new(&mut rng); + rsx!( Row { row_id: f, label: label } ) }) - }) + } } - }) - }; + ) + } c.bench_function("create rows", |b| { + let mut dom = VirtualDom::new(app); + dom.rebuild(); + b.iter(|| { - let mut dom = VirtualDom::new(App); let g = dom.rebuild(); assert!(g.edits.len() > 1); }) diff --git a/packages/dioxus/examples/stress.rs b/packages/dioxus/examples/stress.rs new file mode 100644 index 000000000..248d851d2 --- /dev/null +++ b/packages/dioxus/examples/stress.rs @@ -0,0 +1,101 @@ +use dioxus::prelude::*; +use rand::prelude::*; + +fn main() { + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + + for _ in 0..1000 { + _ = dom.rebuild(); + } +} + +fn app(cx: Scope) -> Element { + let mut rng = SmallRng::from_entropy(); + + cx.render(rsx! ( + table { + tbody { + (0..10_000_usize).map(|f| { + let label = Label::new(&mut rng); + rsx!( table_row { row_id: f, label: label } ) + }) + } + } + )) +} + +#[derive(PartialEq, Props)] +struct RowProps { + row_id: usize, + label: Label, +} +fn table_row(cx: Scope) -> Element { + let [adj, col, noun] = cx.props.label.0; + cx.render(rsx! { + tr { + td { class:"col-md-1", "{cx.props.row_id}" } + td { class:"col-md-1", onclick: move |_| { /* run onselect */ }, + a { class: "lbl", "{adj}" "{col}" "{noun}" } + } + td { class: "col-md-1", + a { class: "remove", onclick: move |_| {/* remove */}, + span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" } + } + } + td { class: "col-md-6" } + } + }) +} + +#[derive(PartialEq)] +struct Label([&'static str; 3]); + +impl Label { + fn new(rng: &mut SmallRng) -> Self { + Label([ + ADJECTIVES.choose(rng).unwrap(), + COLOURS.choose(rng).unwrap(), + NOUNS.choose(rng).unwrap(), + ]) + } +} + +static ADJECTIVES: &[&str] = &[ + "pretty", + "large", + "big", + "small", + "tall", + "short", + "long", + "handsome", + "plain", + "quaint", + "clean", + "elegant", + "easy", + "angry", + "crazy", + "helpful", + "mushy", + "odd", + "unsightly", + "adorable", + "important", + "inexpensive", + "cheap", + "expensive", + "fancy", +]; + +static COLOURS: &[&str] = &[ + "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", + "orange", +]; + +static NOUNS: &[&str] = &[ + "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", + "pizza", "mouse", "keyboard", +]; diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index a8312aa52..fed3d2bd5 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -1,5 +1,3 @@ -#![doc = include_str!("../../../notes/README.md")] - pub use dioxus_core as core; #[cfg(feature = "hooks")] @@ -7,7 +5,7 @@ pub use dioxus_hooks as hooks; pub mod events { #[cfg(feature = "html")] - pub use dioxus_html::{on::*, KeyCode}; + pub use dioxus_html::prelude::*; } #[cfg(feature = "html")] @@ -32,11 +30,5 @@ pub mod prelude { pub use dioxus_html as dioxus_elements; #[cfg(feature = "html")] - pub use dioxus_elements::{GlobalAttributes, SvgAttributes}; - - #[cfg(feature = "hot-reload")] - pub use dioxus_rsx_interpreter::{ - captuered_context::{CapturedContext, FormattedArg, IfmtArgs}, - get_line_num, resolve_scope, CodeLocation, RsxContext, - }; + pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes}; } diff --git a/packages/dioxus/tests/.rustfmt.toml b/packages/dioxus/tests/.rustfmt.toml deleted file mode 100644 index 70a9483a8..000000000 --- a/packages/dioxus/tests/.rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -struct_lit_width = 80 diff --git a/packages/dioxus/tests/borrowedstate.rs b/packages/dioxus/tests/borrowedstate.rs deleted file mode 100644 index 905339793..000000000 --- a/packages/dioxus/tests/borrowedstate.rs +++ /dev/null @@ -1,46 +0,0 @@ -#![allow(non_snake_case)] - -use dioxus::prelude::*; - -#[test] -fn test_borrowed_state() { - let _ = VirtualDom::new(Parent); -} - -fn Parent(cx: Scope) -> Element { - let value = cx.use_hook(String::new); - - cx.render(rsx! { - div { - Child { name: value } - Child { name: value } - Child { name: value } - Child { name: value } - } - }) -} - -#[derive(Props)] -struct ChildProps<'a> { - name: &'a str, -} - -fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - cx.render(rsx! { - div { - h1 { "it's nested" } - Child2 { name: cx.props.name } - } - }) -} - -#[derive(Props)] -struct Grandchild<'a> { - name: &'a str, -} - -fn Child2<'a>(cx: Scope<'a, Grandchild<'a>>) -> Element { - cx.render(rsx! { - div { "Hello {cx.props.name}!" } - }) -} diff --git a/packages/dioxus/tests/create_dom.rs b/packages/dioxus/tests/create_dom.rs deleted file mode 100644 index e66954207..000000000 --- a/packages/dioxus/tests/create_dom.rs +++ /dev/null @@ -1,211 +0,0 @@ -#![allow(unused, non_upper_case_globals, non_snake_case)] - -//! Prove that the dom works normally through virtualdom methods. -//! -//! This methods all use "rebuild" which completely bypasses the scheduler. -//! Hard rebuilds don't consume any events from the event queue. - -use dioxus::prelude::*; - -use dioxus_core::DomEdit::*; - -fn new_dom(app: Component

, props: P) -> VirtualDom { - VirtualDom::new_with_props(app, props) -} - -#[test] -fn test_original_diff() { - static APP: Component = |cx| { - cx.render(rsx! { - div { - div { - "Hello, world!" - } - } - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - assert_eq!( - mutations.edits, - [ - CreateElement { root: 1, tag: "div" }, - CreateElement { root: 2, tag: "div" }, - CreateTextNode { root: 3, text: "Hello, world!" }, - AppendChildren { many: 1 }, - AppendChildren { many: 1 }, - AppendChildren { many: 1 }, - ] - ); -} - -#[test] -fn create() { - static APP: Component = |cx| { - cx.render(rsx! { - div { - div { - "Hello, world!" - div { - div { - Fragment { - "hello" - "world" - } - } - } - } - } - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - CreateElement { root: 1, tag: "div" }, - CreateElement { root: 2, tag: "div" }, - CreateTextNode { root: 3, text: "Hello, world!" }, - CreateElement { root: 4, tag: "div" }, - CreateElement { root: 5, tag: "div" }, - CreateTextNode { root: 6, text: "hello" }, - CreateTextNode { root: 7, text: "world" }, - AppendChildren { many: 2 }, - AppendChildren { many: 1 }, - AppendChildren { many: 2 }, - AppendChildren { many: 1 }, - AppendChildren { many: 1 }, - ] - ); -} - -#[test] -fn create_list() { - static APP: Component = |cx| { - cx.render(rsx! { - {(0..3).map(|f| rsx!{ div { - "hello" - }})} - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - - // copilot wrote this test :P - assert_eq!( - mutations.edits, - [ - CreateElement { root: 1, tag: "div" }, - CreateTextNode { root: 2, text: "hello" }, - AppendChildren { many: 1 }, - CreateElement { root: 3, tag: "div" }, - CreateTextNode { root: 4, text: "hello" }, - AppendChildren { many: 1 }, - CreateElement { root: 5, tag: "div" }, - CreateTextNode { root: 6, text: "hello" }, - AppendChildren { many: 1 }, - AppendChildren { many: 3 }, - ] - ); -} - -#[test] -fn create_simple() { - static APP: Component = |cx| { - cx.render(rsx! { - div {} - div {} - div {} - div {} - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - - // copilot wrote this test :P - assert_eq!( - mutations.edits, - [ - CreateElement { root: 1, tag: "div" }, - CreateElement { root: 2, tag: "div" }, - CreateElement { root: 3, tag: "div" }, - CreateElement { root: 4, tag: "div" }, - AppendChildren { many: 4 }, - ] - ); -} -#[test] -fn create_components() { - static App: Component = |cx| { - cx.render(rsx! { - Child { "abc1" } - Child { "abc2" } - Child { "abc3" } - }) - }; - - #[derive(Props)] - struct ChildProps<'a> { - children: Element<'a>, - } - - fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - cx.render(rsx! { - h1 {} - div { &cx.props.children } - p {} - }) - } - - let mut dom = new_dom(App, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - CreateElement { root: 1, tag: "h1" }, - CreateElement { root: 2, tag: "div" }, - CreateTextNode { root: 3, text: "abc1" }, - AppendChildren { many: 1 }, - CreateElement { root: 4, tag: "p" }, - CreateElement { root: 5, tag: "h1" }, - CreateElement { root: 6, tag: "div" }, - CreateTextNode { root: 7, text: "abc2" }, - AppendChildren { many: 1 }, - CreateElement { root: 8, tag: "p" }, - CreateElement { root: 9, tag: "h1" }, - CreateElement { root: 10, tag: "div" }, - CreateTextNode { root: 11, text: "abc3" }, - AppendChildren { many: 1 }, - CreateElement { root: 12, tag: "p" }, - AppendChildren { many: 9 }, - ] - ); -} -#[test] -fn anchors() { - static App: Component = |cx| { - cx.render(rsx! { - {true.then(|| rsx!{ div { "hello" } })} - {false.then(|| rsx!{ div { "goodbye" } })} - }) - }; - - let mut dom = new_dom(App, ()); - let mutations = dom.rebuild(); - assert_eq!( - mutations.edits, - [ - CreateElement { root: 1, tag: "div" }, - CreateTextNode { root: 2, text: "hello" }, - AppendChildren { many: 1 }, - CreatePlaceholder { root: 3 }, - AppendChildren { many: 2 }, - ] - ); -} diff --git a/packages/dioxus/tests/diffing.rs b/packages/dioxus/tests/diffing.rs deleted file mode 100644 index f01c0e6c3..000000000 --- a/packages/dioxus/tests/diffing.rs +++ /dev/null @@ -1,830 +0,0 @@ -#![allow(unused, non_upper_case_globals)] - -//! Diffing Tests -//! -//! These tests only verify that the diffing algorithm works properly for single components. -//! -//! It does not validated that component lifecycles work properly. This is done in another test file. - -use dioxus::prelude::*; - -fn new_dom() -> VirtualDom { - VirtualDom::new(|cx| render!("hi")) -} - -use dioxus_core::DomEdit::*; - -/// Should push the text node onto the stack and modify it -#[test] -fn html_and_rsx_generate_the_same_output() { - let dom = new_dom(); - let (create, change) = dom.diff_lazynodes( - rsx! ( div { "Hello world" } ), - rsx! ( div { "Goodbye world" } ), - ); - assert_eq!( - create.edits, - [ - CreateElement { root: 1, tag: "div" }, - CreateTextNode { root: 2, text: "Hello world" }, - AppendChildren { many: 1 }, - AppendChildren { many: 1 }, - ] - ); - - assert_eq!(change.edits, [SetText { text: "Goodbye world", root: 2 },]); -} - -/// Should result in 3 elements on the stack -#[test] -fn fragments_create_properly() { - let dom = new_dom(); - - let create = dom.create_vnodes(rsx! { - div { "Hello a" } - div { "Hello b" } - div { "Hello c" } - }); - - assert_eq!( - create.edits, - [ - CreateElement { root: 1, tag: "div" }, - CreateTextNode { root: 2, text: "Hello a" }, - AppendChildren { many: 1 }, - CreateElement { root: 3, tag: "div" }, - CreateTextNode { root: 4, text: "Hello b" }, - AppendChildren { many: 1 }, - CreateElement { root: 5, tag: "div" }, - CreateTextNode { root: 6, text: "Hello c" }, - AppendChildren { many: 1 }, - AppendChildren { many: 3 }, - ] - ); -} - -/// Should result in the creation of an anchor (placeholder) and then a replacewith -#[test] -fn empty_fragments_create_anchors() { - let dom = new_dom(); - - let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) }); - let right = rsx!({ (0..1).map(|_f| rsx! { div {}}) }); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }] - ); - assert_eq!( - change.edits, - [ - CreateElement { root: 2, tag: "div" }, - ReplaceWith { m: 1, root: 1 } - ] - ); -} - -/// Should result in the creation of an anchor (placeholder) and then a replacewith m=5 -#[test] -fn empty_fragments_create_many_anchors() { - let dom = new_dom(); - - let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) }); - let right = rsx!({ (0..5).map(|_f| rsx! { div {}}) }); - - let (create, change) = dom.diff_lazynodes(left, right); - assert_eq!( - create.edits, - [CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }] - ); - - assert_eq!( - change.edits, - [ - CreateElement { root: 2, tag: "div" }, - CreateElement { root: 3, tag: "div" }, - CreateElement { root: 4, tag: "div" }, - CreateElement { root: 5, tag: "div" }, - CreateElement { root: 6, tag: "div" }, - ReplaceWith { m: 5, root: 1 } - ] - ); -} - -/// Should result in the creation of an anchor (placeholder) and then a replacewith -/// Includes child nodes inside the fragment -#[test] -fn empty_fragments_create_anchors_with_many_children() { - let dom = new_dom(); - - let left = rsx!({ (0..0).map(|_| rsx! { div {} }) }); - let right = rsx!({ - (0..3).map(|f| { - rsx! { div { "hello: {f}" }} - }) - }); - - let (create, change) = dom.diff_lazynodes(left, right); - assert_eq!( - create.edits, - [CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }] - ); - - assert_eq!( - change.edits, - [ - CreateElement { tag: "div", root: 2 }, - CreateTextNode { text: "hello: 0", root: 3 }, - AppendChildren { many: 1 }, - CreateElement { tag: "div", root: 4 }, - CreateTextNode { text: "hello: 1", root: 5 }, - AppendChildren { many: 1 }, - CreateElement { tag: "div", root: 6 }, - CreateTextNode { text: "hello: 2", root: 7 }, - AppendChildren { many: 1 }, - ReplaceWith { root: 1, m: 3 } - ] - ); -} - -/// Should result in every node being pushed and then replaced with an anchor -#[test] -fn many_items_become_fragment() { - let dom = new_dom(); - - let left = rsx!({ - (0..2).map(|_| { - rsx! { div { "hello" }} - }) - }); - let right = rsx!({ (0..0).map(|_| rsx! { div {} }) }); - - let (create, change) = dom.diff_lazynodes(left, right); - assert_eq!( - create.edits, - [ - CreateElement { root: 1, tag: "div" }, - CreateTextNode { text: "hello", root: 2 }, - AppendChildren { many: 1 }, - CreateElement { root: 3, tag: "div" }, - CreateTextNode { text: "hello", root: 4 }, - AppendChildren { many: 1 }, - AppendChildren { many: 2 }, - ] - ); - - assert_eq!( - change.edits, - [ - CreatePlaceholder { root: 5 }, - ReplaceWith { root: 1, m: 1 }, - Remove { root: 3 }, - ] - ); -} - -/// Should result in no edits -#[test] -fn two_equal_fragments_are_equal() { - let dom = new_dom(); - - let left = rsx!({ - (0..2).map(|_| { - rsx! { div { "hello" }} - }) - }); - let right = rsx!({ - (0..2).map(|_| { - rsx! { div { "hello" }} - }) - }); - - let (_create, change) = dom.diff_lazynodes(left, right); - assert!(change.edits.is_empty()); -} - -/// Should result the creation of more nodes appended after the old last node -#[test] -fn two_fragments_with_differrent_elements_are_differet() { - let dom = new_dom(); - - let left = rsx!( - { (0..2).map(|_| rsx! { div { }} ) } - p {} - ); - let right = rsx!( - { (0..5).map(|_| rsx! (h1 { }) ) } - p {} - ); - - let (_create, changes) = dom.diff_lazynodes(left, right); - println!("{:#?}", &changes); - assert_eq!( - changes.edits, - [ - // create the new h1s - CreateElement { tag: "h1", root: 4 }, - CreateElement { tag: "h1", root: 5 }, - CreateElement { tag: "h1", root: 6 }, - InsertAfter { root: 2, n: 3 }, - // replace the divs with new h1s - CreateElement { tag: "h1", root: 7 }, - ReplaceWith { root: 1, m: 1 }, - CreateElement { tag: "h1", root: 1 }, // notice how 1 gets re-used - ReplaceWith { root: 2, m: 1 }, - ] - ); -} - -/// Should result in multiple nodes destroyed - with changes to the first nodes -#[test] -fn two_fragments_with_differrent_elements_are_differet_shorter() { - let dom = new_dom(); - - let left = rsx!( - {(0..5).map(|f| {rsx! { div { }}})} - p {} - ); - let right = rsx!( - {(0..2).map(|f| {rsx! { h1 { }}})} - p {} - ); - - let (create, change) = dom.diff_lazynodes(left, right); - assert_eq!( - create.edits, - [ - CreateElement { root: 1, tag: "div" }, - CreateElement { root: 2, tag: "div" }, - CreateElement { root: 3, tag: "div" }, - CreateElement { root: 4, tag: "div" }, - CreateElement { root: 5, tag: "div" }, - CreateElement { root: 6, tag: "p" }, - AppendChildren { many: 6 }, - ] - ); - - // note: key reuse is always the last node that got used - // slab maintains a linked list, essentially - assert_eq!( - change.edits, - [ - Remove { root: 3 }, - Remove { root: 4 }, - Remove { root: 5 }, - CreateElement { root: 5, tag: "h1" }, // 3 gets reused - ReplaceWith { root: 1, m: 1 }, // 1 gets deleted - CreateElement { root: 1, tag: "h1" }, // 1 gets reused - ReplaceWith { root: 2, m: 1 }, - ] - ); -} - -/// Should result in multiple nodes destroyed - with no changes -#[test] -fn two_fragments_with_same_elements_are_differet() { - let dom = new_dom(); - - let left = rsx!( - {(0..2).map(|f| {rsx! { div { }}})} - p {} - ); - let right = rsx!( - {(0..5).map(|f| {rsx! { div { }}})} - p {} - ); - - let (create, change) = dom.diff_lazynodes(left, right); - assert_eq!( - create.edits, - [ - CreateElement { root: 1, tag: "div" }, - CreateElement { root: 2, tag: "div" }, - CreateElement { root: 3, tag: "p" }, - AppendChildren { many: 3 }, - ] - ); - assert_eq!( - change.edits, - [ - CreateElement { root: 4, tag: "div" }, - CreateElement { root: 5, tag: "div" }, - CreateElement { root: 6, tag: "div" }, - InsertAfter { root: 2, n: 3 }, - ] - ); -} - -/// should result in the removal of elements -#[test] -fn keyed_diffing_order() { - let dom = new_dom(); - - let left = rsx!( - {(0..5).map(|f| {rsx! { div { key: "{f}" }}})} - p {"e"} - ); - let right = rsx!( - {(0..2).map(|f| rsx! { div { key: "{f}" }})} - p {"e"} - ); - - let (create, change) = dom.diff_lazynodes(left, right); - assert_eq!( - change.edits, - [Remove { root: 3 }, Remove { root: 4 }, Remove { root: 5 },] - ); -} - -/// Should result in moves, but not removals or additions -#[test] -fn keyed_diffing_out_of_order() { - let dom = new_dom(); - - let left = rsx!({ - [0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let (_, changes) = dom.diff_lazynodes(left, right); - println!("{:?}", &changes); - assert_eq!( - changes.edits, - [PushRoot { root: 7 }, InsertBefore { root: 5, n: 1 }] - ); -} - -/// Should result in moves only -#[test] -fn keyed_diffing_out_of_order_adds() { - let dom = new_dom(); - - let left = rsx!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [/**/ 8, 7, 4, 5, 6 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - assert_eq!( - change.edits, - [ - PushRoot { root: 5 }, - PushRoot { root: 4 }, - InsertBefore { n: 2, root: 1 } - ] - ); -} -/// Should result in moves onl -#[test] -fn keyed_diffing_out_of_order_adds_2() { - let dom = new_dom(); - - let left = rsx!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [/**/ 7, 8, 4, 5, 6 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - assert_eq!( - change.edits, - [ - PushRoot { root: 4 }, - PushRoot { root: 5 }, - InsertBefore { n: 2, root: 1 } - ] - ); -} - -/// Should result in moves onl -#[test] -fn keyed_diffing_out_of_order_adds_3() { - let dom = new_dom(); - - let left = rsx!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [/**/ 4, 8, 7, 5, 6 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - assert_eq!( - change.edits, - [ - PushRoot { root: 5 }, - PushRoot { root: 4 }, - InsertBefore { n: 2, root: 2 } - ] - ); -} - -/// Should result in moves onl -#[test] -fn keyed_diffing_out_of_order_adds_4() { - let dom = new_dom(); - - let left = rsx!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [/**/ 4, 5, 8, 7, 6 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - assert_eq!( - change.edits, - [ - PushRoot { root: 5 }, - PushRoot { root: 4 }, - InsertBefore { n: 2, root: 3 } - ] - ); -} - -/// Should result in moves onl -#[test] -fn keyed_diffing_out_of_order_adds_5() { - let dom = new_dom(); - - let left = rsx!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [/**/ 4, 5, 6, 8, 7 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - assert_eq!( - change.edits, - [PushRoot { root: 5 }, InsertBefore { n: 1, root: 4 }] - ); -} - -#[test] -fn keyed_diffing_additions() { - let dom = new_dom(); - - let left = rsx!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [/**/ 4, 5, 6, 7, 8, 9, 10 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - assert_eq!( - change.edits, - [ - CreateElement { root: 6, tag: "div" }, - CreateElement { root: 7, tag: "div" }, - InsertAfter { n: 2, root: 5 } - ] - ); -} - -#[test] -fn keyed_diffing_additions_and_moves_on_ends() { - let dom = new_dom(); - - let left = rsx!({ - [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - println!("{:?}", change); - assert_eq!( - change.edits, - [ - // create 11, 12 - CreateElement { tag: "div", root: 5 }, - CreateElement { tag: "div", root: 6 }, - InsertAfter { root: 3, n: 2 }, - // move 7 to the front - PushRoot { root: 4 }, - InsertBefore { root: 1, n: 1 } - ] - ); -} - -#[test] -fn keyed_diffing_additions_and_moves_in_middle() { - let dom = new_dom(); - - let left = rsx!({ - [/**/ 1, 2, 3, 4 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - // LIS: 4, 5, 6 - let (_, change) = dom.diff_lazynodes(left, right); - println!("{:#?}", change); - assert_eq!( - change.edits, - [ - // create 5, 6 - CreateElement { tag: "div", root: 5 }, - CreateElement { tag: "div", root: 6 }, - InsertBefore { root: 3, n: 2 }, - // create 7, 8 - CreateElement { tag: "div", root: 7 }, - CreateElement { tag: "div", root: 8 }, - InsertBefore { root: 2, n: 2 }, - // move 7 - PushRoot { root: 4 }, - InsertBefore { root: 1, n: 1 } - ] - ); -} - -#[test] -fn controlled_keyed_diffing_out_of_order() { - let dom = new_dom(); - - let left = rsx!({ - [4, 5, 6, 7].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [0, 5, 9, 6, 4].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - // LIS: 5, 6 - let (_, changes) = dom.diff_lazynodes(left, right); - println!("{:#?}", &changes); - assert_eq!( - changes.edits, - [ - Remove { root: 4 }, - // move 4 to after 6 - PushRoot { root: 1 }, - InsertAfter { n: 1, root: 3 }, - // remove 7 - - // create 9 and insert before 6 - CreateElement { root: 4, tag: "div" }, - InsertBefore { n: 1, root: 3 }, - // create 0 and insert before 5 - CreateElement { root: 5, tag: "div" }, - InsertBefore { n: 1, root: 2 }, - ] - ); -} - -#[test] -fn controlled_keyed_diffing_out_of_order_max_test() { - let dom = new_dom(); - - let left = rsx!({ - [0, 1, 2, 3, 4].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let right = rsx!({ - [3, 0, 1, 10, 2].iter().map(|f| { - rsx! { div { key: "{f}" }} - }) - }); - - let (_, changes) = dom.diff_lazynodes(left, right); - println!("{:#?}", &changes); - assert_eq!( - changes.edits, - [ - Remove { root: 5 }, - CreateElement { root: 5, tag: "div" }, - InsertBefore { n: 1, root: 3 }, - PushRoot { root: 4 }, - InsertBefore { n: 1, root: 1 }, - ] - ); -} - -// noticed some weird behavior in the desktop interpreter -// just making sure it doesnt happen in the core implementation -#[test] -fn remove_list() { - let dom = new_dom(); - - let left = rsx!({ - (0..10).rev().take(5).map(|i| { - rsx! { Fragment { key: "{i}", "{i}" }} - }) - }); - - let right = rsx!({ - (0..10).rev().take(2).map(|i| { - rsx! { Fragment { key: "{i}", "{i}" }} - }) - }); - - let (create, changes) = dom.diff_lazynodes(left, right); - - // dbg!(create); - // dbg!(changes); - - assert_eq!( - changes.edits, - [ - // remove 5, 4, 3 - Remove { root: 3 }, - Remove { root: 4 }, - Remove { root: 5 }, - ] - ); -} - -// noticed some weird behavior in the desktop interpreter -// just making sure it doesnt happen in the core implementation -#[test] -fn remove_list_nokeyed() { - let dom = new_dom(); - - let left = rsx!({ - (0..10).rev().take(5).map(|i| { - rsx! { Fragment { "{i}" }} - }) - }); - - let right = rsx!({ - (0..10).rev().take(2).map(|i| { - rsx! { Fragment { "{i}" }} - }) - }); - - let (create, changes) = dom.diff_lazynodes(left, right); - - assert_eq!( - changes.edits, - [ - // remove 5, 4, 3 - Remove { root: 3 }, - Remove { root: 4 }, - Remove { root: 5 }, - ] - ); -} - -#[test] -fn add_nested_elements() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx! { - div{} - }, - rsx! { - div{ - div{} - } - }, - ); - - assert_eq!( - change.edits, - [ - PushRoot { root: 1 }, - CreateElement { root: 2, tag: "div" }, - AppendChildren { many: 1 }, - PopRoot {}, - ] - ); -} - -#[test] -fn add_listeners() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx! { - div{} - }, - rsx! { - div{ - onkeyup: |_| {}, - onkeydown: |_| {}, - } - }, - ); - - assert_eq!( - change.edits, - [ - NewEventListener { event_name: "keyup", scope: ScopeId(0), root: 1 }, - NewEventListener { event_name: "keydown", scope: ScopeId(0), root: 1 }, - ] - ); -} - -#[test] -fn remove_listeners() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx! { - div{ - onkeyup: |_| {}, - onkeydown: |_| {}, - } - }, - rsx! { - div{} - }, - ); - - assert_eq!( - change.edits, - [ - RemoveEventListener { event: "keyup", root: 1 }, - RemoveEventListener { event: "keydown", root: 1 }, - ] - ); -} - -#[test] -fn diff_listeners() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx! { - div{ - onkeydown: |_| {}, - } - }, - rsx! { - div{ - onkeyup: |_| {}, - } - }, - ); - - assert_eq!( - change.edits, - [ - RemoveEventListener { root: 1, event: "keydown" }, - NewEventListener { event_name: "keyup", scope: ScopeId(0), root: 1 } - ] - ); -} diff --git a/packages/dioxus/tests/earlyabort.rs b/packages/dioxus/tests/earlyabort.rs deleted file mode 100644 index 037240cb6..000000000 --- a/packages/dioxus/tests/earlyabort.rs +++ /dev/null @@ -1,63 +0,0 @@ -#![allow(unused, non_upper_case_globals, non_snake_case)] - -//! Prove that the dom works normally through virtualdom methods. -//! -//! This methods all use "rebuild" which completely bypasses the scheduler. -//! Hard rebuilds don't consume any events from the event queue. - -use dioxus::prelude::*; - -use dioxus_core::{DomEdit::*, ScopeId}; - -const IS_LOGGING_ENABLED: bool = false; - -fn new_dom(app: Component

, props: P) -> VirtualDom { - VirtualDom::new_with_props(app, props) -} - -/// This test ensures that if a component aborts early, it is replaced with a placeholder. -/// In debug, this should also toss a warning. -#[test] -fn test_early_abort() { - const app: Component = |cx| { - let val = cx.use_hook(|| 0); - - *val += 1; - - if *val == 2 { - return None; - } - - render!(div { "Hello, world!" }) - }; - - let mut dom = new_dom(app, ()); - - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - CreateElement { tag: "div", root: 1 }, - CreateTextNode { text: "Hello, world!", root: 2 }, - AppendChildren { many: 1 }, - AppendChildren { many: 1 }, - ] - ); - - let edits = dom.hard_diff(ScopeId(0)); - assert_eq!( - edits.edits, - [CreatePlaceholder { root: 3 }, ReplaceWith { root: 1, m: 1 },], - ); - - let edits = dom.hard_diff(ScopeId(0)); - assert_eq!( - edits.edits, - [ - CreateElement { tag: "div", root: 1 }, // keys get reused - CreateTextNode { text: "Hello, world!", root: 2 }, // keys get reused - AppendChildren { many: 1 }, - ReplaceWith { root: 3, m: 1 }, - ] - ); -} diff --git a/packages/dioxus/tests/lifecycle.rs b/packages/dioxus/tests/lifecycle.rs deleted file mode 100644 index 237938d36..000000000 --- a/packages/dioxus/tests/lifecycle.rs +++ /dev/null @@ -1,261 +0,0 @@ -#![allow(unused, non_upper_case_globals)] -#![allow(non_snake_case)] - -//! Tests for the lifecycle of components. -use dioxus::prelude::*; -use dioxus_core::DomEdit::*; -use std::sync::{Arc, Mutex}; - -type Shared = Arc>; - -#[test] -fn manual_diffing() { - struct AppProps { - value: Shared<&'static str>, - } - - static App: Component = |cx| { - let val = cx.props.value.lock().unwrap(); - cx.render(rsx! { div { "{val}" } }) - }; - - let value = Arc::new(Mutex::new("Hello")); - let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone() }); - - let _ = dom.rebuild(); - - *value.lock().unwrap() = "goodbye"; - - let edits = dom.rebuild(); - - println!("edits: {:?}", edits); -} - -#[test] -fn events_generate() { - fn app(cx: Scope) -> Element { - let count = cx.use_hook(|| 0); - - let inner = match *count { - 0 => { - rsx! { - div { - onclick: move |_| *count += 1, - div { - "nested" - } - "Click me!" - } - } - } - _ => todo!(), - }; - - cx.render(inner) - }; - - let mut dom = VirtualDom::new(app); - let mut channel = dom.get_scheduler_channel(); - assert!(dom.has_work()); - - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - CreateElement { tag: "div", root: 1 }, - NewEventListener { event_name: "click", scope: ScopeId(0), root: 1 }, - CreateElement { tag: "div", root: 2 }, - CreateTextNode { text: "nested", root: 3 }, - AppendChildren { many: 1 }, - CreateTextNode { text: "Click me!", root: 4 }, - AppendChildren { many: 2 }, - AppendChildren { many: 1 }, - ] - ) -} - -#[test] -fn components_generate() { - fn app(cx: Scope) -> Element { - let render_phase = cx.use_hook(|| 0); - *render_phase += 1; - - cx.render(match *render_phase { - 1 => rsx!("Text0"), - 2 => rsx!(div {}), - 3 => rsx!("Text2"), - 4 => rsx!(Child {}), - 5 => rsx!({ None as Option<()> }), - 6 => rsx!("text 3"), - 7 => rsx!({ (0..2).map(|f| rsx!("text {f}")) }), - 8 => rsx!(Child {}), - _ => todo!(), - }) - }; - - fn Child(cx: Scope) -> Element { - println!("Running child"); - cx.render(rsx! { - h1 {} - }) - } - - let mut dom = VirtualDom::new(app); - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - CreateTextNode { text: "Text0", root: 1 }, - AppendChildren { many: 1 }, - ] - ); - - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateElement { tag: "div", root: 2 }, - ReplaceWith { root: 1, m: 1 }, - ] - ); - - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateTextNode { text: "Text2", root: 1 }, - ReplaceWith { root: 2, m: 1 }, - ] - ); - - // child {} - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateElement { tag: "h1", root: 2 }, - ReplaceWith { root: 1, m: 1 }, - ] - ); - - // placeholder - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [CreatePlaceholder { root: 1 }, ReplaceWith { root: 2, m: 1 },] - ); - - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateTextNode { text: "text 3", root: 2 }, - ReplaceWith { root: 1, m: 1 }, - ] - ); - - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateTextNode { text: "text 0", root: 1 }, - CreateTextNode { text: "text 1", root: 3 }, - ReplaceWith { root: 2, m: 2 }, - ] - ); - - assert_eq!( - dom.hard_diff(ScopeId(0)).edits, - [ - CreateElement { tag: "h1", root: 2 }, - ReplaceWith { root: 1, m: 1 }, - Remove { root: 3 }, - ] - ); -} - -#[test] -fn component_swap() { - fn app(cx: Scope) -> Element { - let render_phase = cx.use_hook(|| 0); - *render_phase += 1; - - cx.render(match *render_phase { - 0 => rsx!( - div { - NavBar {} - Dashboard {} - } - ), - 1 => rsx!( - div { - NavBar {} - Results {} - } - ), - 2 => rsx!( - div { - NavBar {} - Dashboard {} - } - ), - 3 => rsx!( - div { - NavBar {} - Results {} - } - ), - 4 => rsx!( - div { - NavBar {} - Dashboard {} - } - ), - _ => rsx!("blah"), - }) - }; - - static NavBar: Component = |cx| { - println!("running navbar"); - cx.render(rsx! { - h1 { - "NavBar" - {(0..3).map(|f| rsx!(NavLink {}))} - } - }) - }; - - static NavLink: Component = |cx| { - println!("running navlink"); - cx.render(rsx! { - h1 { - "NavLink" - } - }) - }; - - static Dashboard: Component = |cx| { - println!("running dashboard"); - cx.render(rsx! { - div { - "dashboard" - } - }) - }; - - static Results: Component = |cx| { - println!("running results"); - cx.render(rsx! { - div { - "results" - } - }) - }; - - let mut dom = VirtualDom::new(app); - let edits = dom.rebuild(); - dbg!(&edits); - - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); -} diff --git a/packages/dioxus/tests/miri_stress.rs b/packages/dioxus/tests/miri_stress.rs deleted file mode 100644 index a5f90907c..000000000 --- a/packages/dioxus/tests/miri_stress.rs +++ /dev/null @@ -1,412 +0,0 @@ -#![allow(non_snake_case)] -/* -Stress Miri as much as possible. - -Prove that we don't leak memory and that our methods are safe. - -Specifically: -- [ ] VirtualDom drops memory safely -- [ ] Borrowed components don't expose invalid pointers -- [ ] Async isn't busted -*/ - -use dioxus::prelude::*; -use dioxus_core::SchedulerMsg; - -fn new_dom(app: Component

, props: P) -> VirtualDom { - VirtualDom::new_with_props(app, props) -} - -/// This test ensures that if a component aborts early, it is replaced with a placeholder. -/// In debug, this should also toss a warning. -#[test] -fn test_memory_leak() { - fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); - - *val += 1; - - if *val == 2 || *val == 4 { - return None; - } - - let name = cx.use_hook(|| String::from("asd")); - - cx.render(rsx!( - div { "Hello, world!" } - Child {} - Child {} - Child {} - Child {} - Child {} - Child {} - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } - )) - } - - #[derive(Props)] - struct BorrowedProps<'a> { - na: &'a str, - } - - fn BorrowedChild<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element { - render!(div { - "goodbye {cx.props.na}" - Child {} - Child {} - }) - } - - fn Child(cx: Scope) -> Element { - render!(div { "goodbye world" }) - } - - let mut dom = new_dom(app, ()); - - dom.rebuild(); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); -} - -#[test] -fn memo_works_properly() { - fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); - - *val += 1; - - if *val == 2 || *val == 4 { - return None; - } - - let name = cx.use_hook(|| String::from("asd")); - - cx.render(rsx!( - div { "Hello, world! {name}" } - Child { na: "asdfg".to_string() } - )) - } - - #[derive(PartialEq, Props)] - struct ChildProps { - na: String, - } - - fn Child(cx: Scope) -> Element { - render!(div { "goodbye world" }) - } - - let mut dom = new_dom(app, ()); - - dom.rebuild(); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); -} - -#[test] -fn free_works_on_root_props() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { a: "alpha"} - Child { a: "beta"} - Child { a: "gamma"} - Child { a: "delta"} - }) - } - - #[derive(Props, PartialEq)] - struct ChildProps { - a: &'static str, - } - - fn Child(cx: Scope) -> Element { - render!("child {cx.props.a}") - } - - struct Custom { - val: String, - } - - impl Drop for Custom { - fn drop(&mut self) { - dbg!("dropped! {}", &self.val); - } - } - - let mut dom = new_dom(app, Custom { val: String::from("asd") }); - dom.rebuild(); -} - -#[test] -fn free_works_on_borrowed() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { a: "alpha", b: "asd".to_string() } - }) - } - #[derive(Props)] - struct ChildProps<'a> { - a: &'a str, - b: String, - } - - fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - dbg!("rendering child"); - render!("child {cx.props.a}, {cx.props.b}") - } - - impl Drop for ChildProps<'_> { - fn drop(&mut self) { - dbg!("dropped child!"); - } - } - - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); -} - -#[test] -fn free_works_on_root_hooks() { - /* - On Drop, scopearena drops all the hook contents. - */ - - struct Droppable(T); - impl Drop for Droppable { - fn drop(&mut self) { - dbg!("dropping droppable"); - } - } - - fn app(cx: Scope) -> Element { - let name = cx.use_hook(|| Droppable(String::from("asd"))); - render!(div { "{name.0}" }) - } - - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); -} - -#[test] -fn old_props_arent_stale() { - fn app(cx: Scope) -> Element { - dbg!("rendering parent"); - let cnt = cx.use_hook(|| 0); - *cnt += 1; - - if *cnt == 1 { - render!(div { Child { a: "abcdef".to_string() } }) - } else { - 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); - - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - - dbg!("forcing update to child"); - - 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); - } -} diff --git a/packages/dioxus/tests/passthru.rs b/packages/dioxus/tests/passthru.rs deleted file mode 100644 index 87d9f8545..000000000 --- a/packages/dioxus/tests/passthru.rs +++ /dev/null @@ -1,100 +0,0 @@ -#![allow(unused, non_upper_case_globals)] -#![allow(non_snake_case)] - -//! Diffing Tests -//! -//! These tests only verify that the diffing algorithm works properly for single components. -//! -//! It does not validated that component lifecycles work properly. This is done in another test file. - -use dioxus::prelude::*; - -fn new_dom() -> VirtualDom { - VirtualDom::new(|cx| render!("hi")) -} - -use dioxus_core::DomEdit::*; - -/// Should push the text node onto the stack and modify it -#[test] -fn nested_passthru_creates() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { - Child { - Child { - div { - "hi" - } - } - } - } - }) - }; - - #[inline_props] - fn Child<'a>(cx: Scope, children: Element<'a>) -> Element { - cx.render(rsx! { children }) - }; - - let mut dom = VirtualDom::new(app); - let mut channel = dom.get_scheduler_channel(); - assert!(dom.has_work()); - - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - CreateElement { tag: "div", root: 1 }, - CreateTextNode { text: "hi", root: 2 }, - AppendChildren { many: 1 }, - AppendChildren { many: 1 }, - ] - ) -} - -/// Should push the text node onto the stack and modify it -#[test] -fn nested_passthru_creates_add() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { - "1" - Child { - "2" - Child { - "3" - div { - "hi" - } - } - } - } - }) - }; - - #[inline_props] - fn Child<'a>(cx: Scope, children: Element<'a>) -> Element { - cx.render(rsx! { - children - }) - }; - - let mut dom = VirtualDom::new(app); - let mut channel = dom.get_scheduler_channel(); - assert!(dom.has_work()); - - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - CreateTextNode { text: "1", root: 1 }, - CreateTextNode { text: "2", root: 2 }, - CreateTextNode { text: "3", root: 3 }, - CreateElement { tag: "div", root: 4 }, - CreateTextNode { text: "hi", root: 5 }, - AppendChildren { many: 1 }, - AppendChildren { many: 4 }, - ] - ) -} diff --git a/packages/dioxus/tests/scheduler.rs b/packages/dioxus/tests/scheduler.rs deleted file mode 100644 index f058dfe4c..000000000 --- a/packages/dioxus/tests/scheduler.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Tests for the scheduler. -//! -//! TODO -//! - priority lanes -//! - periodic checking -//! diff --git a/packages/dioxus/tests/sharedstate.rs b/packages/dioxus/tests/sharedstate.rs deleted file mode 100644 index 85432ad4c..000000000 --- a/packages/dioxus/tests/sharedstate.rs +++ /dev/null @@ -1,126 +0,0 @@ -#![allow(unused, non_upper_case_globals, non_snake_case)] - -use dioxus::prelude::*; -use dioxus_core::{DomEdit, Mutations, SchedulerMsg, ScopeId}; -use std::rc::Rc; -use DomEdit::*; - -#[test] -fn shared_state_test() { - struct MySharedState(&'static str); - - static App: Component = |cx| { - cx.provide_context(Rc::new(MySharedState("world!"))); - cx.render(rsx!(Child {})) - }; - - static Child: Component = |cx| { - let shared = cx.consume_context::>()?; - cx.render(rsx!("Hello, {shared.0}")) - }; - - let mut dom = VirtualDom::new(App); - let Mutations { edits, .. } = dom.rebuild(); - - assert_eq!( - edits, - [ - CreateTextNode { root: 1, text: "Hello, world!" }, - AppendChildren { many: 1 }, - ] - ); -} - -#[test] -fn swap_test() { - struct MySharedState(&'static str); - - fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); - *val += 1; - - cx.provide_context(Rc::new(MySharedState("world!"))); - - let child = match *val % 2 { - 0 => rsx!( - Child1 { - Child1 { } - Child2 { } - } - ), - _ => rsx!( - Child2 { - Child2 { } - Child2 { } - } - ), - }; - - cx.render(rsx!( - Router { - div { child } - } - )) - } - - #[inline_props] - fn Router<'a>(cx: Scope, children: Element<'a>) -> Element<'a> { - cx.render(rsx!(div { children })) - } - - #[inline_props] - fn Child1<'a>(cx: Scope, children: Element<'a>) -> Element { - let shared = cx.consume_context::>().unwrap(); - println!("Child1: {}", shared.0); - cx.render(rsx! { - div { - "{shared.0}", - children - } - }) - } - - #[inline_props] - fn Child2<'a>(cx: Scope, children: Element<'a>) -> Element { - let shared = cx.consume_context::>().unwrap(); - println!("Child2: {}", shared.0); - cx.render(rsx! { - h1 { - "{shared.0}", - children - } - }) - } - - let mut dom = VirtualDom::new(app); - let Mutations { edits, .. } = 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); - 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(0))); - 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(3))); - // 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(0))); - // dom.work_with_deadline(|| false); -} diff --git a/packages/dioxus/tests/task.rs b/packages/dioxus/tests/task.rs deleted file mode 100644 index f328e4d9d..000000000 --- a/packages/dioxus/tests/task.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/packages/dioxus/tests/vdom_rebuild.rs b/packages/dioxus/tests/vdom_rebuild.rs deleted file mode 100644 index 9ddf03d18..000000000 --- a/packages/dioxus/tests/vdom_rebuild.rs +++ /dev/null @@ -1,95 +0,0 @@ -#![allow(unused, non_upper_case_globals)] - -//! Rebuilding tests -//! ---------------- -//! -//! This tests module ensures that the initial build of the virtualdom is correct. -//! This does not include dynamic tests or the diffing algorithm itself. -//! -//! It does prove that mounting works properly and the correct edit streams are generated. -//! -//! Don't have a good way to validate, everything is done manually ATM - -use dioxus::prelude::*; -use dioxus_core::DomEdit::*; - -#[test] -fn app_runs() { - static App: Component = |cx| render!(div{"hello"} ); - - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); -} - -#[test] -fn fragments_work() { - static App: Component = |cx| { - cx.render(rsx!( - div{"hello"} - div{"goodbye"} - )) - }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - // should result in a final "appendchildren n=2" - dbg!(edits); -} - -#[test] -fn lists_work() { - static App: Component = |cx| { - cx.render(rsx!( - h1 {"hello"} - (0..6).map(|f| rsx!(span{ "{f}" })) - )) - }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - dbg!(edits); -} - -#[test] -fn conditional_rendering() { - static App: Component = |cx| { - cx.render(rsx!( - h1 {"hello"} - {true.then(|| rsx!(span{ "a" }))} - {false.then(|| rsx!(span{ "b" }))} - )) - }; - let mut vdom = VirtualDom::new(App); - - let mutations = vdom.rebuild(); - assert_eq!( - mutations.edits, - [ - CreateElement { root: 1, tag: "h1" }, - CreateTextNode { root: 2, text: "hello" }, - AppendChildren { many: 1 }, - CreateElement { root: 3, tag: "span" }, - CreateTextNode { root: 4, text: "a" }, - AppendChildren { many: 1 }, - CreatePlaceholder { root: 5 }, - AppendChildren { many: 3 }, - ] - ) -} - -#[test] -fn child_components() { - static App: Component = |cx| { - cx.render(rsx!( - {true.then(|| rsx!(Child { }))} - {false.then(|| rsx!(Child { }))} - )) - }; - static Child: Component = |cx| { - cx.render(rsx!( - h1 {"hello"} - h1 {"goodbye"} - )) - }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - dbg!(edits); -} diff --git a/packages/fermi/README.md b/packages/fermi/README.md index a9e2e1772..711b1a43a 100644 --- a/packages/fermi/README.md +++ b/packages/fermi/README.md @@ -44,7 +44,7 @@ From anywhere in our app, we can read our the value of our atom: ```rust, ignore fn NameCard(cx: Scope) -> Element { - let name = use_read(&cx, NAME); + let name = use_read(cx, NAME); cx.render(rsx!{ h1 { "Hello, {name}"} }) } ``` @@ -53,7 +53,7 @@ We can also set the value of our atom, also from anywhere in our app: ```rust, ignore fn NameCard(cx: Scope) -> Element { - let set_name = use_set(&cx, NAME); + let set_name = use_set(cx, NAME); cx.render(rsx!{ button { onclick: move |_| set_name("Fermi"), @@ -77,7 +77,7 @@ fermi = { git = "https://github.com/dioxuslabs/dioxus" } ## Running examples The examples here use Dioxus Desktop to showcase their functionality. To run an example, use -``` +```sh $ cargo run --example fermi ``` diff --git a/packages/fermi/src/hooks/atom_root.rs b/packages/fermi/src/hooks/atom_root.rs index f9eb07bc9..24fd70093 100644 --- a/packages/fermi/src/hooks/atom_root.rs +++ b/packages/fermi/src/hooks/atom_root.rs @@ -1,11 +1,12 @@ +use std::rc::Rc; + use crate::AtomRoot; use dioxus_core::ScopeState; -use std::rc::Rc; // Returns the atom root, initiaizing it at the root of the app if it does not exist. pub fn use_atom_root(cx: &ScopeState) -> &Rc { cx.use_hook(|| match cx.consume_context::>() { Some(root) => root, - None => cx.provide_root_context(Rc::new(AtomRoot::new(cx.schedule_update_any()))), + None => panic!("No atom root found in context. Did you forget place an AtomRoot component at the top of your app?"), }) } diff --git a/packages/fermi/src/hooks/state.rs b/packages/fermi/src/hooks/state.rs index 96315044c..15310cff2 100644 --- a/packages/fermi/src/hooks/state.rs +++ b/packages/fermi/src/hooks/state.rs @@ -19,7 +19,7 @@ use std::{ /// static COUNT: Atom = |_| 0; /// /// fn Example(cx: Scope) -> Element { -/// let mut count = use_atom_state(&cx, COUNT); +/// let mut count = use_atom_state(cx, COUNT); /// /// cx.render(rsx! { /// div { @@ -74,7 +74,7 @@ impl AtomState { /// /// ```rust, ignore /// fn component(cx: Scope) -> Element { - /// let count = use_state(&cx, || 0); + /// let count = use_state(cx, || 0); /// cx.spawn({ /// let set_count = count.to_owned(); /// async move { @@ -101,7 +101,7 @@ impl AtomState { /// /// ```rust, ignore /// fn component(cx: Scope) -> Element { - /// let value = use_state(&cx, || 0); + /// let value = use_state(cx, || 0); /// /// rsx!{ /// Component { @@ -128,7 +128,7 @@ impl AtomState { /// # use dioxus_core::prelude::*; /// # use dioxus_hooks::*; /// fn component(cx: Scope) -> Element { - /// let value = use_state(&cx, || 0); + /// let value = use_state(cx, || 0); /// /// // to increment the value /// value.modify(|v| v + 1); @@ -168,7 +168,7 @@ impl AtomState { /// # use dioxus_core::prelude::*; /// # use dioxus_hooks::*; /// fn component(cx: Scope) -> Element { - /// let value = use_state(&cx, || 0); + /// let value = use_state(cx, || 0); /// /// let as_rc = value.get(); /// assert_eq!(as_rc.as_ref(), &0); @@ -190,7 +190,7 @@ impl AtomState { /// /// ```rust, ignore /// fn component(cx: Scope) -> Element { - /// let count = use_state(&cx, || 0); + /// let count = use_state(cx, || 0); /// cx.spawn({ /// let count = count.to_owned(); /// async move { @@ -220,7 +220,7 @@ impl AtomState { /// # Examples /// /// ```ignore - /// let val = use_state(&cx, || 0); + /// let val = use_state(cx, || 0); /// /// val.with_mut(|v| *v = 1); /// ``` @@ -243,7 +243,7 @@ impl AtomState { /// # Examples /// /// ```ignore - /// let val = use_state(&cx, || 0); + /// let val = use_state(cx, || 0); /// /// *val.make_mut() += 1; /// ``` diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 41c4d2854..a9cfa340d 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../../packages/core", version = "^0.2.1" } futures-channel = "0.3.21" -log = { version = "0.4" } +log = "0.4" [dev-dependencies] diff --git a/packages/hooks/README.md b/packages/hooks/README.md index 18133e40b..14c0e4209 100644 --- a/packages/hooks/README.md +++ b/packages/hooks/README.md @@ -15,8 +15,8 @@ You can always use it "normally" with the `split` method: ```rust // Rusty-smart-pointer usage: -let value = use_state(&cx, || 10); +let value = use_state(cx, || 10); // "Classic" usage: -let (value, set_value) = use_state(&cx, || 0).split(); +let (value, set_value) = use_state(cx, || 0).split(); ``` diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 1790eff1d..583eccb37 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -1,5 +1,28 @@ -// #![deny(missing_docs)] -//! Useful foundational hooks for Dioxus +#[macro_export] +/// A helper macro for using hooks in async environements. +/// +/// # Usage +/// +/// +/// ```ignore +/// let (data) = use_ref(cx, || {}); +/// +/// let handle_thing = move |_| { +/// to_owned![data] +/// cx.spawn(async move { +/// // do stuff +/// }); +/// } +/// ``` +macro_rules! to_owned { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.to_owned(); + )*} +} + +mod usecontext; +pub use usecontext::*; mod usestate; pub use usestate::{use_state, UseState}; @@ -19,5 +42,5 @@ pub use usefuture::*; mod useeffect; pub use useeffect::*; -// mod usesuspense; -// pub use usesuspense::*; +mod usecallback; +pub use usecallback::*; diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 9c4048b0e..a355ae42a 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -60,7 +60,7 @@ impl ProvidedStateInner { /// /// /// -pub fn use_context(cx: &ScopeState) -> Option> { +pub fn use_shared_state(cx: &ScopeState) -> Option> { let state = cx.use_hook(|| { let scope_id = cx.scope_id(); let root = cx.consume_context::>(); @@ -165,20 +165,14 @@ where } /// Provide some state for components down the hierarchy to consume without having to drill props. -/// -/// -/// -/// -/// -/// -/// -pub fn use_context_provider(cx: &ScopeState, f: impl FnOnce() -> T) { +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())), notify_any: cx.schedule_update_any(), consumers: HashSet::new(), })); - cx.provide_context(state) + + cx.provide_context(state); }); } diff --git a/packages/hooks/src/usecallback.rs b/packages/hooks/src/usecallback.rs new file mode 100644 index 000000000..3f223b581 --- /dev/null +++ b/packages/hooks/src/usecallback.rs @@ -0,0 +1,47 @@ +use dioxus_core::ScopeState; +use std::future::Future; + +#[macro_export] +macro_rules! use_callback { + // ($cx:ident, || || $($rest:tt)*) => { use_callback( $cx, (), |_| $($rest)* ) }; + // ($cx:ident, || || $($rest:tt)*) => { use_callback( $cx, (), |_| $($rest)* ) }; + ($cx:ident, || $($rest:tt)*) => { + use_callback( + $cx, + move || $($rest)* + ) + }; + ($cx:ident, |$($args:tt),* | $($rest:tt)*) => { + use_callback( + $cx, + move || $($rest)* + ) + }; + ($cx:ident, $($rest:tt)*) => { + use_callback( + $cx, + move || $($rest)* + ) + }; +} +pub fn use_callback(cx: &ScopeState, make: impl FnOnce() -> R) -> impl FnMut(T) + '_ +where + R: FnMut(T) -> F + 'static, + F: Future + 'static, +{ + let mut hook = make(); + + move |evt| cx.spawn(hook(evt)) +} + +fn _it_works(cx: &ScopeState) { + let _p = use_callback(cx, || { + |()| async { + // + } + }); + + // let p = use_callback!(cx, || |evt| async { + // // + // }); +} diff --git a/packages/hooks/src/usecollection.rs b/packages/hooks/src/usecollection.rs index 0eed35b31..b303376b3 100644 --- a/packages/hooks/src/usecollection.rs +++ b/packages/hooks/src/usecollection.rs @@ -16,7 +16,7 @@ This is moderately efficient because the fields of the map are moved, but the da However, if you used similar approach with Dioxus: ```rust -let (map, set_map) = use_state(&cx, || HashMap::new()); +let (map, set_map) = use_state(cx, || HashMap::new()); set_map({ let mut newmap = map.clone(); newmap.set(key, value); diff --git a/packages/hooks/src/usecontext.rs b/packages/hooks/src/usecontext.rs new file mode 100644 index 000000000..fdf363252 --- /dev/null +++ b/packages/hooks/src/usecontext.rs @@ -0,0 +1,19 @@ +use dioxus_core::ScopeState; + +/// Consume some context in the tree, providing a sharable handle to the value +/// +/// Does not regenerate the value if the value is changed at the parent. +pub fn use_context(cx: &ScopeState) -> Option<&T> { + cx.use_hook(|| cx.consume_context::()).as_ref() +} + +/// Provide some context via the tree and return a reference to it +/// +/// Once the context has been provided, it is immutable. Mutations should be done via interior mutability. +pub fn use_context_provider(cx: &ScopeState, f: impl FnOnce() -> T) -> &T { + cx.use_hook(|| { + let val = f(); + cx.provide_context(val.clone()); + val + }) +} diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 503c0e91a..8c735bd80 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -42,7 +42,7 @@ use std::future::Future; /// Stop, /// } /// -/// let chat_client = use_coroutine(&cx, |rx: UnboundedReceiver| async move { +/// let chat_client = use_coroutine(cx, |rx: UnboundedReceiver| async move { /// while let Some(action) = rx.next().await { /// match action { /// Action::Start => {} @@ -59,7 +59,7 @@ use std::future::Future; /// } /// }) /// ``` -pub fn use_coroutine(cx: &ScopeState, init: G) -> &CoroutineHandle +pub fn use_coroutine(cx: &ScopeState, init: G) -> &Coroutine where M: 'static, G: FnOnce(UnboundedReceiver) -> F, @@ -68,24 +68,25 @@ where cx.use_hook(|| { let (tx, rx) = futures_channel::mpsc::unbounded(); let task = cx.push_future(init(rx)); - cx.provide_context(CoroutineHandle { tx, task }) + cx.provide_context(Coroutine { tx, task }) }) } /// Get a handle to a coroutine higher in the tree /// /// See the docs for [`use_coroutine`] for more details. -pub fn use_coroutine_handle(cx: &ScopeState) -> Option<&CoroutineHandle> { - cx.use_hook(|| cx.consume_context::>()) +pub fn use_coroutine_handle(cx: &ScopeState) -> Option<&Coroutine> { + cx.use_hook(|| cx.consume_context::>()) .as_ref() } -pub struct CoroutineHandle { +pub struct Coroutine { tx: UnboundedSender, task: TaskId, } -impl Clone for CoroutineHandle { +// for use in futures +impl Clone for Coroutine { fn clone(&self) -> Self { Self { tx: self.tx.clone(), @@ -94,7 +95,7 @@ impl Clone for CoroutineHandle { } } -impl CoroutineHandle { +impl Coroutine { /// Get the ID of this coroutine #[must_use] pub fn task_id(&self) -> TaskId { @@ -112,22 +113,22 @@ mod tests { #![allow(unused)] use super::*; - use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::prelude::*; + use futures_channel::mpsc::unbounded; use futures_util::StreamExt; fn app(cx: Scope, name: String) -> Element { - let task = use_coroutine(&cx, |mut rx: UnboundedReceiver| async move { + let task = use_coroutine(cx, |mut rx: UnboundedReceiver| async move { while let Some(msg) = rx.next().await { println!("got message: {}", msg); } }); - let task2 = use_coroutine(&cx, view_task); + let task2 = use_coroutine(cx, view_task); - let task3 = use_coroutine(&cx, |rx| complex_task(rx, 10)); + let task3 = use_coroutine(cx, |rx| complex_task(rx, 10)); - None + todo!() } async fn view_task(mut rx: UnboundedReceiver) { diff --git a/packages/hooks/src/useeffect.rs b/packages/hooks/src/useeffect.rs index e82335222..f9ef70b84 100644 --- a/packages/hooks/src/useeffect.rs +++ b/packages/hooks/src/useeffect.rs @@ -17,7 +17,7 @@ use crate::UseFutureDep; /// /// #[inline_props] /// fn app(cx: Scope, name: &str) -> Element { -/// use_effect(&cx, (name,), |(name,)| async move { +/// use_effect(cx, (name,), |(name,)| async move { /// set_title(name); /// })) /// } @@ -72,21 +72,21 @@ mod tests { fn app(cx: Scope) -> Element { // should only ever run once - use_effect(&cx, (), |_| async move { + use_effect(cx, (), |_| async move { // }); // runs when a is changed - use_effect(&cx, (&cx.props.a,), |(a,)| async move { + use_effect(cx, (&cx.props.a,), |(a,)| async move { // }); // runs when a or b is changed - use_effect(&cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { + use_effect(cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { // }); - None + todo!() } } } diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 1aa7800ec..df34a6433 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -1,6 +1,12 @@ #![allow(missing_docs)] use dioxus_core::{ScopeState, TaskId}; -use std::{any::Any, cell::Cell, future::Future, rc::Rc, sync::Arc}; +use std::{ + any::Any, + cell::{Cell, RefCell}, + future::{Future, IntoFuture}, + rc::Rc, + sync::Arc, +}; /// A future that resolves to a value. /// @@ -28,16 +34,13 @@ where let state = cx.use_hook(move || UseFuture { update: cx.schedule_update(), needs_regen: Cell::new(true), - slot: Rc::new(Cell::new(None)), - value: None, + values: Default::default(), task: Cell::new(None), dependencies: Vec::new(), + waker: Default::default(), }); - if let Some(value) = state.slot.take() { - state.value = Some(value); - state.task.set(None); - } + *state.waker.borrow_mut() = None; if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { // We don't need regen anymore @@ -47,8 +50,9 @@ where let fut = future(dependencies.out()); // Clone in our cells - let slot = state.slot.clone(); + let values = state.values.clone(); let schedule_update = state.update.clone(); + let waker = state.waker.clone(); // Cancel the current future if let Some(current) = state.task.take() { @@ -57,8 +61,13 @@ where state.task.set(Some(cx.push_future(async move { let res = fut.await; - slot.set(Some(res)); - schedule_update(); + values.borrow_mut().push(Box::leak(Box::new(res))); + + // if there's a waker, we dont re-render the component. Instead we just progress that future + match waker.borrow().as_ref() { + Some(waker) => waker.wake_by_ref(), + None => schedule_update(), + } }))); } @@ -74,10 +83,10 @@ pub enum FutureState<'a, T> { pub struct UseFuture { update: Arc, needs_regen: Cell, - value: Option, - slot: Rc>>, task: Cell>, dependencies: Vec>, + waker: Rc>>, + values: Rc>>, } pub enum UseFutureState<'a, T> { @@ -105,22 +114,28 @@ impl UseFuture { // clears the value in the future slot without starting the future over pub fn clear(&self) -> Option { - (self.update)(); - self.slot.replace(None) + todo!() + // (self.update)(); + // self.slot.replace(None) } // Manually set the value in the future slot without starting the future over - pub fn set(&self, new_value: T) { - self.slot.set(Some(new_value)); - self.needs_regen.set(true); - (self.update)(); + pub fn set(&self, _new_value: T) { + // self.slot.set(Some(new_value)); + // self.needs_regen.set(true); + // (self.update)(); + todo!() } /// Return any value, even old values if the future has not yet resolved. /// /// If the future has never completed, the returned value will be `None`. pub fn value(&self) -> Option<&T> { - self.value.as_ref() + self.values + .borrow_mut() + .last() + .cloned() + .map(|x| unsafe { &*x }) } /// Get the ID of the future in Dioxus' internal scheduler @@ -130,18 +145,48 @@ impl UseFuture { /// Get the current stateof the future. pub fn state(&self) -> UseFutureState { - match (&self.task.get(), &self.value) { - // If we have a task and an existing value, we're reloading - (Some(_), Some(val)) => UseFutureState::Reloading(val), + 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), - // 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, + // } + } +} + +impl<'a, T> IntoFuture for &'a UseFuture { + type Output = &'a T; + type IntoFuture = UseFutureAwait<'a, T>; + fn into_future(self) -> Self::IntoFuture { + UseFutureAwait { hook: self } + } +} + +pub struct UseFutureAwait<'a, T> { + hook: &'a UseFuture, +} + +impl<'a, T> Future for UseFutureAwait<'a, T> { + type Output = &'a T; + + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + match self.hook.values.borrow_mut().last().cloned() { + Some(value) => std::task::Poll::Ready(unsafe { &*value }), + None => { + self.hook.waker.replace(Some(cx.waker().clone())); + std::task::Poll::Pending + } } } } @@ -239,6 +284,19 @@ impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f,); impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f, G = g,); impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f, G = g, H = h,); +/// A helper macro that merges uses the closure syntax to elaborate the dependency array +#[macro_export] +macro_rules! use_future { + ($cx:ident, || $($rest:tt)*) => { use_future( $cx, (), |_| $($rest)* ) }; + ($cx:ident, | $($args:tt),* | $($rest:tt)*) => { + use_future( + $cx, + ($($args),*), + |($($args),*)| $($rest)* + ) + }; +} + #[cfg(test)] mod tests { use super::*; @@ -256,23 +314,33 @@ mod tests { e: i32, } - fn app(cx: Scope) -> Element { + async fn app(cx: Scope<'_, MyProps>) -> Element { // should only ever run once - let fut = use_future(&cx, (), |_| async move { - // - }); + let fut = use_future(cx, (), |_| async move {}); // runs when a is changed - let fut = use_future(&cx, (&cx.props.a,), |(a,)| async move { - // - }); + let fut = use_future(cx, (&cx.props.a,), |(a,)| async move {}); // runs when a or b is changed - let fut = use_future(&cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { - // + let fut = use_future(cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { 123 }); + + let a = use_future!(cx, || async move { + // do the thing! }); - None + let b = &123; + let c = &123; + + let a = use_future!(cx, |b, c| async move { + let a = b + c; + let blah = "asd"; + }); + + let g2 = a.await; + + let g = fut.await; + + todo!() } } } diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 78cf643b1..2e1aa7295 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -19,7 +19,7 @@ use std::{ /// /// ```ignore /// const Example: Component = |cx| { -/// let count = use_state(&cx, || 0); +/// let count = use_state(cx, || 0); /// /// cx.render(rsx! { /// div { @@ -39,7 +39,7 @@ pub fn use_state( let update_callback = cx.schedule_update(); let slot = Rc::new(RefCell::new(current_val.clone())); let setter = Rc::new({ - dioxus_core::to_owned![update_callback, slot]; + to_owned![update_callback, slot]; move |new| { { let mut slot = slot.borrow_mut(); @@ -92,7 +92,7 @@ impl UseState { /// /// ```rust, ignore /// fn component(cx: Scope) -> Element { - /// let count = use_state(&cx, || 0); + /// let count = use_state(cx, || 0); /// cx.spawn({ /// let set_count = count.to_owned(); /// async move { @@ -119,7 +119,7 @@ impl UseState { /// /// ```rust, ignore /// fn component(cx: Scope) -> Element { - /// let value = use_state(&cx, || 0); + /// let value = use_state(cx, || 0); /// /// rsx!{ /// Component { @@ -144,7 +144,7 @@ impl UseState { /// # use dioxus_core::prelude::*; /// # use dioxus_hooks::*; /// fn component(cx: Scope) -> Element { - /// let value = use_state(&cx, || 0); + /// let value = use_state(cx, || 0); /// /// // to increment the value /// value.modify(|v| v + 1); @@ -185,7 +185,7 @@ impl UseState { /// # use dioxus_core::prelude::*; /// # use dioxus_hooks::*; /// fn component(cx: Scope) -> Element { - /// let value = use_state(&cx, || 0); + /// let value = use_state(cx, || 0); /// /// let as_rc = value.get(); /// assert_eq!(as_rc.as_ref(), &0); @@ -207,7 +207,7 @@ impl UseState { /// /// ```rust, ignore /// fn component(cx: Scope) -> Element { - /// let count = use_state(&cx, || 0); + /// let count = use_state(cx, || 0); /// cx.spawn({ /// let count = count.to_owned(); /// async move { @@ -237,7 +237,7 @@ impl UseState { /// # Examples /// /// ```rust, ignore - /// let val = use_state(&cx, || 0); + /// let val = use_state(cx, || 0); /// /// val.with_mut(|v| *v = 1); /// ``` @@ -269,7 +269,7 @@ impl UseState { /// # Examples /// /// ```rust, ignore - /// let val = use_state(&cx, || 0); + /// let val = use_state(cx, || 0); /// /// *val.make_mut() += 1; /// ``` @@ -448,7 +448,7 @@ impl + Copy> std::ops::DivAssign for UseState { fn api_makes_sense() { #[allow(unused)] fn app(cx: Scope) -> Element { - let val = use_state(&cx, || 0); + let val = use_state(cx, || 0); val.set(0); val.modify(|v| v + 1); @@ -466,12 +466,14 @@ fn api_makes_sense() { } cx.spawn({ - dioxus_core::to_owned![val]; + to_owned![val]; async move { val.modify(|f| f + 1); } }); - cx.render(LazyNodes::new(|f| f.static_text("asd"))) + // cx.render(LazyNodes::new(|f| f.static_text("asd"))) + + todo!() } } diff --git a/packages/hooks/src/usesuspense.rs b/packages/hooks/src/usesuspense.rs deleted file mode 100644 index d4405bd88..000000000 --- a/packages/hooks/src/usesuspense.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::{cell::Cell, future::Future, rc::Rc}; - -use dioxus_core::{Element, ScopeState, TaskId}; - -pub fn use_suspense + 'static>( - cx: &ScopeState, - create_future: impl FnOnce() -> F, - render: impl FnOnce(&R) -> Element, -) -> Element { - let sus = cx.use_hook(|| { - let fut = create_future(); - - let wip_value: Rc>> = Default::default(); - - let wip = wip_value.clone(); - let new_fut = async move { - let val = fut.await; - wip.set(Some(val)); - }; - - let task = cx.push_future(new_fut); - SuspenseInner { - _task: task, - value: None, - _wip_value: wip_value, - } - }); - - if let Some(value) = sus.value.as_ref() { - render(value) - } else { - // generate a placeholder node if the future isnt ready - None - } -} - -struct SuspenseInner { - _task: TaskId, - _wip_value: Rc>>, - value: Option, -} diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 8464189ea..446da9303 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -18,6 +18,7 @@ wasm-bindgen = { version = "0.2.79", optional = true } euclid = "0.22.7" enumset = "1.0.11" keyboard-types = "0.6.2" +async-trait = "0.1.58" [dependencies.web-sys] optional = true diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index ded9a2169..efbb4d797 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1,6 +1,8 @@ +#![allow(non_upper_case_globals)] + use crate::{GlobalAttributes, SvgAttributes}; -use dioxus_core::*; -use std::fmt::Arguments; + +pub type AttributeDiscription = (&'static str, Option<&'static str>, bool); macro_rules! builder_constructors { ( @@ -19,21 +21,17 @@ macro_rules! builder_constructors { $(#[$attr])* pub struct $name; - impl DioxusElement for $name { - const TAG_NAME: &'static str = stringify!($name); - const NAME_SPACE: Option<&'static str> = None; + + impl $name { + pub const TAG_NAME: &'static str = stringify!($name); + pub const NAME_SPACE: Option<&'static str> = None; + + $( + pub const $fil: AttributeDiscription = (stringify!($fil), None, false); + )* } impl GlobalAttributes for $name {} - - impl $name { - $( - $(#[$attr_method])* - pub fn $fil<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { - cx.attr(stringify!($fil), val, None, false) - } - )* - } )* }; @@ -48,18 +46,14 @@ macro_rules! builder_constructors { $(#[$attr])* pub struct $name; - impl DioxusElement for $name { - const TAG_NAME: &'static str = stringify!($name); - const NAME_SPACE: Option<&'static str> = Some($namespace); - } - impl SvgAttributes for $name {} impl $name { + pub const TAG_NAME: &'static str = stringify!($name); + pub const NAME_SPACE: Option<&'static str> = Some($namespace); + $( - pub fn $fil<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { - cx.attr(stringify!($fil), val, Some(stringify!($namespace)), false) - } + pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($namespace)), false) )* } )* @@ -499,7 +493,9 @@ builder_constructors! { /// Build a /// [`