diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..332e03eb2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: jkelleyrtp # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +open_collective: dioxus-labs # Replace with a single Open Collective username diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..e31219bb6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve Dioxus +--- + +**Problem** + + + +**Steps To Reproduce** + +Steps to reproduce the behavior: + +- +- +- + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots to help explain your problem. + +**Environment:** + - Dioxus version: [e.g. v0.17, `master`] + - Rust version: [e.g. 1.43.0, `nightly`] + - OS info: [e.g. MacOS] + - App platform: [e.g. `web`, `desktop`] + +**Questionnaire** + +- [ ] I'm interested in fixing this myself but don't know where to start +- [ ] I would like to fix and I have a solution +- [ ] I don't have time to fix this right now, but maybe later \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_requst.md b/.github/ISSUE_TEMPLATE/feature_requst.md new file mode 100644 index 000000000..f8735b9a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_requst.md @@ -0,0 +1,16 @@ +--- +name: Feature Request +about: If you have any interesting advice, you can tell us. +--- + +## Specific Demand + + + +## Implement Suggestion + + \ No newline at end of file diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 183fb282d..2b6ea5c23 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -19,13 +19,7 @@ jobs: max-parallel: 2 fail-fast: false matrix: - target: - [ - i686-pc-windows-gnu, - i686-pc-windows-msvc, - x86_64-pc-windows-gnu, - x86_64-pc-windows-msvc, - ] + target: [x86_64-pc-windows-gnu, x86_64-pc-windows-msvc] cfg_release_channel: [stable] steps: @@ -47,12 +41,6 @@ jobs: rustup target add ${{ matrix.target }} shell: powershell - - name: Add mingw32 to path for i686-gnu - run: | - echo "C:\msys64\mingw32\bin" >> $GITHUB_PATH - if: matrix.target == 'i686-pc-windows-gnu' && matrix.channel == 'nightly' - shell: bash - - name: Add mingw64 to path for x86_64-gnu run: echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly' diff --git a/.gitignore b/.gitignore index 9534dc207..9df7e9f93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/dist Cargo.lock .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b624794..b056a9590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Commit Statistics + + + + - 1 commit contributed to the release over the course of 7 calendar days. + - 0 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) +
+ ## v0.1.7 (2022-01-08) ### Bug Fixes diff --git a/Cargo.toml b/Cargo.toml index 884513a5b..468518f5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,26 @@ [package] name = "dioxus" -version = "0.1.7" +version = "0.1.8" authors = ["Jonathan Kelley"] edition = "2018" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "./packages/core", version = "^0.1.7" } -dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } -dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.6", optional = true } -dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } +dioxus-core = { path = "./packages/core", version = "^0.1.9" } +dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true } +dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } +dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } -dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } -dioxus-desktop = { path = "./packages/desktop", version = "^0.1.5", optional = true } -dioxus-ssr = { path = "./packages/ssr", version = "^0.1.2", optional = true } +dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } +dioxus-desktop = { path = "./packages/desktop", version = "^0.1.6", optional = true } +dioxus-ssr = { path = "./packages/ssr", version = "^0.1.3", optional = true } -dioxus-router = { path = "./packages/router", version = "^0.1.0", optional = true } +dioxus-router = { path = "./packages/router", version = "^0.1.1", optional = true } dioxus-mobile = { path = "./packages/mobile", version = "^0.0.3", optional = true } # dioxus-liveview = { path = "./packages/liveview", optional = true } diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8dbc080b8..000000000 --- a/LICENSE +++ /dev/null @@ -1 +0,0 @@ -MIT/Apache-2 diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 000000000..1b5ec8b78 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 000000000..31aa79387 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 641f5a8b8..52493b119 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,12 @@ - Awesome Page + Discord Link +

Website @@ -64,12 +65,12 @@ 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 (count, set_count) = use_state(&cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } )) } ``` @@ -84,7 +85,7 @@ If you know React, then you already know Dioxus. - Comprehensive inline documentation - hover and guides for all HTML elements, listeners, and events. - Extremely memory efficient - 0 global allocations for steady-state components. - Multi-channel asynchronous scheduler for first-class async support. -- And more! Read the [full release post here](https://dioxuslabs.com/blog/introducing-dioxus/). +- And more! Read the [full release post](https://dioxuslabs.com/blog/introducing-dioxus/). ### Examples @@ -121,9 +122,9 @@ See the [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) page for ## Why Dioxus and why Rust? -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. +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. -In contrast, Dioxus is written in Rust - which is almost like "TypeScript on steroids". +In contrast, Dioxus is written in Rust - which is almost like "TypeScript on steroids". By using Rust, we gain: @@ -141,76 +142,56 @@ By using Rust, we gain: Specifically, Dioxus provides us many other assurances: -- Proper use of immutable datastructures -- Guaranteed error handling (so you can sleep easy at night not worrying about `cannot read property of undefined`) +- 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. +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? +## 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 ThreadSafe) +- 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**. +## 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 of React-hooks, no borrowed props, supports SSR (no hydration). -- [Percy](https://github.com/chinedufn/percy): Supports SSR but less emphasis on state management and event handling. +- [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 lacking in ergonomics. - [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 +## 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. This parity table does not necessarily include important ecosystem crates like code blocks, markdown, resizing hooks, etc. +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 -| Feature | Dioxus | React | Notes for Dioxus | -| ------------------------- | ------ | ----- | -------------------------------------------------------------------- | -| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | -| Map, Iterator | ✅ | ✅ | map/filter/reduce to produce rsx! | -| Keyed Components | ✅ | ✅ | advanced diffing with keys | -| Web | ✅ | ✅ | renderer for web browser | -| Desktop (webview) | ✅ | ✅ | renderer for desktop | -| Shared State (Context) | ✅ | ✅ | share state through the tree | -| Hooks | ✅ | ✅ | memory cells in components | -| SSR | ✅ | ✅ | render directly to string | -| Component Children | ✅ | ✅ | cx.children() as a list of nodes | -| Headless components | ✅ | ✅ | components that don't return real elements | -| Fragments | ✅ | ✅ | multiple elements without a real root | -| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax | -| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | -| CSS/Inline Styles | ✅ | ✅ | syntax for inline styles/attribute groups | -| Custom elements | ✅ | ✅ | Define new element primitives | -| Suspense | ✅ | ✅ | schedule future render from future/promise | -| Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax | -| NodeRef | ✅ | ✅ | gain direct access to nodes | -| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint | -| Jank-Free Rendering | ✅ | ✅ | Large diffs are segmented across frames for silky-smooth transitions | -| Effects | ✅ | ✅ | Run effects after a component has been committed to render | -| Portals | 🛠 | ✅ | Render nodes outside of the traditional tree structure | -| Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events | -| Server Components | 🛠 | ✅ | Hybrid components for SPA and Server | -| Bundle Splitting | 👀 | ✅ | Efficiently and asynchronously load the app | -| Lazy Components | 👀 | ✅ | Dynamically load the new components as the page is loaded | -| 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 | -| 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 | +Dioxus is unique in the Rust ecosystem in that it supports: -- ✅ = implemented and working -- 🛠 = actively being worked on -- 👀 = not yet implemented or being worked on -- ❓ = not sure if will or can implement +- 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 diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index c91f9f1bb..c00dcd274 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -22,7 +22,7 @@ In general, Dioxus and React share many functional similarities. If this guide i ## Multiplatform -Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to Web-Sys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `Html` feature enabled which can be disabled depending on your target renderer. +Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to Web-Sys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `html` feature enabled, but this can be disabled depending on your target renderer. Right now, we have several 1st-party renderers: - WebSys (for WASM) @@ -33,8 +33,7 @@ Right now, we have several 1st-party renderers: ### Web Support --- - -The Web is the most-supported target platform for Dioxus. To run on the Web, your app must be compiled to WebAssembly and depend on the `dioxus` crate with the `web` feature enabled. Because of the Wasm limitation, not every crate will work with your web-apps, so you'll need to make sure that your crates work without native system calls (timers, IO, etc). +The Web is the best-supported target platform for Dioxus. To run on the Web, your app must be compiled to WebAssembly and depend on the `dioxus` crate with the `web` feature enabled. Because of the limitations of Wasm not every crate will work with your web-apps, so you'll need to make sure that your crates work without native system calls (timers, IO, etc). Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features. @@ -45,9 +44,10 @@ Examples: - [ECommerce](https://github.com/DioxusLabs/example-projects/tree/master/ecommerce-site) [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) + ### SSR Support --- -Dioxus supports server-side rendering! +Dioxus supports server-side rendering! For rendering statically to an `.html` file or from a WebServer, then you'll want to make sure the `ssr` feature is enabled in the `dioxus` crate and use the `dioxus::ssr` API. We don't expect the SSR API to change drastically in the future. @@ -90,13 +90,13 @@ Examples: ### LiveView / Server Component Support --- -The internal architecture of Dioxus was designed from day one to support the `LiveView` use-case, where a web server hosts a running app for each connected user. As of today, there is no first-class LiveView support - you'll need to wire this up yourself. +The internal architecture of Dioxus was designed from day one to support the `LiveView` use-case, where a web server hosts a running app for each connected user. As of today, there is no first-class LiveView support - you'll need to wire this up yourself. While not currently fully implemented, the expectation is that LiveView apps can be a hybrid between Wasm and server-rendered where only portions of a page are "live" and the rest of the page is either server-rendered, statically generated, or handled by the host SPA. ### Multithreaded Support --- -The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. This means you can't easily use Dioxus with most web frameworks like Tide, Rocket, Axum, etc. +The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. This means you can't easily use Dioxus with most web frameworks like Tide, Rocket, Axum, etc. To solve this, you'll want to spawn a VirtualDom on its own thread and communicate with it via channels. diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 2e77d4ab8..2fa29f39e 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -1,7 +1,7 @@ # Summary - [Introduction](README.md) -- [Getting Setup](setup.md) +- [Getting Set Up](setup.md) - [Hello, World!](hello_world.md) - [Describing the UI](elements/index.md) - [Intro to Elements](elements/vnodes.md) @@ -17,8 +17,8 @@ - [User Input and Controlled Components](interactivity/user_input.md) - [Lifecycle, updates, and effects](interactivity/lifecycles.md) - [Managing State](state/index.md) - - [Local State](state/localstate.md) - - [Lifting State](state/liftingstate.md) + - [Local State](state/localstate.md) + - [Lifting State](state/liftingstate.md) - [Global State](state/sharedstate.md) - [Error handling](state/errorhandling.md) - [Working with Async](async/index.md) diff --git a/docs/guide/src/advanced-guides/00-index.md b/docs/guide/src/advanced-guides/00-index.md index 13a61a868..aedc39e34 100644 --- a/docs/guide/src/advanced-guides/00-index.md +++ b/docs/guide/src/advanced-guides/00-index.md @@ -1,6 +1,6 @@ # Core Topics -In this chapter, we'll cover some core topics on how Dioxus works and how to best leverage the features to build a beautiful, reactive app. +In this chapter, we'll cover some core topics about how Dioxus works and how to best leverage the features to build a beautiful, reactive app. At a very high level, Dioxus is simply a Rust framework for _declaring_ user interfaces and _reacting_ to changes. diff --git a/docs/guide/src/advanced-guides/10-concurrent-mode.md b/docs/guide/src/advanced-guides/10-concurrent-mode.md index 5d7801cb6..6bbebbeed 100644 --- a/docs/guide/src/advanced-guides/10-concurrent-mode.md +++ b/docs/guide/src/advanced-guides/10-concurrent-mode.md @@ -33,9 +33,9 @@ async fetch_name() -> String { This component will only schedule its render once the fetch is complete. However, we _don't_ recommend using async/await directly in your components. -Async is a notoriously challenging yet rewarding tool for efficient tools. If not careful, locking and unlocking shared aspects of the component's context can lead to data races and panics. If a shared resource is locked while the component is awaiting, then other components can be locked or panic when trying to access the same resource. These rules are especially important when references to shared global state are accessed using the context object's lifetime. If mutable references to data captured immutably by the context are taken, then the component will panic, and cause confusion. +Async is a notoriously challenging yet rewarding tool for efficient tools. If not careful, locking and unlocking shared aspects of the component's context can lead to data races and panics. If a shared resource is locked while the component is awaiting, then other components can be locked or panic when trying to access the same resource. These rules are especially important when references to shared global state are accessed using the context object's lifetime. If mutable references to data captured immutably by the context are taken, then the component will panic, causing confusion. -Instead, we suggest using hooks and future combinators that can safely utilize the safe guards of the component's Context when interacting with async tasks. +Instead, we suggest using hooks and future combinators that can safely utilize the safeguards of the component's Context when interacting with async tasks. As part of our Dioxus hooks crate, we provide a data loader hook which pauses a component until its async dependencies are ready. This caches requests, reruns the fetch if dependencies have changed, and provides the option to render something else while the component is loading. diff --git a/docs/guide/src/advanced-guides/11-arena-memo.md b/docs/guide/src/advanced-guides/11-arena-memo.md index d95a67d19..d5eaaa1d0 100644 --- a/docs/guide/src/advanced-guides/11-arena-memo.md +++ b/docs/guide/src/advanced-guides/11-arena-memo.md @@ -2,13 +2,13 @@ Dioxus differs slightly from other UI virtual doms in some subtle ways due to its memory allocator. -One important aspect to understand is how props are passed down from parent components to children. All "components" (custom user-made UI elements) are tightly allocated together in an arena. However, because props and hooks are generically typed, they are casted to any and allocated on the heap - not in the arena with the components. +One important aspect to understand is how props are passed down from parent components to children. All "components" (custom user-made UI elements) are tightly allocated together in an arena. However, because props and hooks are generically typed, they are casted to `Any` and allocated on the heap - not in the arena with the components. With this system, we try to be more efficient when leaving the component arena and entering the heap. By default, props are memoized between renders using COW and context. This makes props comparisons fast - done via ptr comparisons on the cow pointer. Because memoization is done by default, parent re-renders will _not_ cascade to children if the child's props did not change. https://dmitripavlutin.com/use-react-memo-wisely/ -This behavior is defined as an implicit attribute to user components. When in React land you might wrap a component is `react.memo`, Dioxus components are automatically memoized via an implicit attribute. You can manually configure this behavior on any component with "nomemo" to disable memoization. +This behavior is defined as an attribute implicit to user components. When in React land you might wrap a component with `react.memo`, Dioxus components are automatically memoized via an implicit attribute. You can manually configure this behavior on any component with "nomemo" to disable memoization. ```rust fn test() -> DomTree { @@ -40,7 +40,7 @@ fn test_component(cx: Scope, name: String) -> Element { "This is different than React, why differ?". -Take a component likes this: +Take a component like this: ```rust fn test(cx: Scope) -> DomTree { @@ -55,4 +55,4 @@ fn test(cx: Scope) -> DomTree { } ``` -While the contents of the destructured bundle might change, not every child component will need to be re-rendered. +While the contents of the destructured bundle might change, not every child component will need to be re-rendered every time the context changes. diff --git a/docs/guide/src/advanced-guides/12-signals.md b/docs/guide/src/advanced-guides/12-signals.md index 051247475..cae2e1726 100644 --- a/docs/guide/src/advanced-guides/12-signals.md +++ b/docs/guide/src/advanced-guides/12-signals.md @@ -1,6 +1,6 @@ # Signals: Skipping the Diff -In most cases, the traditional VirtualDOM diffing pattern is plenty fast. Dioxus will compare trees of VNodes, find the differences, and then update the Renderer's DOM with the updates. However, this can generate a lot of overhead for certain types of components. In apps where reducing visual latency is a top priority, you can opt into the `Signals` api to entirely disable diffing of hot-path components. Dioxus will then automatically construct a state machine for your component, making updates nearly instant. +In most cases, the traditional VirtualDOM diffing pattern is plenty fast. Dioxus will compare trees of VNodes, find the differences, and then update the Renderer's DOM with the diffs. However, this can generate a lot of overhead for certain types of components. In apps where reducing visual latency is a top priority, you can opt into the `Signals` api to entirely disable diffing of hot-path components. Dioxus will then automatically construct a state machine for your component, making updates nearly instant. Signals build on the same infrastructure that powers asynchronous rendering where in-tree values can be updated outside of the render phase. In async rendering, a future is used as the signal source. With the raw signal API, any value can be used as a signal source. @@ -69,7 +69,7 @@ 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 that a basic operation (`std::ops`) we can either chain signals together or combine them: +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); @@ -88,7 +88,7 @@ let mut a = use_signal(&cx, || 0); let c = *a + *b; ``` -Calling `deref` or `derefmut` is actually more complex than it seems. When a value is derefed, you're essentially telling Dioxus that _this_ element _needs_ to be subscribed to the signal. If a signal is derefed outside of an element, the entire component will be subscribed and the advantage of skipping diffing will be lost. Dioxus will throw an error in the console when this happens to tell you that you're using signals wrong, but your component will continue to work. +Calling `deref` or `deref_mut` is actually more complex than it seems. When a value is derefed, you're essentially telling Dioxus that _this_ element _needs_ to be subscribed to the signal. If a signal is derefed outside of an element, the entire component will be subscribed and the advantage of skipping diffing will be lost. Dioxus will throw an error in the console when this happens to tell you that you're using signals wrong, but your component will continue to work. ## Global Signals @@ -128,7 +128,7 @@ Sometimes you want to use a collection of items. With Signals, you can bypass di By default, Dioxus is limited when you use iter/map. With the `For` component, you can provide an iterator and a function for the iterator to map to. -Dioxus automatically understands how to use your signals when mixed with iterators through Deref/DerefMut. This lets you efficiently map collections while avoiding the re-rendering of lists. In essence, signals act as a hint to Dioxus on how to avoid un-necessary checks and renders, making your app faster. +Dioxus automatically understands how to use your signals when mixed with iterators through `Deref`/`DerefMut`. This lets you efficiently map collections while avoiding the re-rendering of lists. In essence, signals act as a hint to Dioxus on how to avoid un-necessary checks and renders, making your app faster. ```rust const DICT: AtomFamily = |_| {}; diff --git a/docs/guide/src/advanced-guides/13-subtrees.md b/docs/guide/src/advanced-guides/13-subtrees.md index 37a2ef786..7c99ecf4f 100644 --- a/docs/guide/src/advanced-guides/13-subtrees.md +++ b/docs/guide/src/advanced-guides/13-subtrees.md @@ -6,7 +6,7 @@ For a VirtualDom that has a root tree with two subtrees, the edits follow a patt Root -> Tree 1 --> Tree 2 +-> Tree 2 -> Original root tree - Root edits @@ -39,7 +39,7 @@ fn Window() -> DomTree { onassign: move |e| { // create window } - children() + children() } } diff --git a/docs/guide/src/advanced-guides/custom-renderer.md b/docs/guide/src/advanced-guides/custom-renderer.md index fbff277b0..d2aa40bc9 100644 --- a/docs/guide/src/advanced-guides/custom-renderer.md +++ b/docs/guide/src/advanced-guides/custom-renderer.md @@ -20,7 +20,7 @@ For reference, check out the WebSys renderer as a starting point for your custom ## Trait implementation and DomEdits -The current `RealDom` trait lives in `dioxus_core/diff`. A version of it is provided here (but might not be up-to-date): +The current `RealDom` trait lives in `dioxus-core/diff`. A version of it is provided here (but might not be up-to-date): ```rust pub trait RealDom<'a> { diff --git a/docs/guide/src/async/index.md b/docs/guide/src/async/index.md index 41a8b9231..47b84e81a 100644 --- a/docs/guide/src/async/index.md +++ b/docs/guide/src/async/index.md @@ -1,13 +1,13 @@ # Working with Async -Not all apps you'll build can be self-contained with synchronous code. You'll often need to interact with file systems, network interfaces, hardware, or timers. +Not all apps you'll build can be self-contained with synchronous code. You'll often need to interact with file systems, network interfaces, hardware, or timers. So far, we've only talked about building apps with synchronous code, so this chapter will focus integrating asynchronous code into your app. ## The Runtime -By default, Dioxus-Desktop ships with the `Tokio` runtime and automatically sets everything up for you. +By default, Dioxus-Desktop ships with the `Tokio` runtime and automatically sets everything up for you. diff --git a/docs/guide/src/elements/component_children.md b/docs/guide/src/elements/component_children.md index d8469241a..d99273394 100644 --- a/docs/guide/src/elements/component_children.md +++ b/docs/guide/src/elements/component_children.md @@ -9,7 +9,7 @@ In this chapter, you'll learn about: ## The use case -Let's say you're building a user interface and want to make some part of it clickable to another website. You would normally start with the HTML `` tag, like so: +Let's say you're building a user interface and want to make some part of it a clickable link to another website. You would normally start with the HTML `` tag, like so: ```rust rsx!( @@ -98,7 +98,7 @@ struct ClickableProps<'a> { children: Element<'a> } -fn clickable(cx: Scope) -> Element { +fn Clickable(cx: Scope) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -162,7 +162,7 @@ struct ClickableProps<'a> { fn clickable(cx: Scope) -> Element { cx.render(rsx!( - a { + a { ..cx.props.attributes, "Any link, anywhere" } @@ -186,7 +186,7 @@ struct ClickableProps<'a> { fn clickable(cx: Scope) -> Element { cx.render(rsx!( - a { + a { onclick: move |evt| cx.props.onclick.call(evt) } )) diff --git a/docs/guide/src/elements/components.md b/docs/guide/src/elements/components.md index 8567e9270..ea43034d9 100644 --- a/docs/guide/src/elements/components.md +++ b/docs/guide/src/elements/components.md @@ -3,8 +3,8 @@ In the previous chapter, we learned about Elements and how they can be composed to create a basic User Interface. In this chapter, we'll learn how to group Elements together to form Components. In this chapter, we'll learn: -- What makes a Component -- How to model a component and its properties in Dioxus +- What makes a Component +- How to model a component and its properties in Dioxus - How to "think declaratively" ## What is a component? @@ -39,7 +39,7 @@ struct PostData { } ``` -If we look at the layout of the component, we notice quite a few buttons and functionality: +If we look at the layout of the component, we notice quite a few buttons and pieces of functionality: - Upvote/Downvote - View comments @@ -51,9 +51,9 @@ If we look at the layout of the component, we notice quite a few buttons and fun - Crosspost - Filter by site - View article -- Visit user +- Visit user -If we included all this functionality in one `rsx!` call, it would be huge! Instead, let's break the post down into some core pieces: +If we included all this functionality in one `rsx!` call it would be huge! Instead, let's break the post down into some core pieces: ![Post as Component](../images/reddit_post_components.png) @@ -112,7 +112,7 @@ Let's take a look at the `VoteButton` component. For now, we won't include any i Most of your Components will look exactly like this: a Props struct and a render function. Every component must take a `Scope` generic over some `Props` and return an `Element`. -As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`. +As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`. ```rust #[derive(PartialEq, Props)] @@ -133,9 +133,9 @@ fn VoteButton(cx: Scope) -> Element { ## Borrowing -You can avoid clones using borrowed component syntax. For example, let's say we passed the `TitleCard` title as an `&str` instead of `String`. In JavaScript, the string would be copied by reference - none of the contents would be copied, but rather the reference to the string's contents are copied. In Rust, this would be similar to calling `clone` on `Rc`. +You can avoid clones by using borrowed component syntax. For example, let's say we passed the `TitleCard` title as an `&str` instead of `String`. In JavaScript, the string would be copied by reference - none of the contents would be copied, but rather the reference to the string's contents are copied. In Rust, this would be similar to calling `clone` on `Rc`. -Because we're working in Rust, we can choose to either use `Rc`, clone `Title` on every re-render of `Post`, or simply borrow it. In most cases, you'll just want to let `Title` be cloned. +Because we're working in Rust, we can choose to either use `Rc`, clone `Title` on every re-render of `Post`, or simply borrow it. In most cases, you'll just want to let `Title` be cloned. To enable borrowed values for your component, we need to add a lifetime to let the Rust compiler know that the output `Element` borrows from the component's props. @@ -149,7 +149,7 @@ fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element { cx.render(rsx!{ h1 { "{cx.props.title}" } }) -} +} ``` For users of React: Dioxus knows *not* to memoize components that borrow property fields. By default, every component in Dioxus is memoized. This can be disabled by the presence of a non-`'static` borrow. @@ -174,7 +174,7 @@ fn TitleCard(cx: Scope) -> Element { cx.render(rsx!{ h1 { "{cx.props.title}" } }) -} +} ``` to: @@ -185,7 +185,7 @@ fn TitleCard(cx: Scope, title: String) -> Element { cx.render(rsx!{ h1 { "{title}" } }) -} +} ``` Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality. @@ -194,9 +194,9 @@ However, it's great for quickly throwing together an app without dealing with *a ## The `Scope` object -Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration. +Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration. -Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state? +Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state? React uses global variables to store this information. Global mutable variables must be carefully managed and are broadly discouraged in Rust programs. diff --git a/docs/guide/src/elements/conditional_rendering.md b/docs/guide/src/elements/conditional_rendering.md index 211da5d1a..c13263cf6 100644 --- a/docs/guide/src/elements/conditional_rendering.md +++ b/docs/guide/src/elements/conditional_rendering.md @@ -27,7 +27,7 @@ Now that we have a "logged_in" flag accessible in our props, we can render two d ```rust fn App(cx: Scope) -> Element { - if props.logged_in { + if cx.props.logged_in { cx.render(rsx!{ DashboardScreen {} }) @@ -39,7 +39,7 @@ fn App(cx: Scope) -> Element { } ``` -When the user is logged in, then this component will return the DashboardScreen. Else, the component will render the LoginScreen. +When the user is logged in, then this component will return the DashboardScreen. If they're not logged in, the component will render the LoginScreen. ## Using match statements diff --git a/docs/guide/src/elements/exporting_components.md b/docs/guide/src/elements/exporting_components.md index 16d6f1002..117249db8 100644 --- a/docs/guide/src/elements/exporting_components.md +++ b/docs/guide/src/elements/exporting_components.md @@ -25,27 +25,27 @@ fn main() { dioxus::desktop::launch(App); } -fn App(Scope) -> Element {} +fn App(Scope) -> Element {} #[derive(PartialEq, Props)] struct PostProps{} -fn Post(Scope) -> Element {} +fn Post(Scope) -> Element {} #[derive(PartialEq, Props)] struct VoteButtonsProps {} -fn VoteButtons(Scope) -> Element {} +fn VoteButtons(Scope) -> Element {} #[derive(PartialEq, Props)] struct TitleCardProps {} -fn TitleCard(Scope) -> Element {} +fn TitleCard(Scope) -> Element {} #[derive(PartialEq, Props)] struct MetaCardProps {} -fn MetaCard(Scope) -> Element {} +fn MetaCard(Scope) -> Element {} #[derive(PartialEq, Props)] struct ActionCardProps {} -fn ActionCard(Scope) -> Element {} +fn ActionCard(Scope) -> Element {} ``` That's a lot of components for one file! We've successfully refactored our app into components, but we should probably start breaking it up into a file for each component. @@ -61,7 +61,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] struct ActionCardProps {} -fn ActionCard(Scope) -> Element {} +fn ActionCard(Scope) -> Element {} ``` We should also create a `mod.rs` file in the `post` folder so we can use it from our `main.rs`. Our `Post` component and its props will go into this file. @@ -104,10 +104,10 @@ fn App(Scope) -> Element { original_poster: "me".to_string() } }) -} +} ``` -If you tried to build this app right now, you'll get an error message saying that `Post is private, trying changing it to public`. This is because we haven't properly exported our component! To fix this, we need to make sure both the Props and Component are declared as "public": +If you tried to build this app right now, you'll get an error message saying that `Post is private, try changing it to public`. This is because we haven't properly exported our component! To fix this, we need to make sure both the Props and Component are declared as "public": ```rust // src/post/mod.rs @@ -116,7 +116,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct PostProps {} -pub fn Post(Scope) -> Element {} +pub fn Post(Scope) -> Element {} ``` While we're here, we also need to make sure each of our subcomponents are included as modules and exported. @@ -203,7 +203,7 @@ fn App(Scope) -> Element { original_poster: "me".to_string() } }) -} +} ``` @@ -255,7 +255,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct VoteButtonsProps {} -pub fn VoteButtons(Scope) -> Element {} +pub fn VoteButtons(Scope) -> Element {} ``` ```rust @@ -264,7 +264,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct TitleCardProps {} -pub fn TitleCard(Scope) -> Element {} +pub fn TitleCard(Scope) -> Element {} ``` ```rust @@ -273,7 +273,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct MetaCardProps {} -pub fn MetaCard(Scope) -> Element {} +pub fn MetaCard(Scope) -> Element {} ``` ```rust @@ -282,7 +282,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct ActionCardProps {} -pub fn ActionCard(Scope) -> Element {} +pub fn ActionCard(Scope) -> Element {} ``` ## Moving forward diff --git a/docs/guide/src/elements/index.md b/docs/guide/src/elements/index.md index 13a61a868..aedc39e34 100644 --- a/docs/guide/src/elements/index.md +++ b/docs/guide/src/elements/index.md @@ -1,6 +1,6 @@ # Core Topics -In this chapter, we'll cover some core topics on how Dioxus works and how to best leverage the features to build a beautiful, reactive app. +In this chapter, we'll cover some core topics about how Dioxus works and how to best leverage the features to build a beautiful, reactive app. At a very high level, Dioxus is simply a Rust framework for _declaring_ user interfaces and _reacting_ to changes. diff --git a/docs/guide/src/elements/lists.md b/docs/guide/src/elements/lists.md index 32dbfd833..37e87b7da 100644 --- a/docs/guide/src/elements/lists.md +++ b/docs/guide/src/elements/lists.md @@ -1,6 +1,6 @@ # Conditional Lists and Keys -You will often want to display multiple similar components from a collection of data. +You will often want to display multiple similar components from a collection of data. In this chapter, you will learn: @@ -22,9 +22,9 @@ rsx!( ) ``` -Instead, we need to transform the list of data into a list of Elements. +Instead, we need to transform the list of data into a list of Elements. -For convenience, `rsx!` supports any type in curly braces that implements the `IntoVnodeList` trait. Conveniently, every iterator that returns something that can be rendered as an Element also implements `IntoVnodeList`. +For convenience, `rsx!` supports any type in curly braces that implements the `IntoVnodeList` trait. Conveniently, every iterator that returns something that can be rendered as an Element also implements `IntoVnodeList`. As a simple example, let's render a list of names. First, start with our input data: @@ -61,7 +61,7 @@ The HTML-rendered version of this list would follow what you would expect: ### Rendering our posts with a PostList component -Let's start by modeling this problem with a component and some properties. +Let's start by modeling this problem with a component and some properties. For this example, we're going to use the borrowed component syntax since we probably have a large list of posts that we don't want to clone every time we render the Post List. @@ -98,7 +98,7 @@ fn App(cx: Scope) -> Element { Rust's iterators are extremely powerful, especially when used for filtering tasks. When building user interfaces, you might want to display a list of items filtered by some arbitrary check. -As a very simple example, let's set up a filter where we only list names that begin with the letter "J". +As a very simple example, let's set up a filter where we only list names that begin with the letter "J". Let's make our list of names: @@ -117,7 +117,7 @@ let name_list = names Rust's iterators provide us tons of functionality and are significantly easier to work with than JavaScript's map/filter/reduce. -For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collected` our filtered list into new Vec, then we would need to make an allocation to store these new elements. Instead, we create an entirely new _lazy_ iterator which will then be consumed by Dioxus in the `render` call. +For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collected` our filtered list into new Vec, then we would need to make an allocation to store these new elements. Instead, we create an entirely new _lazy_ iterator which will then be consumed by Dioxus in the `render` call. The `render` method is extraordinarily efficient, so it's best practice to let it do most of the allocations for us. diff --git a/docs/guide/src/elements/vnodes.md b/docs/guide/src/elements/vnodes.md index b407ed02f..d441693d7 100644 --- a/docs/guide/src/elements/vnodes.md +++ b/docs/guide/src/elements/vnodes.md @@ -93,7 +93,7 @@ Alternatively, `&str` can be included directly, though it must be inside of an a rsx!( "Hello ", [if enabled { "Jack" } else { "Bob" }] ) ``` -This is different from React's way of generating arbitrary markup but fits within idiomatic Rust. +This is different from React's way of generating arbitrary markup but fits within idiomatic Rust. Typically, with Dioxus, you'll just want to compute your substrings outside of the `rsx!` call and leverage the f-string formatting: @@ -138,11 +138,11 @@ rsx!( All element attributes must occur *before* child elements. The `rsx!` macro will throw an error if your child elements come before any of your attributes. If you don't see the error, try editing your Rust-Analyzer IDE setting to ignore macro-errors. This is a temporary workaround because Rust-Analyzer currently throws *two* errors instead of just the one we care about. ```rust -// settings.json +// settings.json { "rust-analyzer.diagnostics.disabled": [ "macro-error" - ], + ], } ``` @@ -166,7 +166,7 @@ This chapter just scratches the surface on how Elements can be defined. We learned: - Elements are the basic building blocks of User Interfaces -- Elements can contain other elements +- Elements can contain other elements - Elements can either be a named container or text - Some Elements have properties that the renderer can use to draw the UI to the screen diff --git a/docs/guide/src/final.md b/docs/guide/src/final.md index 68706f1aa..41449edc7 100644 --- a/docs/guide/src/final.md +++ b/docs/guide/src/final.md @@ -15,7 +15,7 @@ With any luck, you followed through the "Putting it All Together" mini guide and Continuing on your journey with Dioxus, you can try a number of things: -- Build a simple TUI app +- Build a simple TUI app - Publish your search engine app - Deploy a WASM app to GitHub - Design a custom hook @@ -37,7 +37,7 @@ The core team is actively working on: - Declarative window management (via Tauri) for Desktop apps - Portals for Dioxus Core -- Mobile support +- Mobile support - Integration with 3D renderers - Better async story (suspense, error handling) - Global state management diff --git a/docs/guide/src/hello_world.md b/docs/guide/src/hello_world.md index e9eb92081..e1d4777d9 100644 --- a/docs/guide/src/hello_world.md +++ b/docs/guide/src/hello_world.md @@ -1,6 +1,6 @@ # "Hello, World" desktop app -Let's put together a simple "hello world" desktop application to get acquainted with Dioxus. +Let's put together a simple "hello world" desktop application to get acquainted with Dioxus. In this chapter, we'll cover: @@ -31,7 +31,7 @@ $ tree We are greeted with a pre-initialized git repository, our code folder (`src`) and our project file (`Cargo.toml`). -Our `src` folder holds our code. Our `main.rs` file holds our `fn main` which will be executed when our app is ran. +Our `src` folder holds our code. Our `main.rs` file holds our `fn main` which will be executed when our app is run. ```shell $ more src/main.rs @@ -128,13 +128,13 @@ Finally, our app. Every component in Dioxus is a function that takes in `Context fn App(cx: Scope) -> Element { cx.render(rsx! { div { "Hello, world!" } - }) + }) } ``` ### What is this `Scope` object? -Coming from React, the `Scope` object might be confusing. In React, you'll want to store data between renders with hooks. However, hooks rely on global variables which make them difficult to integrate in multi-tenant systems like server-rendering. +Coming from React, the `Scope` object might be confusing. In React, you'll want to store data between renders with hooks. However, hooks rely on global variables which make them difficult to integrate in multi-tenant systems like server-rendering. In Dioxus, you are given an explicit `Scope` object to control how the component renders and stores data. The `Scope` object provides a handful of useful APIs for features like suspense, rendering, and more. diff --git a/docs/guide/src/interactivity/hooks.md b/docs/guide/src/interactivity/hooks.md index 029e2db15..adf43c6ef 100644 --- a/docs/guide/src/interactivity/hooks.md +++ b/docs/guide/src/interactivity/hooks.md @@ -1,5 +1,5 @@ # Hooks and Internal State - + In the [Adding Interactivity](./interactivity.md) section, we briefly covered the concept of hooks and state stored internal to components. In this section, we'll dive a bit deeper into hooks, exploring both the theory and mechanics. @@ -10,7 +10,7 @@ In this section, we'll dive a bit deeper into hooks, exploring both the theory a Over the past several decades, computer scientists and engineers have long sought the "right way" of designing user interfaces. With each new programming language, novel features are unlocked that change the paradigm in which user interfaces are coded. -Generally, a number of patterns have emerged, each with their own strengths and tradeoffs. +Generally, a number of patterns have emerged, each with their own strengths and tradeoffs. Broadly, there are two types of GUI structures: @@ -21,7 +21,7 @@ Typically, immediate-mode GUIs are simpler to write but can slow down as more fe Many GUIs today are written in *Retained mode* - your code changes the data of the user interface but the renderer is responsible for actually drawing to the screen. In these cases, our GUI's state sticks around as the UI is rendered. To help accommodate retained mode GUIs, like the web browser, Dioxus provides a mechanism to keep state around. -> Note: Even though hooks are accessible, you should still prefer to one-way data flow and encapsulation. Your UI code should be as predictable as possible. Dioxus is plenty fast, even for the largest apps. +> Note: Even though hooks are accessible, you should still prefer one-way data flow and encapsulation. Your UI code should be as predictable as possible. Dioxus is plenty fast, even for the largest apps. ## Mechanics of Hooks In order to have state stick around between renders, Dioxus provides the `hook` through the `use_hook` API. This gives us a mutable reference to data returned from the initialization function. @@ -48,7 +48,7 @@ fn example(cx: Scope) -> Element { } ``` -Mechanically, each call to `use_hook` provides us with `&mut T` for a new value. +Mechanically, each call to `use_hook` provides us with `&mut T` for a new value. ```rust fn example(cx: Scope) -> Element { @@ -182,7 +182,7 @@ By default, we bundle a handful of hooks in the Dioxus-Hooks package. Feel free - [use_context](https://docs.rs/dioxus_hooks/use_context) - consume state provided by `use_provide_context` For a more in-depth guide to building new hooks, checkout out the advanced hook building guide in the reference. - + ## Wrapping up In this chapter, we learned about the mechanics and intricacies of storing state inside a component. diff --git a/docs/guide/src/interactivity/index.md b/docs/guide/src/interactivity/index.md index 9e23862f1..0e1a61252 100644 --- a/docs/guide/src/interactivity/index.md +++ b/docs/guide/src/interactivity/index.md @@ -10,7 +10,7 @@ Before we get too deep into the mechanics of interactivity, we should first unde Every app you'll ever build has some sort of information that needs to be rendered to the screen. Dioxus is responsible for translating your desired user interface to what is rendered to the screen. *You* are responsible for providing the content. -The dynamic data in your user interface is called `State`. +The dynamic data in your user interface is called `State`. When you first launch your app with `dioxus::web::launch_with_props` you'll be providing the initial state. You need to declare the initial state *before* starting the app. @@ -32,7 +32,7 @@ fn main() { } ``` -When Dioxus renders your app, it will pass an immutable reference of `PostProps` to your `Post` component. Here, you can pass the state down into children. +When Dioxus renders your app, it will pass an immutable reference to `PostProps` into your `Post` component. Here, you can pass the state down into children. ```rust fn App(cx: Scope) -> Element { @@ -73,14 +73,14 @@ fn App(cx: Scope)-> Element { url: String::from("dioxuslabs.com"), title: String::from("Hello, world"), original_poster: String::from("dioxus") - } + } }); cx.render(rsx!{ Title { title: &post.title } Score { score: &post.score } // etc - }) + }) } ``` @@ -102,7 +102,7 @@ We'll dive deeper into how exactly these hooks work later. ### When do I update my state? -There are a few different approaches to choosing when to update your state. You can update your state in response to user-triggered events or asynchronously in some background task. +There are a few different approaches to choosing when to update your state. You can update your state in response to user-triggered events or asynchronously in some background task. ### Updating state in listeners @@ -120,7 +120,7 @@ fn App(cx: Scope)-> Element { "Generate a random post" } Post { props: &post } - }) + }) } ``` @@ -162,7 +162,7 @@ Whenever you inform Dioxus that the component needs to be updated, it will "rend ![Diffing](../images/diffing.png) -In React, the specifics of when a component gets re-rendered is somewhat blurry. With Dioxus, any component can mark itself as "dirty" through a method on `Context`: `needs_update`. In addition, any component can mark any _other_ component as dirty provided it knows the other component's ID with `needs_update_any`. +In React, the specifics of when a component gets re-rendered is somewhat blurry. With Dioxus, any component can mark itself as "dirty" through a method on `Context`: `needs_update`. In addition, any component can mark any _other_ component as dirty provided it knows the other component's ID with `needs_update_any`. With these building blocks, we can craft new hooks similar to `use_state` that let us easily tell Dioxus that new information is ready to be sent to the screen. diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 223150521..de634dc0a 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -1,6 +1,6 @@ # Overview -In this chapter, we're going to get "set up" with a small desktop application. +In this chapter, we're going to get set up with a small desktop application. We'll learn about: - Installing the Rust programming language @@ -15,16 +15,15 @@ For platform-specific guides, check out the [Platform Specific Guides](../platfo Dioxus requires a few main things to get up and running: - The [Rust compiler](https://www.rust-lang.org) and associated build tooling - - An editor of your choice, ideally configured with the [Rust-Analyzer LSP plugin](https://rust-analyzer.github.io) Dioxus integrates very well with the Rust-Analyzer IDE plugin which will provide appropriate syntax highlighting, code navigation, folding, and more. ### Installing Rust -Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler. +Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler. -Once installed, make sure to install wasm32-unknown-unknown as a target if you're planning on deploying your app to the web. +Once installed, make sure to install wasm32-unknown-unknown as a target if you're planning on deploying your app to the web. ``` rustup target add wasm32-unknown-unknown @@ -32,7 +31,7 @@ rustup target add wasm32-unknown-unknown ### Platform-Specific Dependencies -If you are running a modern, mainstream operating system, you should need no additional setup to build WebView-based Desktop apps. However, if you are running an older version of Windows or a flavor of linux with no default web rendering engine, you might need to install some additional dependencies. +If you are running a modern, mainstream operating system, you should need no additional setup to build WebView-based Desktop apps. However, if you are running an older version of Windows or a flavor of Linux with no default web rendering engine, you might need to install some additional dependencies. For windows users: download the [bootstrapper for Webview2 from Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) @@ -49,13 +48,13 @@ When distributing onto older Windows platforms or less-mainstream ### Dioxus-CLI for dev server, bundling, etc. -We also recommend installing the Dioxus CLI. The Dioxus CLI automates building and packaging for various targets and integrates with simulators, development servers, and app deployment. To install the CLI, you'll need cargo (should be automatically installed with Rust): +We also recommend installing the Dioxus CLI. The Dioxus CLI automates building and packaging for various targets and integrates with simulators, development servers, and app deployment. To install the CLI, you'll need cargo (which should be automatically installed with Rust): ``` $ cargo install dioxus-cli ``` -You can update the dioxus-cli at any time with: +You can update dioxus-cli at any time with: ``` $ cargo install --force dioxus-cli diff --git a/docs/guide/src/state/index.md b/docs/guide/src/state/index.md index 9b3e054fc..1874dd73e 100644 --- a/docs/guide/src/state/index.md +++ b/docs/guide/src/state/index.md @@ -2,7 +2,7 @@ Every app you'll build with Dioxus will have some sort of state that needs to be maintained and updated as your users interact with it. However, managing state can be particular challenging at times, and is frequently the source of bugs in many GUI frameworks. -In this chapter, we'll cover the various ways to manage state, the appropriate terminology, various patterns, and then take an overview +In this chapter, we'll cover the various ways to manage state, the appropriate terminology, various patterns, and then take an overview -## Terminology +## Terminology diff --git a/examples/assets/logo.png b/examples/assets/logo.png new file mode 100644 index 000000000..7d08f2af8 Binary files /dev/null and b/examples/assets/logo.png differ diff --git a/examples/borderless.rs b/examples/borderless.rs new file mode 100644 index 000000000..d52e9fb5a --- /dev/null +++ b/examples/borderless.rs @@ -0,0 +1,38 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch_cfg(app, |cfg| { + cfg.with_window(|w| w.with_title("BorderLess Demo").with_decorations(false)) + }); +} + +fn app(cx: Scope) -> Element { + let window = dioxus::desktop::use_window(&cx); + + cx.render(rsx!( + link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } + header { + class: "text-gray-400 bg-gray-900 body-font", + onmousedown: move |_| window.drag(), + div { + class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", + a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", + span { class: "ml-3 text-xl", "Dioxus"} + } + 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(), + onclick: move |_| window.minimize(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(), + onclick: move |_| window.close(), + "Close" + } + } + } + )) +} diff --git a/examples/borrowed.rs b/examples/borrowed.rs index 63191bc97..815d6ceed 100644 --- a/examples/borrowed.rs +++ b/examples/borrowed.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + /* Dioxus manages borrow lifetimes for you. This means any child may borrow from its parent. However, it is not possible to hand out an &mut T to children - all props are consumed by &P, so you'd only get an &&mut T. diff --git a/examples/calculator.rs b/examples/calculator.rs index a849833a3..5e23a3846 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -18,17 +18,19 @@ fn main() { } fn app(cx: Scope) -> Element { - let display_value: UseState = use_state(&cx, || String::from("0")); + let (display_value, set_display_value) = use_state(&cx, || String::from("0")); let input_digit = move |num: u8| { - if display_value.get() == "0" { - display_value.set(String::new()); + if display_value == "0" { + set_display_value(String::new()); } - display_value.modify().push_str(num.to_string().as_str()); + set_display_value + .make_mut() + .push_str(num.to_string().as_str()); }; let input_operator = move |key: &str| { - display_value.modify().push_str(key); + set_display_value.make_mut().push_str(key); }; cx.render(rsx!( @@ -53,7 +55,7 @@ fn app(cx: Scope) -> Element { KeyCode::Num9 => input_digit(9), KeyCode::Backspace => { if !display_value.len() != 0 { - display_value.modify().pop(); + set_display_value.make_mut().pop(); } } _ => {} @@ -65,21 +67,21 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-clear", onclick: move |_| { - display_value.set(String::new()); - if display_value != "" { - display_value.set("0".into()); + set_display_value(String::new()); + if !display_value.is_empty(){ + set_display_value("0".into()); } }, - [if display_value == "" { "C" } else { "AC" }] + [if display_value.is_empty() { "C" } else { "AC" }] } button { class: "calculator-key key-sign", onclick: move |_| { - let temp = calc_val(display_value.get().clone()); + let temp = calc_val(display_value.clone()); if temp > 0.0 { - display_value.set(format!("-{}", temp)); + set_display_value(format!("-{}", temp)); } else { - display_value.set(format!("{}", temp.abs())); + set_display_value(format!("{}", temp.abs())); } }, "±" @@ -87,8 +89,8 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-percent", onclick: move |_| { - display_value.set( - format!("{}", calc_val(display_value.get().clone()) / 100.0) + set_display_value( + format!("{}", calc_val(display_value.clone()) / 100.0) ); }, "%" @@ -98,7 +100,7 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-0", onclick: move |_| input_digit(0), "0" } - button { class: "calculator-key key-dot", onclick: move |_| display_value.modify().push_str("."), + button { class: "calculator-key key-dot", onclick: move |_| set_display_value.make_mut().push('.'), "●" } (1..10).map(|k| rsx!{ @@ -130,7 +132,7 @@ fn app(cx: Scope) -> Element { } button { class: "calculator-key key-equals", onclick: move |_| { - display_value.set(format!("{}", calc_val(display_value.get().clone()))); + set_display_value(format!("{}", calc_val(display_value.clone()))); }, "=" } @@ -175,7 +177,7 @@ fn calc_val(val: String) -> f64 { for c in val[start_index..].chars() { if c == '+' || c == '-' || c == '*' || c == '/' { - if temp != "" { + if !temp.is_empty() { match &operation as &str { "+" => result += temp.parse::().unwrap(), "-" => result -= temp.parse::().unwrap(), @@ -191,7 +193,7 @@ fn calc_val(val: String) -> f64 { } } - if temp != "" { + if !temp.is_empty() { match &operation as &str { "+" => result += temp.parse::().unwrap(), "-" => result -= temp.parse::().unwrap(), diff --git a/examples/crm.rs b/examples/crm.rs index d4f3d1579..f6e7e05af 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -1,7 +1,7 @@ /* Tiny CRM: A port of the Yew CRM example to Dioxus. */ -use dioxus::{events::FormEvent, prelude::*}; +use dioxus::prelude::*; fn main() { dioxus::desktop::launch(app); @@ -20,11 +20,11 @@ pub struct Client { } fn app(cx: Scope) -> Element { - let scene = use_state(&cx, || Scene::ClientsList); 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 (scene, set_scene) = use_state(&cx, || Scene::ClientsList); + let (firstname, set_firstname) = use_state(&cx, String::new); + let (lastname, set_lastname) = use_state(&cx, String::new); + let (description, set_description) = use_state(&cx, String::new); cx.render(rsx!( body { @@ -38,7 +38,7 @@ fn app(cx: Scope) -> Element { h1 {"Dioxus CRM Example"} - match *scene { + match scene { Scene::ClientsList => rsx!( div { class: "crm", h2 { margin_bottom: "10px", "List of clients" } @@ -51,8 +51,8 @@ fn app(cx: Scope) -> Element { }) ) } - button { class: "pure-button pure-button-primary", onclick: move |_| scene.set(Scene::NewClientForm), "Add New" } - button { class: "pure-button", onclick: move |_| scene.set(Scene::Settings), "Settings" } + button { class: "pure-button pure-button-primary", onclick: move |_| set_scene(Scene::NewClientForm), "Add New" } + button { class: "pure-button", onclick: move |_| set_scene(Scene::Settings), "Settings" } } ), Scene::NewClientForm => rsx!( @@ -63,19 +63,19 @@ fn app(cx: Scope) -> Element { class: "new-client firstname", placeholder: "First name", value: "{firstname}", - oninput: move |e| firstname.set(e.value.clone()) + oninput: move |e| set_firstname(e.value.clone()) } input { class: "new-client lastname", placeholder: "Last name", value: "{lastname}", - oninput: move |e| lastname.set(e.value.clone()) + oninput: move |e| set_lastname(e.value.clone()) } textarea { class: "new-client description", placeholder: "Description", value: "{description}", - oninput: move |e| description.set(e.value.clone()) + oninput: move |e| set_description(e.value.clone()) } } button { @@ -86,13 +86,13 @@ fn app(cx: Scope) -> Element { first_name: (*firstname).clone(), last_name: (*lastname).clone(), }); - description.set(String::new()); - firstname.set(String::new()); - lastname.set(String::new()); + set_description(String::new()); + set_firstname(String::new()); + set_lastname(String::new()); }, "Add New" } - button { class: "pure-button", onclick: move |_| scene.set(Scene::ClientsList), + button { class: "pure-button", onclick: move |_| set_scene(Scene::ClientsList), "Go Back" } } @@ -108,7 +108,7 @@ fn app(cx: Scope) -> Element { } button { class: "pure-button pure-button-primary", - onclick: move |_| scene.set(Scene::ClientsList), + onclick: move |_| set_scene(Scene::ClientsList), "Go Back" } } diff --git a/examples/custom_assets.rs b/examples/custom_assets.rs new file mode 100644 index 000000000..e623d5b50 --- /dev/null +++ b/examples/custom_assets.rs @@ -0,0 +1,14 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + "This should show an image:" + img { src: "examples/assets/logo.png", } + } + }) +} diff --git a/examples/disabled.rs b/examples/disabled.rs index 390a5550f..949c25369 100644 --- a/examples/disabled.rs +++ b/examples/disabled.rs @@ -5,12 +5,12 @@ fn main() { } fn app(cx: Scope) -> Element { - let disabled = use_state(&cx, || false); + let (disabled, set_disabled) = use_state(&cx, || false); cx.render(rsx! { div { button { - onclick: move |_| disabled.set(!disabled.get()), + onclick: move |_| set_disabled(!disabled), "click to " [if *disabled {"enable"} else {"disable"} ] " the lower button" } @@ -18,11 +18,6 @@ fn app(cx: Scope) -> Element { disabled: "{disabled}", "lower button" } - - input { - value: "false", - } - } }) } diff --git a/examples/dog_app.rs b/examples/dog_app.rs index b2f8429ee..a30f3e5e8 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -1,5 +1,6 @@ +#![allow(non_snake_case)] + //! Render a bunch of doggos! -//! use std::collections::HashMap; @@ -15,7 +16,7 @@ struct ListBreeds { } fn app(cx: Scope) -> Element { - let fut = use_future(&cx, || async move { + let breeds = use_future(&cx, || async move { reqwest::get("https://dog.ceo/api/breeds/list/all") .await .unwrap() @@ -23,9 +24,9 @@ fn app(cx: Scope) -> Element { .await }); - let selected_breed = use_state(&cx, || None); + let (breed, set_breed) = use_state(&cx, || None); - match fut.value() { + match breeds.value() { Some(Ok(breeds)) => cx.render(rsx! { div { h1 {"Select a dog breed!"} @@ -35,14 +36,14 @@ fn app(cx: Scope) -> Element { breeds.message.keys().map(|breed| rsx!( li { button { - onclick: move |_| selected_breed.set(Some(breed.clone())), + onclick: move |_| set_breed(Some(breed.clone())), "{breed}" } } )) } div { flex: "50%", - match &*selected_breed { + match breed { Some(breed) => rsx!( Breed { breed: breed.clone() } ), None => rsx!("No Breed selected"), } @@ -50,7 +51,7 @@ fn app(cx: Scope) -> Element { } } }), - Some(Err(e)) => cx.render(rsx! { + Some(Err(_e)) => cx.render(rsx! { div { "Error fetching breeds" } }), None => cx.render(rsx! { @@ -72,9 +73,9 @@ fn Breed(cx: Scope, breed: String) -> Element { reqwest::get(endpoint).await.unwrap().json::().await }); - let breed_name = use_state(&cx, || breed.clone()); - if breed_name.get() != breed { - breed_name.set(breed.clone()); + let (name, set_name) = use_state(&cx, || breed.clone()); + if name != breed { + set_name(breed.clone()); fut.restart(); } diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index 5d4bf8be8..728812027 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -15,7 +15,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let files = use_ref(&cx, || Files::new()); + let files = use_ref(&cx, Files::new); rsx!(cx, div { link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", } @@ -28,8 +28,8 @@ fn app(cx: Scope) -> Element { } main { files.read().path_names.iter().enumerate().map(|(dir_id, path)| { - let path_end = path.split('/').last().unwrap_or(path.as_str()); - let icon_type = if path_end.contains(".") { + let path_end = path.split('/').last().unwrap_or_else(|| path.as_str()); + let icon_type = if path_end.contains('.') { "description" } else { "folder" @@ -56,7 +56,6 @@ fn app(cx: Scope) -> Element { ) }) } - }) } diff --git a/examples/filedragdrop.rs b/examples/filedragdrop.rs index a36b64ddb..e5644e2a0 100644 --- a/examples/filedragdrop.rs +++ b/examples/filedragdrop.rs @@ -2,9 +2,9 @@ use dioxus::prelude::*; fn main() { dioxus::desktop::launch_with_props(app, (), |c| { - c.with_file_drop_handler(|w, e| { + c.with_file_drop_handler(|_w, e| { println!("{:?}", e); - false + true }) }); } diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index 0a70bd25b..8dba1cb91 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + use dioxus::prelude::*; use rand::prelude::*; @@ -15,9 +17,9 @@ impl Label { fn new_list(num: usize) -> Vec { let mut rng = SmallRng::from_entropy(); let mut labels = Vec::with_capacity(num); - for _ in 0..num { + for x in 0..num { labels.push(Label { - key: 0, + key: x, labels: [ ADJECTIVES.choose(&mut rng).unwrap(), COLOURS.choose(&mut rng).unwrap(), @@ -30,8 +32,8 @@ impl Label { } fn app(cx: Scope) -> Element { - let items = use_ref(&cx, || vec![]); - let selected = use_state(&cx, || None); + let items = use_ref(&cx, Vec::new); + let (selected, set_selected) = use_state(&cx, || None); cx.render(rsx! { div { class: "container", @@ -69,7 +71,7 @@ fn app(cx: Scope) -> Element { rsx!(tr { class: "{is_in_danger}", td { class:"col-md-1" } td { class:"col-md-1", "{item.key}" } - td { class:"col-md-1", onclick: move |_| selected.set(Some(id)), + td { class:"col-md-1", onclick: move |_| set_selected(Some(id)), a { class: "lbl", item.labels } } td { class: "col-md-1", diff --git a/examples/hydration.rs b/examples/hydration.rs index 6ec777352..83b7633df 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -20,13 +20,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let val = use_state(&cx, || 0); + let (val, set_val) = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "hello world. Count: {val}" } button { - onclick: move |_| *val.modify() += 1, + onclick: move |_| set_val(val + 1), "click to increment" } } diff --git a/examples/inputs.rs b/examples/inputs.rs index 11334b59a..5e0a52244 100644 --- a/examples/inputs.rs +++ b/examples/inputs.rs @@ -2,8 +2,6 @@ //! //! There is some conversion happening when input types are checkbox/radio/select/textarea etc. -use std::sync::Arc; - use dioxus::{events::FormEvent, prelude::*}; fn main() { diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index c679d1427..a7b821750 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Example: Calculator //! ------------------- //! @@ -30,7 +32,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")] } @@ -171,8 +173,8 @@ impl Calculator { } } fn input_dot(&mut self) { - if self.display_value.find(".").is_none() { - self.display_value.push_str("."); + if !self.display_value.contains('.') { + self.display_value.push('.'); } } fn perform_operation(&mut self) { @@ -190,8 +192,8 @@ impl Calculator { } } fn toggle_sign(&mut self) { - if self.display_value.starts_with("-") { - self.display_value = self.display_value.trim_start_matches("-").to_string(); + if self.display_value.starts_with('-') { + self.display_value = self.display_value.trim_start_matches('-').to_string(); } else { self.display_value = format!("-{}", self.display_value); } diff --git a/examples/pattern_reducer.rs b/examples/pattern_reducer.rs index 82634d771..04c1811a1 100644 --- a/examples/pattern_reducer.rs +++ b/examples/pattern_reducer.rs @@ -15,16 +15,16 @@ fn main() { } fn app(cx: Scope) -> Element { - let state = use_state(&cx, PlayerState::new); + let (state, set_state) = use_state(&cx, PlayerState::new); cx.render(rsx!( div { h1 {"Select an option"} h3 { "The radio is... " [state.is_playing()] "!" } - button { onclick: move |_| state.modify().reduce(PlayerAction::Pause), + button { onclick: move |_| set_state.make_mut().reduce(PlayerAction::Pause), "Pause" } - button { onclick: move |_| state.modify().reduce(PlayerAction::Play), + button { onclick: move |_| set_state.make_mut().reduce(PlayerAction::Play), "Play" } } diff --git a/examples/readme.rs b/examples/readme.rs index a9ea60822..b37a8e07a 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -9,13 +9,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } } }) } diff --git a/examples/router.rs b/examples/router.rs index 121514423..216b69922 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -19,7 +19,7 @@ fn app(cx: Scope) -> Element { Route { to: "/", "Home" } Route { to: "users", Route { to: "/", "User list" } - Route { to: ":name", BlogPost {} } + Route { to: ":name", User {} } } Route { to: "blog" Route { to: "/", "Blog list" } @@ -48,12 +48,20 @@ struct Query { fn User(cx: Scope) -> Element { let post = dioxus::router::use_route(&cx).last_segment(); - let query = dioxus::router::use_route(&cx).query::(); + let query = dioxus::router::use_route(&cx) + .query::() + .unwrap_or(Query { bold: false }); cx.render(rsx! { div { h1 { "Reading blog post: {post}" } p { "example blog post" } + + if query.bold { + rsx!{ b { "bold" } } + } else { + rsx!{ i { "italic" } } + } } }) } diff --git a/examples/rsx_compile_fail.rs b/examples/rsx_compile_fail.rs index 20760ef5b..8f490bf26 100644 --- a/examples/rsx_compile_fail.rs +++ b/examples/rsx_compile_fail.rs @@ -12,7 +12,7 @@ fn main() { } fn example(cx: Scope) -> Element { - let items = use_state(&cx, || { + let (items, _set_items) = use_state(&cx, || { vec![Thing { a: "asd".to_string(), b: 10, diff --git a/examples/suspense.rs b/examples/suspense.rs index 881d6cae7..5a9bf4fd8 100644 --- a/examples/suspense.rs +++ b/examples/suspense.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Suspense in Dioxus //! //! Currently, `rsx!` does not accept futures as values. To achieve the functionality diff --git a/examples/tailwind.rs b/examples/tailwind.rs index cbefd248f..6611fc47b 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Example: Basic Tailwind usage //! //! This example shows how an app might be styled with TailwindCSS. diff --git a/examples/tasks.rs b/examples/tasks.rs index a0ab7675c..3c4d4cba3 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -10,16 +10,15 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); use_future(&cx, move || { - let count = UseState::for_async(&count); - // for_async![count]; + let set_count = set_count.to_owned(); async move { - // loop { - // tokio::time::sleep(Duration::from_millis(1000)).await; - // count += 1; - // } + loop { + tokio::time::sleep(Duration::from_millis(1000)).await; + set_count.modify(|f| f + 1); + } } }); @@ -27,7 +26,7 @@ fn app(cx: Scope) -> Element { div { h1 { "Current count: {count}" } button { - onclick: move |_| count.set(0), + onclick: move |_| set_count(0), "Reset the count" } } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 8adb01d34..e5f7d1dfa 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -19,15 +19,15 @@ 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 mut todo_id = use_state(&cx, || 0); + let (todos, set_todos) = use_state(&cx, im_rc::HashMap::::default); + let (filter, set_filter) = use_state(&cx, || FilterState::All); + let (draft, set_draft) = use_state(&cx, || "".to_string()); + let (todo_id, set_todo_id) = use_state(&cx, || 0); // Filter the todos based on the filter state let mut filtered_todos = todos .iter() - .filter(|(_, item)| match *filter { + .filter(|(_, item)| match filter { FilterState::All => true, FilterState::Active => !item.checked, FilterState::Completed => item.checked, @@ -54,27 +54,25 @@ 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| set_draft(evt.value.clone()), onkeydown: move |evt| { - if evt.key == "Enter" { - if !draft.is_empty() { - todos.modify().insert( - *todo_id, - TodoItem { - id: *todo_id, - checked: false, - contents: draft.get().clone(), - }, - ); - todo_id += 1; - draft.set("".to_string()); - } + if evt.key == "Enter" && !draft.is_empty() { + set_todos.make_mut().insert( + *todo_id, + TodoItem { + id: *todo_id, + checked: false, + contents: draft.clone(), + }, + ); + set_todo_id(todo_id + 1); + set_draft("".to_string()); } } } } ul { class: "todo-list", - filtered_todos.iter().map(|id| rsx!(todo_entry( key: "{id}", id: *id, todos: todos ))) + filtered_todos.iter().map(|id| rsx!(todo_entry( key: "{id}", id: *id, set_todos: set_todos ))) } (!todos.is_empty()).then(|| rsx!( footer { class: "footer", @@ -83,14 +81,14 @@ pub fn app(cx: Scope<()>) -> Element { span {"{item_text} left"} } ul { class: "filters", - li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }} - li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }} - li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }} + li { class: "All", a { onclick: move |_| set_filter(FilterState::All), "All" }} + li { class: "Active", a { onclick: move |_| set_filter(FilterState::Active), "Active" }} + li { class: "Completed", a { onclick: move |_| set_filter(FilterState::Completed), "Completed" }} } (show_clear_completed).then(|| rsx!( button { class: "clear-completed", - onclick: move |_| todos.modify().retain(|_, todo| todo.checked == false), + onclick: move |_| set_todos.make_mut().retain(|_, todo| !todo.checked), "Clear completed" } )) @@ -108,24 +106,26 @@ pub fn app(cx: Scope<()>) -> Element { #[derive(Props)] pub struct TodoEntryProps<'a> { - todos: UseState<'a, im_rc::HashMap>, + set_todos: &'a UseState>, id: u32, } pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { - let todo = &cx.props.todos[&cx.props.id]; - let is_editing = use_state(&cx, || false); + let (is_editing, set_is_editing) = use_state(&cx, || false); + + let todos = cx.props.set_todos.get(); + let todo = &todos[&cx.props.id]; let completed = if todo.checked { "completed" } else { "" }; - let editing = if *is_editing.get() { "editing" } else { "" }; + let editing = if *is_editing { "editing" } else { "" }; rsx!(cx, li { class: "{completed} {editing}", - onclick: move |_| is_editing.set(true), - onfocusout: move |_| is_editing.set(false), + onclick: move |_| set_is_editing(true), + onfocusout: move |_| set_is_editing(false), div { class: "view", input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{todo.checked}", onchange: move |evt| { - cx.props.todos.modify()[&cx.props.id].checked = evt.value.parse().unwrap(); + cx.props.set_todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap(); } } label { r#for: "cbg-{todo.id}", pointer_events: "none", "{todo.contents}" } @@ -134,11 +134,11 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { input { class: "edit", value: "{todo.contents}", - oninput: move |evt| cx.props.todos.modify()[&cx.props.id].contents = evt.value.clone(), + oninput: move |evt| cx.props.set_todos.make_mut()[&cx.props.id].contents = evt.value.clone(), autofocus: "true", onkeydown: move |evt| { match evt.key.as_str() { - "Enter" | "Escape" | "Tab" => is_editing.set(false), + "Enter" | "Escape" | "Tab" => set_is_editing(false), _ => {} } }, diff --git a/examples/xss_safety.rs b/examples/xss_safety.rs index 36fc81a95..2790a8a89 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, set_contents) = use_state(&cx, || { String::from("") }); @@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element { input { value: "{contents}", r#type: "text", - oninput: move |e| contents.set(e.value.clone()), + oninput: move |e| set_contents(e.value.clone()), } } }) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 614774595..28bd07ed0 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -35,7 +35,7 @@ - Awesome Page + Discord Link

@@ -113,7 +113,7 @@ cargo run --example EXAMPLE ## Dioxus 项目 -| 文件浏览器 (桌面应用) | WiFi 扫描器 (桌面应用) | Todo管理 (所有平台) | 商城系统 (SSR/liveview) | +| 文件浏览器 (桌面应用) | 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) | diff --git a/packages/core-macro/CHANGELOG.md b/packages/core-macro/CHANGELOG.md index 79cbe766f..cf1143e1e 100644 --- a/packages/core-macro/CHANGELOG.md +++ b/packages/core-macro/CHANGELOG.md @@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### New Features + + - flatten props attrs + - add "optional" flag for props + +### Bug Fixes + + - detection of f-string formatting in components + +### Commit Statistics + + + + - 9 commits contributed to the release over the course of 12 calendar days. + - 3 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #95 from DioxusLabs/jk/filedragindrop ([`ca0dd4a`](https://github.comgit//DioxusLabs/dioxus/commit/ca0dd4aa7192d483a195d420363f39d771f3e471)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - flatten props attrs ([`d237271`](https://github.comgit//DioxusLabs/dioxus/commit/d2372717bd01fcff50af0572360e3f763d4c869d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - detection of f-string formatting in components ([`90abd9c`](https://github.comgit//DioxusLabs/dioxus/commit/90abd9c9a08c165384fae9c1f7c3fd098d512c48)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - Merge pull request #107 from autarch/autarch/half-assed-router ([`8d3ac3f`](https://github.comgit//DioxusLabs/dioxus/commit/8d3ac3ff148aef9d10a393eda453a11c1e882f58)) + - Merge pull request #127 from DioxusLabs/jk/handler-tweak ([`5bce294`](https://github.comgit//DioxusLabs/dioxus/commit/5bce294a86941090660b1cfd87809a2b0b76d2ce)) + - add "optional" flag for props ([`47bc4e4`](https://github.comgit//DioxusLabs/dioxus/commit/47bc4e4a44a7d08d4f42102d13f0766d9d6bf358)) +
+ ## v0.1.6 (2022-01-08) ### Documentation @@ -64,7 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 138 commits contributed to the release over the course of 352 calendar days. + - 139 commits contributed to the release over the course of 352 calendar days. - 123 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -75,6 +112,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-core-macro v0.1.6 ([`db0a5bd`](https://github.comgit//DioxusLabs/dioxus/commit/db0a5bd6ec93803cddb3c6fda4172257b9c301c6)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index ee3dc2883..eb8210923 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core-macro" -version = "0.1.6" +version = "0.1.7" authors = ["Jonathan Kelley"] edition = "2021" description = "Core macro for Dioxus Virtual DOM" diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 4ad7543a2..7099311ce 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -81,7 +81,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// // Using an "ID" associated with your data is a good idea. /// data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" })) /// }} -/// +/// /// // Matching /// {match true { /// true => rsx!(h1 {"Top text"}), @@ -229,18 +229,18 @@ pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStrea /// #[inline_props] /// fn app(cx: Scope, bob: String) -> Element { /// cx.render(rsx!("hello, {bob}")) -/// } +/// } /// /// // is equivalent to /// /// #[derive(PartialEq, Props)] /// struct AppProps { /// bob: String, -/// } +/// } /// /// fn app(cx: Scope) -> Element { /// cx.render(rsx!("hello, {bob}")) -/// } +/// } /// ``` #[proc_macro_attribute] pub fn inline_props(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 5257022a4..4e9d449cf 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -135,11 +135,14 @@ impl ToTokens for Component { None => quote! { None }, }; + let fn_name = self.name.segments.last().unwrap().ident.to_string(); + tokens.append_all(quote! { __cx.component( #name, #builder, #key_token, + #fn_name ) }) } diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index a0aa9f28d..563106db3 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -5,6 +5,58 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Documentation + + - add comments for the Handler + +### New Features + + - add "spawn" method + - allow providing context to the root component + +### Bug Fixes + + - provide_root_context on root scopes + - proprogation of root context + - allow eventhandler to derive default + +### Commit Statistics + + + + - 18 commits contributed to the release over the course of 13 calendar days. + - 8 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a)) + - Merge branch 'master' of github.com:DioxusLabs/dioxus ([`8899701`](https://github.comgit//DioxusLabs/dioxus/commit/88997019c5c74658bfd76344d6bb6f3565943b41)) + - add miri stress test ([`e9792e9`](https://github.comgit//DioxusLabs/dioxus/commit/e9792e9b95521fcf0b0b4e240d7cf0397cd79b8f)) + - add "spawn" method ([`bad4b77`](https://github.comgit//DioxusLabs/dioxus/commit/bad4b773b7bbab6a38d4f48d88d8c5a8787927ac)) + - drop hooks before resetting bump arena ([`2e4f765`](https://github.comgit//DioxusLabs/dioxus/commit/2e4f7659327b69b74e20af59b6d5e65c699b6adb)) + - provide_root_context on root scopes ([`d9a07dd`](https://github.comgit//DioxusLabs/dioxus/commit/d9a07ddddb45ba00e35645ac0e0ad1a4d2dd996d)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #121 from DioxusLabs/jk/unify ([`b287a4c`](https://github.comgit//DioxusLabs/dioxus/commit/b287a4cab3eec9e6961a3f010846291f4f105747)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - proprogation of root context ([`c8d528b`](https://github.comgit//DioxusLabs/dioxus/commit/c8d528b3b18e320ca0cee1542ae5fc659c1cca81)) + - allow providing context to the root component ([`d2bd175`](https://github.comgit//DioxusLabs/dioxus/commit/d2bd1751436ef4bec554bb361ba87aea1357036a)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - Merge pull request #107 from autarch/autarch/half-assed-router ([`8d3ac3f`](https://github.comgit//DioxusLabs/dioxus/commit/8d3ac3ff148aef9d10a393eda453a11c1e882f58)) + - Merge pull request #138 from mrxiaozhuox/master ([`8c7473d`](https://github.comgit//DioxusLabs/dioxus/commit/8c7473d1943dd133f388ec36116c9d8295861b97)) + - Merge pull request #133 from mrxiaozhuox/master ([`887f69d`](https://github.comgit//DioxusLabs/dioxus/commit/887f69d5b47bdcde4fe0eab094c0cd0de23e4f3f)) + - Don't expect all components to have a scope in ScopeArena.ensure_drop_safety ([`9b282d8`](https://github.comgit//DioxusLabs/dioxus/commit/9b282d877ba0fe2463687cde7ea899b7f8510425)) + - add comments for the Handler ([`036a0ff`](https://github.comgit//DioxusLabs/dioxus/commit/036a0ff49a7dade0e04c9c07071a1ff49133ee24)) + - allow eventhandler to derive default ([`e47ead5`](https://github.comgit//DioxusLabs/dioxus/commit/e47ead5347ce778935f8f2127fcb14f520eda0ce)) +
+ ## v0.1.7 (2022-01-08) ### Documentation @@ -138,7 +190,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 437 commits contributed to the release over the course of 358 calendar days. + - 438 commits contributed to the release over the course of 358 calendar days. - 416 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -149,8 +201,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** - - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - Release dioxus-core v0.1.7 ([`16d73b2`](https://github.comgit//DioxusLabs/dioxus/commit/16d73b240fc446c6f6996c19ccf52bbcda2eca79)) - add title to doc comment ([`d11f322`](https://github.comgit//DioxusLabs/dioxus/commit/d11f322f554e7dbf43b988c9cfda56498cc49872)) + - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - better document the `EventHandler` type ([`be9f1a5`](https://github.comgit//DioxusLabs/dioxus/commit/be9f1a52ad2b04f101397ae34482ea7394df653b)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index f54c6d5d9..03b378e88 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core" -version = "0.1.7" +version = "0.1.9" authors = ["Jonathan Kelley"] edition = "2018" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" diff --git a/packages/core/README.md b/packages/core/README.md index 6313b1415..0cb185f45 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -94,5 +94,5 @@ Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinn 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 variants required to execute certain features. +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 f852fda3c..87c3cf4a6 100644 --- a/packages/core/architecture.md +++ b/packages/core/architecture.md @@ -5,7 +5,7 @@ This document is mostly a brain-dump on how things work. A lot of this informati Main topics covered here: - Fiber, Concurrency, and Cooperative Scheduling - Suspense -- Signals +- Signals - Patches - Diffing - Const/Static structures @@ -25,7 +25,7 @@ During diffing, the "caller" closure is updated if the props are not `static. Th Hooks are a form of state that's slightly more finicky than structs but more extensible overall. Hooks cannot be used in conditionals, but are portable enough to run on most targets. -The Dioxus hook model uses a Bump arena where user's data lives. +The Dioxus hook model uses a Bump arena where user's data lives. Initializing hooks: - The component is created @@ -38,7 +38,7 @@ Initializing hooks: Running hooks: - Each time use_hook is called, the internal hook state is fetched as &mut T -- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies +- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies - The hook counter is incremented @@ -62,7 +62,7 @@ The diffing engine in Dioxus expects the RealDom Dioxus uses patches - not imperative methods - to modify the real dom. This speeds up the diffing operation and makes diffing cancelable which is useful for cooperative scheduling. In general, the RealDom trait exists so renderers can share "Node pointers" across runtime boundaries. -There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself. +There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself. @@ -70,11 +70,11 @@ There are no contractual obligations between the VirtualDOM and RealDOM. When th When an EventTrigger enters the queue and "progress" is called (an async function), Dioxus will get to work running scopes and diffing nodes. Scopes are run and nodes are diffed together. Dioxus records which scopes get diffed to track the progress of its work. -While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending. +While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending. -Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom. +Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom. -All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks. +All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks. ```js // returns a "deadline" object function idle() { @@ -83,7 +83,7 @@ function idle() { ``` ## Suspense -In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier. +In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier. In Dioxus, we have similar philosophy, but the use and details of suspense is slightly different. For starters, we don't currently allow using futures in the element structure. Technically, we can allow futures - and we do with "Signals" - but the "suspense" feature itself is meant to be self-contained within a single component. This forces you to handle all the loading states within your component, instead of outside the component, keeping things a bit more containerized. diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 499d9543e..3f3629c10 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,33 +1,36 @@ -//! This module contains the stateful DiffState and all methods to diff VNodes, their properties, and their children. +#![warn(clippy::pedantic)] +#![allow(clippy::cast_possible_truncation)] + +//! This module contains the stateful [`DiffState`] and all methods to diff [`VNodes`], 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 RealDom to apply. +//! of mutations for the [`RealDom`] 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, NodeRefs, pausing, priority +//! Components, Fragments, Suspense, [`SubTree`] memoization, incremental diffing, cancellation, [`NodeRefs`], pausing, priority //! scheduling, and additional batching operations. //! //! ## Implementation Details: //! //! ### IDs for elements //! -------------------- -//! All nodes are addressed by their IDs. The RealDom provides an imperative interface for making changes to these nodes. +//! All nodes are addressed by their IDs. The [`RealDom`] provides an imperative interface for making changes to these nodes. //! 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 VirtualDOM replaces old nodes. +//! garbage collection as the [`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 +//! 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 NodeFactory - it is +//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the [`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. @@ -41,13 +44,13 @@ //! 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 "yield_now" we can get the pure "fetch-as-you-render" behavior of React Fiber. +//! Due to the frequent calls to [`yield_now`] 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 the "use_suspense" hook. In the interim, this is the quickest way to get Suspense working. +//! and contents, without the need for the [`use_suspense`] hook. In the interim, this is the quickest way to get Suspense working. //! //! ## Subtree Memoization //! ----------------------- @@ -88,254 +91,111 @@ //! More info on how to improve this diffing algorithm: //! - -use crate::innerlude::*; +use crate::innerlude::{ + AnyProps, ElementId, Mutations, ScopeArena, ScopeId, VComponent, VElement, VFragment, VNode, + VPlaceholder, VText, +}; use fxhash::{FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -use DomEdit::*; -/// Our DiffState is an iterative tree differ. -/// -/// It uses techniques of a stack machine to allow pausing and restarting of the diff algorithm. This -/// was originally implemented using recursive techniques, but Rust lacks the ability to call async functions recursively, -/// meaning we could not "pause" the original diffing algorithm. -/// -/// Instead, we use a traditional stack machine approach to diff and create new nodes. The diff algorithm periodically -/// calls "yield_now" which allows the machine to pause and return control to the caller. The caller can then wait for -/// the next period of idle time, preventing our diff algorithm from blocking the main thread. -/// -/// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's -/// stack machines all the way down! pub(crate) struct DiffState<'bump> { pub(crate) scopes: &'bump ScopeArena, pub(crate) mutations: Mutations<'bump>, - pub(crate) stack: DiffStack<'bump>, pub(crate) force_diff: bool, + pub(crate) element_stack: SmallVec<[ElementId; 10]>, + pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, } -impl<'bump> DiffState<'bump> { - pub(crate) fn new(scopes: &'bump ScopeArena) -> Self { +impl<'b> DiffState<'b> { + pub fn new(scopes: &'b ScopeArena) -> Self { Self { scopes, mutations: Mutations::new(), - stack: DiffStack::new(), force_diff: false, - } - } -} - -/// The stack instructions we use to diff and create new nodes. -#[derive(Debug)] -pub(crate) enum DiffInstruction<'a> { - Diff { - old: &'a VNode<'a>, - new: &'a VNode<'a>, - }, - - Create { - node: &'a VNode<'a>, - }, - - /// pushes the node elements onto the stack for use in mount - PrepareMove { - node: &'a VNode<'a>, - }, - - Mount { - and: MountType<'a>, - }, - - PopScope, - PopElement, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) enum MountType<'a> { - Absorb, - Append, - Replace { old: &'a VNode<'a> }, - InsertAfter { other_node: &'a VNode<'a> }, - InsertBefore { other_node: &'a VNode<'a> }, -} - -pub(crate) struct DiffStack<'bump> { - pub(crate) instructions: Vec>, - pub(crate) nodes_created_stack: SmallVec<[usize; 10]>, - pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, - pub(crate) element_stack: SmallVec<[ElementId; 10]>, -} - -impl<'bump> DiffStack<'bump> { - fn new() -> Self { - Self { - instructions: Vec::with_capacity(1000), - nodes_created_stack: smallvec![], - scope_stack: smallvec![], element_stack: smallvec![], + scope_stack: smallvec![], } } - fn pop(&mut self) -> Option> { - self.instructions.pop() + pub fn diff_scope(&mut self, scopeid: ScopeId) { + let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); + self.scope_stack.push(scopeid); + let scope = self.scopes.get_scope(scopeid).unwrap(); + self.element_stack.push(scope.container); + + self.diff_node(old, new); + + self.mutations.mark_dirty_scope(scopeid); } - fn pop_off_scope(&mut self) { - self.scope_stack.pop(); - } - - pub(crate) fn push(&mut self, instruction: DiffInstruction<'bump>) { - self.instructions.push(instruction) - } - - fn create_children(&mut self, children: &'bump [VNode<'bump>], and: MountType<'bump>) { - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::Mount { and }); - - for child in children.iter().rev() { - self.instructions - .push(DiffInstruction::Create { node: child }); - } - } - - // todo: subtrees - // fn push_subtree(&mut self) { - // self.nodes_created_stack.push(0); - // self.instructions.push(DiffInstruction::Mount { - // and: MountType::Append, - // }); - // } - - fn push_nodes_created(&mut self, count: usize) { - self.nodes_created_stack.push(count); - } - - pub(crate) fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) { - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::Mount { and }); - self.instructions.push(DiffInstruction::Create { node }); - } - - fn add_child_count(&mut self, count: usize) { - *self.nodes_created_stack.last_mut().unwrap() += count; - } - - fn pop_nodes_created(&mut self) -> usize { - self.nodes_created_stack.pop().unwrap() - } - - fn current_scope(&self) -> Option { - self.scope_stack.last().copied() - } - - fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) { - // Push the new scope onto the stack - self.scope_stack.push(idx); - - self.instructions.push(DiffInstruction::PopScope); - - // Run the creation algorithm with this scope on the stack - // ?? I think we treat components as fragments?? - self.instructions.push(DiffInstruction::Create { node }); - } -} - -impl<'bump> DiffState<'bump> { - /// Progress the diffing for this "fiber" - /// - /// This method implements a depth-first iterative tree traversal. - /// - /// We do depth-first to maintain high cache locality (nodes were originally generated recursively). - /// - /// Returns a `bool` indicating that the work completed properly. - pub fn work(&mut self, mut deadline_expired: impl FnMut() -> bool) -> bool { - while let Some(instruction) = self.stack.pop() { - match instruction { - DiffInstruction::Diff { old, new } => self.diff_node(old, new), - DiffInstruction::Create { node } => self.create_node(node), - DiffInstruction::Mount { and } => self.mount(and), - DiffInstruction::PrepareMove { node } => { - let num_on_stack = self.push_all_nodes(node); - self.stack.add_child_count(num_on_stack); + 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) { + // Check the most common cases first + // these are *actual* elements, not wrappers around lists + (Text(old), Text(new)) => { + if std::ptr::eq(old, new) { + return; } - DiffInstruction::PopScope => self.stack.pop_off_scope(), - DiffInstruction::PopElement => { - self.stack.element_stack.pop(); + + let root = old + .id + .get() + .expect("existing text nodes should have an ElementId"); + + if old.text != new.text { + self.mutations.set_text(new.text, root.as_u64()); } - }; + self.scopes.update_node(new_node, root); - if deadline_expired() { - log::trace!("Deadline expired before we could finish!"); - return false; - } - } - - true - } - - // recursively push all the nodes of a tree onto the stack and return how many are there - fn push_all_nodes(&mut self, node: &'bump VNode<'bump>) -> usize { - match node { - VNode::Text(_) | VNode::Placeholder(_) => { - self.mutations.push_root(node.mounted_id()); - 1 + new.id.set(Some(root)); } - VNode::Fragment(_) | VNode::Component(_) => { - // - let mut added = 0; - for child in node.children() { - added += self.push_all_nodes(child); + (Placeholder(old), Placeholder(new)) => { + if std::ptr::eq(old, new) { + return; } - added + + let root = old + .id + .get() + .expect("existing placeholder nodes should have an ElementId"); + + self.scopes.update_node(new_node, root); + new.id.set(Some(root)); } - VNode::Element(el) => { - let mut num_on_stack = 0; - for child in el.children.iter() { - num_on_stack += self.push_all_nodes(child); + (Element(old), Element(new)) => { + if std::ptr::eq(old, new) { + return; } - self.mutations.push_root(el.id.get().unwrap()); - - num_on_stack + 1 + self.diff_element_nodes(old, new, old_node, new_node); } + + // These two sets are pointers to nodes but are not actually nodes themselves + (Component(old), Component(new)) => { + if std::ptr::eq(old, new) { + return; + } + self.diff_component_nodes(old_node, new_node, *old, *new); + } + + (Fragment(old), Fragment(new)) => { + if std::ptr::eq(old, new) { + return; + } + self.diff_fragment_nodes(old, new); + } + + // Anything else is just a basic replace and create + ( + Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), + Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), + ) => self.replace_node(old_node, new_node), } } - fn mount(&mut self, and: MountType<'bump>) { - let nodes_created = self.stack.pop_nodes_created(); - match and { - // add the nodes from this virtual list to the parent - // used by fragments and components - MountType::Absorb => { - self.stack.add_child_count(nodes_created); - } - - MountType::Replace { old } => { - self.replace_node(old, nodes_created); - } - - MountType::Append => { - self.mutations.edits.push(AppendChildren { - many: nodes_created as u32, - }); - } - - MountType::InsertAfter { other_node } => { - let root = self.find_last_element(other_node).unwrap(); - self.mutations.insert_after(root, nodes_created as u32); - } - - MountType::InsertBefore { other_node } => { - let root = self.find_first_element_id(other_node).unwrap(); - self.mutations.insert_before(root, nodes_created as u32); - } - } - } - - // ================================= - // Tools for creating new nodes - // ================================= - - fn create_node(&mut self, node: &'bump VNode<'bump>) { + 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), @@ -345,24 +205,21 @@ impl<'bump> DiffState<'bump> { } } - fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) { + fn create_text_node(&mut self, text: &'b VText<'b>, node: &'b VNode<'b>) -> usize { let real_id = self.scopes.reserve_node(node); - - self.mutations.create_text_node(vtext.text, real_id); - vtext.id.set(Some(real_id)); - self.stack.add_child_count(1); + text.id.set(Some(real_id)); + self.mutations.create_text_node(text.text, real_id); + 1 } - fn create_anchor_node(&mut self, anchor: &'bump VPlaceholder, node: &'bump VNode<'bump>) { + fn create_anchor_node(&mut self, anchor: &'b VPlaceholder, node: &'b VNode<'b>) -> usize { let real_id = self.scopes.reserve_node(node); - - self.mutations.create_placeholder(real_id); anchor.id.set(Some(real_id)); - - self.stack.add_child_count(1); + self.mutations.create_placeholder(real_id); + 1 } - fn create_element_node(&mut self, element: &'bump VElement<'bump>, node: &'bump VNode<'bump>) { + fn create_element_node(&mut self, element: &'b VElement<'b>, node: &'b VNode<'b>) -> usize { let VElement { tag: tag_name, listeners, @@ -372,174 +229,120 @@ impl<'bump> DiffState<'bump> { id: dom_id, parent: parent_id, .. - } = element; + } = &element; - // set the parent ID for event bubbling - self.stack.instructions.push(DiffInstruction::PopElement); + parent_id.set(self.element_stack.last().copied()); - let parent = self.stack.element_stack.last().unwrap(); - parent_id.set(Some(*parent)); - - // set the id of the element let real_id = self.scopes.reserve_node(node); - self.stack.element_stack.push(real_id); + dom_id.set(Some(real_id)); - self.mutations.create_element(tag_name, *namespace, real_id); + self.element_stack.push(real_id); + { + self.mutations.create_element(tag_name, *namespace, real_id); - self.stack.add_child_count(1); + let cur_scope_id = self + .current_scope() + .expect("diffing should always have a scope"); - if let Some(cur_scope_id) = self.stack.current_scope() { - for listener in *listeners { + for listener in listeners.iter() { listener.mounted_node.set(Some(real_id)); self.mutations.new_event_listener(listener, cur_scope_id); } - } else { - log::warn!("create element called with no scope on the stack - this is an error for a live dom"); - } - for attr in *attributes { - self.mutations.set_attribute(attr, real_id.as_u64()); - } + for attr in attributes.iter() { + self.mutations.set_attribute(attr, real_id.as_u64()); + } - // todo: the settext optimization - // - // if children.len() == 1 { - // if let VNode::Text(vtext) = children[0] { - // self.mutations.set_text(vtext.text, real_id.as_u64()); - // return; - // } - // } - - if !children.is_empty() { - self.stack.create_children(children, MountType::Append); + if !children.is_empty() { + self.create_and_append_children(children); + } } + self.element_stack.pop(); + + 1 } - fn create_fragment_node(&mut self, frag: &'bump VFragment<'bump>) { - self.stack.create_children(frag.children, MountType::Absorb); + fn create_fragment_node(&mut self, frag: &'b VFragment<'b>) -> usize { + self.create_children(frag.children) } - fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { - let parent_idx = self.stack.current_scope().unwrap(); + fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize { + let parent_idx = self.current_scope().unwrap(); - // 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) }; - let new_idx = self.scopes.new_with_key( - vcomponent.user_fc, - props, - Some(parent_idx), - self.stack.element_stack.last().copied().unwrap(), - 0, - ); + // ensure this scope doesn't already exist if we're trying to create it + debug_assert!( + vcomponent + .scope + .get() + .and_then(|f| self.scopes.get_scope(f)) + .is_none(), + "component scope already exists" + ); - new_idx - }; + // 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) }; + let new_idx = self.scopes.new_with_key( + vcomponent.user_fc, + props, + Some(parent_idx), + self.element_stack.last().copied().unwrap(), + 0, + ); // Actually initialize the caller's slot with the right address vcomponent.scope.set(Some(new_idx)); - match vcomponent.can_memoize { - true => { - // todo: implement promotion logic. save us from boxing props that we don't need - } - false => { - // track this component internally so we know the right drop order - } - } + log::trace!( + "created component \"{}\", id: {:?} parent {:?} orig: {:?}", + vcomponent.fn_name, + new_idx, + parent_idx, + vcomponent.originator + ); - // Run the scope for one iteration to initialize it - self.scopes.run_scope(new_idx); + // 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 + // } - // Take the node that was just generated from running the component - let nextnode = self.scopes.fin_head(new_idx); - self.stack.create_component(new_idx, nextnode); + self.enter_scope(new_idx); - // Finally, insert this scope as a seen node. - self.mutations.dirty_scopes.insert(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); - // ================================= - // Tools for diffing nodes - // ================================= + // Take the node that was just generated from running the component + let nextnode = self.scopes.fin_head(new_idx); + self.create_node(nextnode) + }; - pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { - use VNode::*; - match (old_node, new_node) { - // Check the most common cases first - // these are *actual* elements, not wrappers around lists - (Text(old), Text(new)) => { - if let Some(root) = old.id.get() { - if old.text != new.text { - self.mutations.set_text(new.text, root.as_u64()); - } - self.scopes.update_node(new_node, root); + self.leave_scope(); - new.id.set(Some(root)); - } - } - - (Placeholder(old), Placeholder(new)) => { - if let Some(root) = old.id.get() { - self.scopes.update_node(new_node, root); - new.id.set(Some(root)) - } - } - - (Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node), - - // These two sets are pointers to nodes but are not actually nodes themselves - (Component(old), Component(new)) => { - self.diff_component_nodes(old_node, new_node, *old, *new) - } - - (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), - - // The normal pathway still works, but generates slightly weird instructions - // This pathway ensures uses the ReplaceAll, not the InsertAfter and remove - (Placeholder(_), Fragment(new)) => { - self.stack - .create_children(new.children, MountType::Replace { old: old_node }); - } - - // Anything else is just a basic replace and create - ( - Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), - Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), - ) => self - .stack - .create_node(new_node, MountType::Replace { old: old_node }), - } + created } fn diff_element_nodes( &mut self, - old: &'bump VElement<'bump>, - new: &'bump VElement<'bump>, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, + old: &'b VElement<'b>, + new: &'b VElement<'b>, + old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, ) { - let root = old.id.get().unwrap(); + let root = old + .id + .get() + .expect("existing element nodes should have an ElementId"); // 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 { - // maybe make this an instruction? - // issue is that we need the "vnode" but this method only has the velement - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::Replace { old: old_node }, - }); - self.create_element_node(new, new_node); + self.replace_node(old_node, new_node); return; } @@ -570,7 +373,7 @@ impl<'bump> DiffState<'bump> { self.mutations.remove_attribute(attribute, root.as_u64()); } for attribute in new.attributes { - self.mutations.set_attribute(attribute, root.as_u64()) + self.mutations.set_attribute(attribute, root.as_u64()); } } @@ -582,7 +385,7 @@ impl<'bump> DiffState<'bump> { // 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 - if let Some(cur_scope_id) = self.stack.current_scope() { + if let Some(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()) { if old_l.event != new_l.event { @@ -604,159 +407,97 @@ impl<'bump> DiffState<'bump> { } } - if old.children.is_empty() && !new.children.is_empty() { - self.mutations.edits.push(PushRoot { - root: root.as_u64(), - }); - self.stack.element_stack.push(root); - self.stack.instructions.push(DiffInstruction::PopElement); - self.stack.create_children(new.children, MountType::Append); - } else { - self.stack.element_stack.push(root); - self.stack.instructions.push(DiffInstruction::PopElement); - self.diff_children(old.children, new.children); - } - - // todo: this is for the "settext" optimization - // it works, but i'm not sure if it's the direction we want to take right away - // I haven't benchmarked the performance imporvemenet yet. Perhaps - // we can make it a config? - - // match (old.children.len(), new.children.len()) { - // (0, 0) => {} - // (1, 1) => { - // let old1 = &old.children[0]; - // let new1 = &new.children[0]; - - // match (old1, new1) { - // (VNode::Text(old_text), VNode::Text(new_text)) => { - // if old_text.text != new_text.text { - // self.mutations.set_text(new_text.text, root.as_u64()); - // } - // } - // (VNode::Text(_old_text), _) => { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.stack.create_node(new1, MountType::Append); - // } - // (_, VNode::Text(new_text)) => { - // self.remove_nodes([old1], false); - // self.mutations.set_text(new_text.text, root.as_u64()); - // } - // _ => { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.diff_children(old.children, new.children); - // } - // } - // } - // (0, 1) => { - // if let VNode::Text(text) = &new.children[0] { - // self.mutations.set_text(text.text, root.as_u64()); - // } else { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // } - // } - // (0, _) => { - // self.mutations.edits.push(PushRoot { - // root: root.as_u64(), - // }); - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.stack.create_children(new.children, MountType::Append); - // } - // (_, 0) => { - // self.remove_nodes(old.children, false); - // self.mutations.set_text("", root.as_u64()); - // } - // (_, _) => { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.diff_children(old.children, new.children); - // } - // } + match (old.children.len(), new.children.len()) { + (0, 0) => {} + (0, _) => { + let created = self.create_children(new.children); + self.mutations.append_children(created as u32); + } + (_, _) => self.diff_children(old.children, new.children), + }; } fn diff_component_nodes( &mut self, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, - old: &'bump VComponent<'bump>, - new: &'bump VComponent<'bump>, + old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + old: &'b VComponent<'b>, + new: &'b VComponent<'b>, ) { - let scope_addr = old.scope.get().unwrap(); - log::trace!("diff_component_nodes: {:?}", scope_addr); + let scope_addr = old + .scope + .get() + .expect("existing component nodes should have a scope"); if std::ptr::eq(old, new) { - log::trace!("skipping component diff - component is the sames"); return; } // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { - self.stack.scope_stack.push(scope_addr); + 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 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)); - // 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().unwrap(); - - let should_run = { - if old.can_memoize { - let props_are_the_same = unsafe { - scope - .props - .borrow() - .as_ref() - .unwrap() - .memoize(new_props.as_ref()) - }; - !props_are_the_same || self.force_diff - } else { - true - } - }; - - if should_run { - let _old_props = scope + // take the new props out regardless + // when memoizing, push to the existing scope if memoization happens + let new_props = new .props - .replace(unsafe { std::mem::transmute(Some(new_props)) }); + .borrow_mut() + .take() + .expect("new component props should exist"); - // this should auto drop the previous props - self.scopes.run_scope(scope_addr); - self.mutations.dirty_scopes.insert(scope_addr); + 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 + } + }; - self.diff_node( - self.scopes.wip_head(scope_addr), - self.scopes.fin_head(scope_addr), - ); - } else { - log::trace!("memoized"); - // memoization has taken place - drop(new_props); - }; + if should_diff { + let _old_props = scope + .props + .replace(unsafe { std::mem::transmute(Some(new_props)) }); - self.stack.scope_stack.pop(); + // 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.stack - .create_node(new_node, MountType::Replace { old: old_node }); + self.replace_node(old_node, new_node); } } - fn diff_fragment_nodes(&mut self, old: &'bump VFragment<'bump>, new: &'bump VFragment<'bump>) { + fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) { // 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 { - self.diff_node(&old.children[0], &new.children[0]); + if !std::ptr::eq(old, new) { + self.diff_node(&old.children[0], &new.children[0]); + } return; } @@ -766,10 +507,6 @@ impl<'bump> DiffState<'bump> { self.diff_children(old.children, new.children); } - // ============================================= - // Utilities for creating new diff instructions - // ============================================= - // Diff the given set of old and new children. // // The parent must be on top of the change list stack when this function is @@ -785,11 +522,15 @@ impl<'bump> DiffState<'bump> { // // 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: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + 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.stack.create_children(new, MountType::Append), + ([], _) => self.create_and_append_children(new), (_, []) => self.remove_nodes(old, true), _ => { let new_is_keyed = new[0].key().is_some(); @@ -821,29 +562,21 @@ impl<'bump> DiffState<'bump> { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + use std::cmp::Ordering; + // Handled these cases in `diff_children` before calling this function. debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); - for (new, old) in new.iter().zip(old.iter()).rev() { - self.stack.push(DiffInstruction::Diff { new, old }); - } - - use std::cmp::Ordering; match old.len().cmp(&new.len()) { Ordering::Greater => self.remove_nodes(&old[new.len()..], true), - Ordering::Less => { - self.stack.create_children( - &new[old.len()..], - MountType::InsertAfter { - other_node: old.last().unwrap(), - }, - ); - } - Ordering::Equal => { - // nothing - they're the same size - } + Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), + Ordering::Equal => {} + } + + for (new, old) in new.iter().zip(old.iter()) { + self.diff_node(old, new); } } @@ -863,10 +596,10 @@ impl<'bump> DiffState<'bump> { // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 // // The stack is empty upon entry. - fn diff_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + 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 assert_unique_keys = |children: &'bump [VNode<'bump>]| { + let mut assert_unique_keys = |children: &'b [VNode<'b>]| { keys.clear(); for child in children { let key = child.key(); @@ -907,6 +640,7 @@ impl<'bump> DiffState<'bump> { !((old_middle.len() == new_middle.len()) && old_middle.is_empty()), "keyed children must have the same number of children" ); + if new_middle.is_empty() { // remove the old elements self.remove_nodes(old_middle, true); @@ -916,30 +650,15 @@ impl<'bump> DiffState<'bump> { if left_offset == 0 { // insert at the beginning of the old list let foothold = &old[old.len() - right_offset]; - self.stack.create_children( - new_middle, - MountType::InsertBefore { - other_node: foothold, - }, - ); + self.create_and_insert_before(new_middle, foothold); } else if right_offset == 0 { // insert at the end the old list let foothold = old.last().unwrap(); - self.stack.create_children( - new_middle, - MountType::InsertAfter { - other_node: foothold, - }, - ); + self.create_and_insert_after(new_middle, foothold); } else { // inserting in the middle let foothold = &old[left_offset - 1]; - self.stack.create_children( - new_middle, - MountType::InsertAfter { - other_node: foothold, - }, - ); + self.create_and_insert_after(new_middle, foothold); } } else { self.diff_keyed_middle(old_middle, new_middle); @@ -953,9 +672,8 @@ impl<'bump> DiffState<'bump> { /// If there is no offset, then this function returns None and the diffing is complete. fn diff_keyed_ends( &mut self, - - old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], ) -> Option<(usize, usize)> { let mut left_offset = 0; @@ -964,19 +682,14 @@ impl<'bump> DiffState<'bump> { if old.key() != new.key() { break; } - self.stack.push(DiffInstruction::Diff { old, new }); + self.diff_node(old, new); left_offset += 1; } // If that was all of the old children, then create and append the remaining // new children and we're finished. if left_offset == old.len() { - self.stack.create_children( - &new[left_offset..], - MountType::InsertAfter { - other_node: old.last().unwrap(), - }, - ); + self.create_and_insert_after(&new[left_offset..], old.last().unwrap()); return None; } @@ -1014,7 +727,8 @@ impl<'bump> DiffState<'bump> { // This function will load the appropriate nodes onto the stack and do diffing in place. // // Upon exit from this function, it will be restored to that same self. - fn diff_keyed_middle(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + #[allow(clippy::too_many_lines)] + fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { /* 1. Map the old keys into a numerical ordering based on indices. 2. Create a map of old key to its index @@ -1039,8 +753,8 @@ impl<'bump> DiffState<'bump> { // 0. Debug sanity checks // Should have already diffed the shared-key prefixes and suffixes. - debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); - debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); + 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)); // 1. Map the old keys into a numerical ordering based on indices. // 2. Create a map of old key to its index @@ -1072,10 +786,12 @@ impl<'bump> DiffState<'bump> { if shared_keys.is_empty() { if let Some(first_old) = old.get(0) { self.remove_nodes(&old[1..], true); - self.stack - .create_children(new, MountType::Replace { old: first_old }) + let nodes_created = self.create_children(new); + self.replace_inner(first_old, nodes_created); } else { - self.stack.create_children(new, MountType::Append {}); + // 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); } return; } @@ -1103,33 +819,31 @@ impl<'bump> DiffState<'bump> { lis_sequence.pop(); } - let apply = |new_idx, new_node: &'bump VNode<'bump>, stack: &mut DiffStack<'bump>| { - let old_index = new_index_to_old_index[new_idx]; - if old_index == u32::MAX as usize { - stack.create_node(new_node, MountType::Absorb); - } else { - // this function should never take LIS indices - stack.push(DiffInstruction::PrepareMove { node: new_node }); - stack.push(DiffInstruction::Diff { - new: new_node, - old: &old[old_index], - }); - } - }; + for idx in &lis_sequence { + self.diff_node(&old[new_index_to_old_index[*idx]], &new[*idx]); + } - // add mount instruction for the last items not covered by the lis - let first_lis = *lis_sequence.first().unwrap(); - if first_lis > 0 { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertBefore { - other_node: &new[first_lis], - }, - }); + let mut nodes_created = 0; - for (idx, new_node) in new[..first_lis].iter().enumerate().rev() { - apply(idx, new_node, &mut self.stack); + // add mount instruction for the first items not covered by the lis + let last = *lis_sequence.last().unwrap(); + if last < (new.len() - 1) { + for (idx, new_node) in new[(last + 1)..].iter().enumerate() { + 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); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } } + + self.mutations.insert_after( + self.find_last_element(&new[last]).unwrap(), + nodes_created as u32, + ); + nodes_created = 0; } // for each spacing, generate a mount instruction @@ -1137,85 +851,53 @@ impl<'bump> DiffState<'bump> { let mut last = *lis_iter.next().unwrap(); for next in lis_iter { if last - next > 1 { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertBefore { - other_node: &new[last], - }, - }); - for (idx, new_node) in new[(next + 1)..last].iter().enumerate().rev() { - apply(idx + next + 1, new_node, &mut self.stack); + for (idx, new_node) in new[(next + 1)..last].iter().enumerate() { + 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); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } } + + self.mutations.insert_before( + self.find_first_element(&new[last]).unwrap(), + nodes_created as u32, + ); + + nodes_created = 0; } last = *next; } - // add mount instruction for the first items not covered by the lis - let last = *lis_sequence.last().unwrap(); - if last < (new.len() - 1) { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertAfter { - other_node: &new[last], - }, - }); - for (idx, new_node) in new[(last + 1)..].iter().enumerate().rev() { - apply(idx + last + 1, new_node, &mut self.stack); + // add mount instruction for the last items not covered by the lis + let first_lis = *lis_sequence.first().unwrap(); + if first_lis > 0 { + 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); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } } - } - for idx in lis_sequence.iter().rev() { - self.stack.push(DiffInstruction::Diff { - new: &new[*idx], - old: &old[new_index_to_old_index[*idx]], - }); + self.mutations.insert_before( + self.find_first_element(&new[first_lis]).unwrap(), + nodes_created as u32, + ); } } - // ===================== - // Utilities - // ===================== - - fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> 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 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); } - fn find_first_element_id(&mut self, vnode: &'bump VNode<'bump>) -> Option { - let mut search_node = Some(vnode); - - loop { - match &search_node.take().unwrap() { - // the ones that have a direct id - VNode::Fragment(frag) => { - search_node = Some(&frag.children[0]); - } - VNode::Component(el) => { - let scope_id = el.scope.get().unwrap(); - search_node = Some(self.scopes.root_node(scope_id)); - } - VNode::Text(t) => break t.id.get(), - VNode::Element(t) => break t.id.get(), - VNode::Placeholder(t) => break t.id.get(), - } - } - } - - fn replace_node(&mut self, old: &'bump VNode<'bump>, nodes_created: usize) { + fn replace_inner(&mut self, old: &'b VNode<'b>, nodes_created: usize) { match old { VNode::Element(el) => { let id = old @@ -1224,6 +906,7 @@ impl<'bump> DiffState<'bump> { self.mutations.replace_with(id, nodes_created as u32); self.remove_nodes(el.children, false); + self.scopes.collect_garbage(id); } VNode::Text(_) | VNode::Placeholder(_) => { @@ -1232,35 +915,38 @@ impl<'bump> DiffState<'bump> { .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_node(&f.children[0], nodes_created); + self.replace_inner(&f.children[0], nodes_created); self.remove_nodes(f.children.iter().skip(1), true); } VNode::Component(c) => { - let node = self.scopes.fin_head(c.scope.get().unwrap()); - self.replace_node(node, nodes_created); - + log::trace!("Replacing component {:?}", old); let scope_id = c.scope.get().unwrap(); + let node = self.scopes.fin_head(scope_id); - // we can only remove components if they are actively being diffed - if self.stack.scope_stack.contains(&c.originator) { - self.scopes.try_remove(scope_id).unwrap(); + self.enter_scope(scope_id); + { + self.replace_inner(node, nodes_created); + + log::trace!("Replacing component x2 {:?}", old); + + // we can only remove components if they are actively being diffed + if self.scope_stack.contains(&c.originator) { + log::trace!("Removing component {:?}", old); + + self.scopes.try_remove(scope_id).unwrap(); + } } + self.leave_scope(); } } } - /// schedules nodes for garbage collection and pushes "remove" to the mutation stack - /// remove can happen whenever - pub(crate) fn remove_nodes( - &mut self, - nodes: impl IntoIterator>, - gen_muts: bool, - ) { - // or cache the vec on the diff machine + pub fn remove_nodes(&mut self, nodes: impl IntoIterator>, gen_muts: bool) { for node in nodes { match node { VNode::Text(t) => { @@ -1288,6 +974,8 @@ impl<'bump> DiffState<'bump> { self.mutations.remove(id.as_u64()); } + self.scopes.collect_garbage(id); + self.remove_nodes(e.children, false); } @@ -1296,16 +984,124 @@ impl<'bump> DiffState<'bump> { } VNode::Component(c) => { - let scope_id = c.scope.get().unwrap(); - let root = self.scopes.root_node(scope_id); - self.remove_nodes(Some(root), gen_muts); + 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); - // we can only remove this node if the originator is actively - if self.stack.scope_stack.contains(&c.originator) { - self.scopes.try_remove(scope_id).unwrap(); + // we can only remove this node if the originator is actively in our stackß + if self.scope_stack.contains(&c.originator) { + self.scopes.try_remove(scope_id).unwrap(); + } } + self.leave_scope(); } } } } + + 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) -> Option { + self.scope_stack.last().copied() + } + + 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 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)); + } + } + } + } + + // recursively push all the nodes of a tree onto the stack and return how many are there + fn push_all_nodes(&mut self, node: &'b VNode<'b>) -> usize { + match node { + VNode::Text(_) | VNode::Placeholder(_) => { + self.mutations.push_root(node.mounted_id()); + 1 + } + + VNode::Fragment(frag) => { + let mut added = 0; + for child in frag.children { + added += self.push_all_nodes(child); + } + added + } + + VNode::Component(c) => { + let scope_id = c.scope.get().unwrap(); + let root = self.scopes.root_node(scope_id); + self.push_all_nodes(root) + } + + VNode::Element(el) => { + let mut num_on_stack = 0; + for child in el.children.iter() { + num_on_stack += self.push_all_nodes(child); + } + + self.mutations.push_root(el.id.get().unwrap()); + + num_on_stack + 1 + } + } + } } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index c0dad0a47..8386cedd8 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -18,7 +18,7 @@ impl BubbleState { } } -/// User Events are events that are shuttled from the renderer into the VirtualDom trhough the scheduler channel. +/// User Events are events that are shuttled from the renderer into the VirtualDom through the scheduler channel. /// /// 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. diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index dba4effbb..e81ddca87 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -12,7 +12,6 @@ pub(crate) mod util; pub(crate) mod virtual_dom; pub(crate) mod innerlude { - pub(crate) use crate::diff::*; pub use crate::events::*; pub use crate::lazynodes::*; pub use crate::mutations::*; diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index fa96edd1b..bd94d0b71 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -143,6 +143,10 @@ impl<'a> Mutations<'a> { 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 }); @@ -217,6 +221,10 @@ impl<'a> Mutations<'a> { let name = attribute.name; self.edits.push(RemoveAttribute { name, root }); } + + pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) { + self.dirty_scopes.insert(scope); + } } // refs are only assigned once diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 80de92917..7a36f58ce 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -4,7 +4,7 @@ //! cheap and *very* fast to construct - building a full tree should be quick. use crate::{ - innerlude::{Element, Properties, Scope, ScopeId, ScopeState}, + innerlude::{ComponentPtr, Element, Properties, Scope, ScopeId, ScopeState}, lazynodes::LazyNodes, AnyEvent, Component, }; @@ -21,7 +21,7 @@ use std::{ /// - the `rsx!` macro /// - the [`NodeFactory`] API pub enum VNode<'src> { - /// Text VNodes simply bump-allocated (or static) string slices + /// Text VNodes are simply bump-allocated (or static) string slices /// /// # Example /// @@ -147,13 +147,6 @@ impl<'src> VNode<'src> { } } - pub(crate) fn children(&self) -> &[VNode<'src>] { - match &self { - VNode::Fragment(f) => f.children, - _ => &[], - } - } - // Create an "owned" version of the vnode. pub fn decouple(&self) -> VNode<'src> { match *self { @@ -175,13 +168,21 @@ impl Debug for VNode<'_> { .field("key", &el.key) .field("attrs", &el.attributes) .field("children", &el.children) + .field("id", &el.id) .finish(), VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text), - VNode::Placeholder(_) => write!(s, "VNode::VPlaceholder"), + VNode::Placeholder(t) => write!(s, "VNode::VPlaceholder {{ id: {:?} }}", t.id), VNode::Fragment(frag) => { write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children) } - VNode::Component(comp) => write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc), + VNode::Component(comp) => s + .debug_struct("VNode::VComponent") + .field("name", &comp.fn_name) + .field("fnptr", &comp.user_fc) + .field("key", &comp.key) + .field("scope", &comp.scope) + .field("originator", &comp.originator) + .finish(), } } } @@ -351,11 +352,18 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// } /// /// ``` -#[derive(Default)] pub struct EventHandler<'bump, T = ()> { pub callback: RefCell>>, } +impl<'a, T> Default for EventHandler<'a, T> { + fn default() -> Self { + Self { + callback: RefCell::new(None), + } + } +} + impl EventHandler<'_, T> { /// Call this event handler with the appropriate event type pub fn call(&self, event: T) { @@ -377,7 +385,8 @@ pub struct VComponent<'src> { pub originator: ScopeId, pub scope: Cell>, pub can_memoize: bool, - pub user_fc: *const (), + pub user_fc: ComponentPtr, + pub fn_name: &'static str, pub props: RefCell>>, } @@ -399,7 +408,7 @@ impl

AnyProps for VComponentProps

{ } // Safety: - // this will downcat the other ptr as our swallowed type! + // 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 { @@ -542,6 +551,7 @@ impl<'a> NodeFactory<'a> { component: fn(Scope<'a, P>) -> Element, props: P, key: Option, + fn_name: &'static str, ) -> VNode<'a> where P: Properties + 'a, @@ -550,8 +560,9 @@ impl<'a> NodeFactory<'a> { key: key.map(|f| self.raw_text(f).0), scope: Default::default(), can_memoize: P::IS_STATIC, - user_fc: component as *const (), + user_fc: component as ComponentPtr, originator: self.scope.scope_id(), + fn_name, props: RefCell::new(Some(Box::new(VComponentProps { // local_props: RefCell::new(Some(props)), // heap_props: RefCell::new(None), diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index cfc555543..00c3024bf 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -13,7 +13,9 @@ use std::{ rc::Rc, }; -pub(crate) type FcSlot = *const (); +/// 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; pub(crate) struct Heuristic { hook_arena_size: usize, @@ -28,7 +30,7 @@ pub(crate) struct ScopeArena { pub scope_gen: Cell, pub bump: Bump, pub scopes: RefCell>, - pub heuristics: RefCell>, + pub heuristics: RefCell>, pub free_scopes: RefCell>, pub nodes: RefCell>>, pub tasks: Rc, @@ -70,7 +72,7 @@ impl ScopeArena { } /// Safety: - /// - Obtaining a mutable refernece to any Scope is unsafe + /// - 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) } @@ -82,7 +84,7 @@ impl ScopeArena { pub(crate) fn new_with_key( &self, - fc_ptr: *const (), + fc_ptr: ComponentPtr, vcomp: Box, parent_scope: Option, container: ElementId, @@ -94,14 +96,14 @@ impl ScopeArena { // Get the height of the scope let height = parent_scope - .map(|id| self.get_scope(id).map(|scope| scope.height)) + .map(|id| self.get_scope(id).map(|scope| scope.height + 1)) .flatten() .unwrap_or_default(); let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten(); /* - This scopearena aggressively reuse old scopes when possible. + 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. @@ -110,31 +112,62 @@ impl ScopeArena { if let Some(old_scope) = self.free_scopes.borrow_mut().pop() { // reuse the old scope let scope = unsafe { &mut *old_scope }; - scope.props.get_mut().replace(vcomp); + + 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.our_arena_idx = new_scope_id; - scope.container = container; + 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::new( - height, + self.bump.alloc(ScopeState { container, - new_scope_id, + our_arena_idx: new_scope_id, parent_scope, - vcomp, - self.tasks.clone(), - self.heuristics - .borrow() - .get(&fc_ptr) - .map(|h| (h.node_arena_size, h.hook_arena_size)) - .unwrap_or_default(), - )), + 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::new(false), + + generation: 0.into(), + + tasks: self.tasks.clone(), + shared_contexts: Default::default(), + + items: RefCell::new(SelfReferentialItems { + listeners: Default::default(), + borrowed_props: Default::default(), + }), + + hook_arena: Bump::new(), + hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)), + hook_idx: Default::default(), + }), ); } @@ -189,8 +222,6 @@ impl ScopeArena { /// 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) { - log::trace!("Ensuring drop safety for scope {:?}", scope_id); - if let Some(scope) = self.get_scope(scope_id) { let mut items = scope.items.borrow_mut(); @@ -201,7 +232,6 @@ impl ScopeArena { if let Some(scope_id) = comp.scope.get() { self.ensure_drop_safety(scope_id); } - drop(comp.props.take()); }); @@ -217,18 +247,18 @@ impl ScopeArena { // 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 - log::trace!("Running scope {:?}", id); 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); - // book keeping to ensure safety around the borrowed data { // Safety: // - We've dropped all references to the wip bump frame with "ensure_drop_safety" @@ -241,8 +271,6 @@ impl ScopeArena { debug_assert!(items.borrowed_props.is_empty()); } - // safety: this is definitely not dropped - /* 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. @@ -284,14 +312,13 @@ impl ScopeArena { while let Some(id) = cur_el.take() { if let Some(el) = nodes.get(id.0) { - log::trace!("Found valid receiver element"); - let real_el = unsafe { &**el }; + log::debug!("looking for listener on {:?}", real_el); + if let VNode::Element(real_el) = real_el { for listener in real_el.listeners.borrow().iter() { if listener.event == event.name { - log::trace!("Found valid receiver event"); - + log::debug!("calling listener {:?}", listener.event); if state.canceled.get() { // stop bubbling if canceled break; @@ -421,6 +448,7 @@ pub struct ScopeState { pub(crate) container: ElementId, pub(crate) our_arena_idx: ScopeId, pub(crate) height: u32, + pub(crate) fnptr: ComponentPtr, // todo: subtrees pub(crate) is_subtree_root: Cell, @@ -449,43 +477,6 @@ pub struct SelfReferentialItems<'a> { // Public methods exposed to libraries and components impl ScopeState { - fn new( - height: u32, - container: ElementId, - our_arena_idx: ScopeId, - parent_scope: Option<*mut ScopeState>, - vcomp: Box, - tasks: Rc, - (node_capacity, hook_capacity): (usize, usize), - ) -> Self { - ScopeState { - container, - our_arena_idx, - parent_scope, - height, - props: RefCell::new(Some(vcomp)), - frames: [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)], - - // todo: subtrees - subtree: Cell::new(0), - is_subtree_root: Cell::new(false), - - generation: 0.into(), - - tasks, - shared_contexts: Default::default(), - - items: RefCell::new(SelfReferentialItems { - listeners: Default::default(), - borrowed_props: Default::default(), - }), - - hook_arena: Bump::new(), - hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)), - hook_idx: Default::default(), - } - } - /// 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 @@ -695,6 +686,7 @@ impl ScopeState { .insert(TypeId::of::(), value.clone()) .map(|f| f.downcast::().ok()) .flatten(); + return value; } let mut search_parent = self.parent_scope; @@ -750,6 +742,11 @@ impl ScopeState { self.tasks.push_fut(fut) } + /// Spawns the future but does not return the TaskId + pub fn spawn(&self, fut: impl Future + 'static) { + self.push_future(fut); + } + // todo: attach some state to the future to know if we should poll it pub fn remove_future(&self, id: TaskId) { self.tasks.remove_fut(id); @@ -890,13 +887,15 @@ impl ScopeState { self.frames[0].reset(); self.frames[1].reset(); - // Finally, free up the hook values - self.hook_arena.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(); } } diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index f29a13e17..b9640792d 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -3,7 +3,7 @@ use crate::innerlude::*; pub struct ElementIdIterator<'a> { vdom: &'a VirtualDom, - // Heuristcally we should never bleed into 5 completely nested fragments/components + // 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]>, } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 29e8355cc..a7092104c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -2,6 +2,7 @@ //! //! 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}; @@ -9,12 +10,12 @@ use fxhash::FxHashSet; use indexmap::IndexSet; use std::{collections::VecDeque, iter::FromIterator, task::Poll}; -/// A virtual node s ystem that progresses user events and diffs UI trees. +/// 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`]. +/// Components are defined as simple functions that take [`Scope`] and return an [`Element`]. /// /// ```rust, ignore /// #[derive(Props, PartialEq)] @@ -212,7 +213,7 @@ impl VirtualDom { let scopes = ScopeArena::new(channel.0.clone()); scopes.new_with_key( - root as *const _, + root as ComponentPtr, Box::new(VComponentProps { props: root_props, memo: |_a, _b| unreachable!("memo on root will neve be run"), @@ -233,7 +234,7 @@ impl VirtualDom { /// Get the [`Scope`] for the root component. /// - /// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus + /// 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))` @@ -448,6 +449,7 @@ impl VirtualDom { /// apply_mutations(mutations); /// } /// ``` + #[allow(unused)] pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec { let mut committed_mutations = vec![]; @@ -467,36 +469,41 @@ impl VirtualDom { h1.cmp(&h2).reverse() }); + log::debug!("dirty_scopes: {:?}", self.dirty_scopes); + if let Some(scopeid) = self.dirty_scopes.pop() { if !ran_scopes.contains(&scopeid) { ran_scopes.insert(scopeid); self.scopes.run_scope(scopeid); - let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); - diff_state.stack.push(DiffInstruction::Diff { new, old }); - diff_state.stack.scope_stack.push(scopeid); + diff_state.diff_scope(scopeid); - let scope = scopes.get_scope(scopeid).unwrap(); - diff_state.stack.element_stack.push(scope.container); + let DiffState { mutations, .. } = diff_state; + + log::debug!("succesffuly resolved scopes {:?}", mutations.dirty_scopes); + for scope in &mutations.dirty_scopes { + self.dirty_scopes.remove(scope); + } + + 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; + // } } } - - 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; - } } committed_mutations @@ -524,13 +531,15 @@ impl VirtualDom { let mut diff_state = DiffState::new(&self.scopes); self.scopes.run_scope(scope_id); - diff_state - .stack - .create_node(self.scopes.fin_head(scope_id), MountType::Append); - diff_state.stack.element_stack.push(ElementId(0)); - diff_state.stack.scope_stack.push(scope_id); - diff_state.work(|| false); + 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()); @@ -577,12 +586,11 @@ impl VirtualDom { ); diff_machine.force_diff = true; - diff_machine.stack.push(DiffInstruction::Diff { old, new }); - diff_machine.stack.scope_stack.push(scope_id); - + diff_machine.scope_stack.push(scope_id); let scope = diff_machine.scopes.get_scope(scope_id).unwrap(); - diff_machine.stack.element_stack.push(scope.container); - diff_machine.work(|| false); + diff_machine.element_stack.push(scope.container); + + diff_machine.diff_node(old, new); diff_machine.mutations } @@ -618,13 +626,13 @@ impl VirtualDom { /// /// 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.stack.push(DiffInstruction::Diff { new, old }); - machine.stack.element_stack.push(ElementId(0)); - machine.stack.scope_stack.push(ScopeId(0)); - machine.work(|| false); + machine.element_stack.push(ElementId(0)); + machine.scope_stack.push(ScopeId(0)); + machine.diff_node(old, new); + machine.mutations } @@ -643,11 +651,11 @@ impl VirtualDom { /// ``` pub fn create_vnodes<'a>(&'a self, nodes: LazyNodes<'a, '_>) -> Mutations<'a> { let mut machine = DiffState::new(&self.scopes); - machine.stack.element_stack.push(ElementId(0)); - machine - .stack - .create_node(self.render_vnodes(nodes), MountType::Append); - machine.work(|| false); + 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 } @@ -672,16 +680,15 @@ impl VirtualDom { let (old, new) = (self.render_vnodes(left), self.render_vnodes(right)); let mut create = DiffState::new(&self.scopes); - create.stack.scope_stack.push(ScopeId(0)); - create.stack.element_stack.push(ElementId(0)); - create.stack.create_node(old, MountType::Append); - create.work(|| false); + 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.stack.scope_stack.push(ScopeId(0)); - edit.stack.element_stack.push(ElementId(0)); - edit.stack.push(DiffInstruction::Diff { old, new }); - edit.work(|| false); + edit.scope_stack.push(ScopeId(0)); + edit.element_stack.push(ElementId(0)); + edit.diff_node(old, new); (create.mutations, edit.mutations) } diff --git a/packages/core/tests/.rustfmt.toml b/packages/core/tests/.rustfmt.toml new file mode 100644 index 000000000..70a9483a8 --- /dev/null +++ b/packages/core/tests/.rustfmt.toml @@ -0,0 +1 @@ +struct_lit_width = 80 diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs index 044be289c..0cdc37c6d 100644 --- a/packages/core/tests/create_dom.rs +++ b/packages/core/tests/create_dom.rs @@ -36,18 +36,9 @@ fn test_original_diff() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "Hello, world!" - }, + 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 }, @@ -81,34 +72,13 @@ fn create() { 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" - }, + 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 }, @@ -135,32 +105,14 @@ fn create_list() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - root: 4, - text: "hello" - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { root: 4, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "hello" - }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "hello" }, AppendChildren { many: 1 }, AppendChildren { many: 3 }, ] @@ -185,22 +137,10 @@ fn create_simple() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, AppendChildren { many: 4 }, ] ); @@ -235,36 +175,18 @@ fn create_components() { mutations.edits, [ CreateElement { root: 1, tag: "h1" }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "abc1" - }, + 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" - }, + 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" - }, + CreateElement { root: 10, tag: "div" }, + CreateTextNode { root: 11, text: "abc3" }, AppendChildren { many: 1 }, CreateElement { root: 12, tag: "p" }, AppendChildren { many: 9 }, @@ -285,14 +207,8 @@ fn anchors() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, CreatePlaceholder { root: 3 }, AppendChildren { many: 2 }, diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 5b98ea80e..6bc9b5596 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -1,4 +1,5 @@ #![allow(unused, non_upper_case_globals)] + //! Diffing Tests //! //! These tests only verify that the diffing algorithm works properly for single components. @@ -31,26 +32,14 @@ fn html_and_rsx_generate_the_same_output() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "Hello world" - }, + 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 - },] - ); + assert_eq!(change.edits, [SetText { text: "Goodbye world", root: 2 },]); } /// Should result in 3 elements on the stack @@ -67,32 +56,14 @@ fn fragments_create_properly() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "Hello a" - }, + 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" - }, + 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" - }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "Hello c" }, AppendChildren { many: 1 }, AppendChildren { many: 3 }, ] @@ -116,10 +87,7 @@ fn empty_fragments_create_anchors() { assert_eq!( change.edits, [ - CreateElement { - root: 2, - tag: "div" - }, + CreateElement { root: 2, tag: "div" }, ReplaceWith { m: 1, root: 1 } ] ); @@ -138,29 +106,15 @@ fn empty_fragments_create_many_anchors() { 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" - }, + 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 } ] ); @@ -188,32 +142,14 @@ fn empty_fragments_create_anchors_with_many_children() { assert_eq!( change.edits, [ - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "hello: 0", - root: 3 - }, + 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 - }, + 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 - }, + CreateElement { tag: "div", root: 6 }, + CreateTextNode { text: "hello: 2", root: 7 }, AppendChildren { many: 1 }, ReplaceWith { root: 1, m: 3 } ] @@ -236,23 +172,11 @@ fn many_items_become_fragment() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - text: "hello", - root: 2 - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { text: "hello", root: 2 }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - text: "hello", - root: 4 - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { text: "hello", root: 4 }, AppendChildren { many: 1 }, AppendChildren { many: 2 }, ] @@ -315,7 +239,7 @@ fn two_fragments_with_differrent_elements_are_differet() { // replace the divs with new h1s CreateElement { tag: "h1", root: 7 }, ReplaceWith { root: 1, m: 1 }, - CreateElement { tag: "h1", root: 8 }, + CreateElement { tag: "h1", root: 1 }, // notice how 1 gets re-used ReplaceWith { root: 2, m: 1 }, ] ); @@ -339,39 +263,27 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() { 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: 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: 7, tag: "h1" }, - ReplaceWith { root: 1, m: 1 }, - CreateElement { root: 8, tag: "h1" }, + 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 }, ] ); @@ -395,14 +307,8 @@ fn two_fragments_with_same_elements_are_differet() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, CreateElement { root: 3, tag: "p" }, AppendChildren { many: 3 }, ] @@ -410,18 +316,9 @@ fn two_fragments_with_same_elements_are_differet() { assert_eq!( change.edits, [ - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateElement { root: 6, tag: "div" }, InsertAfter { root: 2, n: 3 }, ] ); @@ -628,14 +525,8 @@ fn keyed_diffing_additions() { assert_eq!( change.edits, [ - CreateElement { - root: 6, - tag: "div" - }, - CreateElement { - root: 7, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, + CreateElement { root: 7, tag: "div" }, InsertAfter { n: 2, root: 5 } ] ); @@ -663,14 +554,8 @@ fn keyed_diffing_additions_and_moves_on_ends() { change.edits, [ // create 11, 12 - CreateElement { - tag: "div", - root: 5 - }, - CreateElement { - tag: "div", - root: 6 - }, + CreateElement { tag: "div", root: 5 }, + CreateElement { tag: "div", root: 6 }, InsertAfter { root: 3, n: 2 }, // move 7 to the front PushRoot { root: 4 }, @@ -684,13 +569,13 @@ fn keyed_diffing_additions_and_moves_in_middle() { let dom = new_dom(); let left = rsx!({ - [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { + [/**/ 1, 2, 3, 4 /**/].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); let right = rsx!({ - [/**/ 7, 4, 13, 17, 5, 11, 12, 6 /**/].iter().map(|f| { + [/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); @@ -701,26 +586,14 @@ fn keyed_diffing_additions_and_moves_in_middle() { assert_eq!( change.edits, [ - // create 13, 17 - CreateElement { - tag: "div", - root: 5 - }, - CreateElement { - tag: "div", - root: 6 - }, - InsertBefore { root: 2, n: 2 }, - // create 11, 12 - CreateElement { - tag: "div", - root: 7 - }, - CreateElement { - tag: "div", - root: 8 - }, + // 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 } @@ -756,16 +629,10 @@ fn controlled_keyed_diffing_out_of_order() { // remove 7 // create 9 and insert before 6 - CreateElement { - root: 5, - tag: "div" - }, + CreateElement { root: 5, tag: "div" }, InsertBefore { n: 1, root: 3 }, // create 0 and insert before 5 - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, InsertBefore { n: 1, root: 2 }, ] ); @@ -792,10 +659,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { assert_eq!( changes.edits, [ - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, InsertBefore { n: 1, root: 3 }, PushRoot { root: 4 }, InsertBefore { n: 1, root: 1 }, diff --git a/packages/core/tests/earlyabort.rs b/packages/core/tests/earlyabort.rs index 7dc2ae4d4..3999a7402 100644 --- a/packages/core/tests/earlyabort.rs +++ b/packages/core/tests/earlyabort.rs @@ -42,14 +42,8 @@ fn test_early_abort() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 1, - }, - CreateTextNode { - text: "Hello, world!", - root: 2, - }, + CreateElement { tag: "div", root: 1 }, + CreateTextNode { text: "Hello, world!", root: 2 }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, ] @@ -65,14 +59,8 @@ fn test_early_abort() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "Hello, world!", - root: 4, - }, + 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/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 6c0df4e1e..f977ba96e 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -27,12 +27,7 @@ fn manual_diffing() { }; let value = Arc::new(Mutex::new("Hello")); - let mut dom = VirtualDom::new_with_props( - App, - AppProps { - value: value.clone(), - }, - ); + let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone() }); let _ = dom.rebuild(); @@ -45,7 +40,7 @@ fn manual_diffing() { #[test] fn events_generate() { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let count = cx.use_hook(|_| 0); let inner = match *count { @@ -66,7 +61,7 @@ fn events_generate() { cx.render(inner) }; - let mut dom = VirtualDom::new(App); + let mut dom = VirtualDom::new(app); let mut channel = dom.get_scheduler_channel(); assert!(dom.has_work()); @@ -74,28 +69,12 @@ fn events_generate() { 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, - }, + 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, - }, + CreateTextNode { text: "Click me!", root: 4 }, AppendChildren { many: 2 }, AppendChildren { many: 1 }, ] @@ -104,7 +83,7 @@ fn events_generate() { #[test] fn components_generate() { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let render_phase = cx.use_hook(|_| 0); *render_phase += 1; @@ -121,106 +100,84 @@ fn components_generate() { }) }; - static Child: Component = |cx| { + fn Child(cx: Scope) -> Element { + log::debug!("Running child"); cx.render(rsx! { h1 {} }) - }; + } - let mut dom = VirtualDom::new(App); + let mut dom = VirtualDom::new(app); let edits = dom.rebuild(); assert_eq!( edits.edits, [ - CreateTextNode { - text: "Text0", - root: 1, - }, + CreateTextNode { text: "Text0", root: 1 }, AppendChildren { many: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateElement { - tag: "div", - root: 2, - }, + CreateElement { tag: "div", root: 2 }, ReplaceWith { root: 1, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateTextNode { - text: "Text2", - root: 3, - }, + CreateTextNode { text: "Text2", root: 1 }, ReplaceWith { root: 2, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); + // child {} assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateElement { tag: "h1", root: 4 }, - ReplaceWith { root: 3, m: 1 }, + CreateElement { tag: "h1", root: 2 }, + ReplaceWith { root: 1, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); + // placeholder assert_eq!( - edits.edits, - [CreatePlaceholder { root: 5 }, ReplaceWith { root: 4, m: 1 },] + dom.hard_diff(ScopeId(0)).edits, + [CreatePlaceholder { root: 1 }, ReplaceWith { root: 2, m: 1 },] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateTextNode { - text: "text 3", - root: 6, - }, - ReplaceWith { root: 5, m: 1 }, + CreateTextNode { text: "text 3", root: 2 }, + ReplaceWith { root: 1, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateTextNode { - text: "text 0", - root: 7, - }, - CreateTextNode { - text: "text 1", - root: 8, - }, - ReplaceWith { root: 6, m: 2 }, + CreateTextNode { text: "text 0", root: 1 }, + CreateTextNode { text: "text 1", root: 3 }, + ReplaceWith { root: 2, m: 2 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateElement { tag: "h1", root: 9 }, - ReplaceWith { root: 7, m: 1 }, - Remove { root: 8 }, + CreateElement { tag: "h1", root: 2 }, + ReplaceWith { root: 1, m: 1 }, + Remove { root: 3 }, ] ); } #[test] fn component_swap() { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let render_phase = cx.use_hook(|_| 0); *render_phase += 1; @@ -296,7 +253,7 @@ fn component_swap() { }) }; - let mut dom = VirtualDom::new(App); + let mut dom = VirtualDom::new(app); let edits = dom.rebuild(); dbg!(&edits); diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index bb4963b71..8ce030147 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -152,12 +152,7 @@ fn free_works_on_root_props() { } } - let mut dom = new_dom( - app, - Custom { - val: String::from("asd"), - }, - ); + let mut dom = new_dom(app, Custom { val: String::from("asd") }); dom.rebuild(); } @@ -313,3 +308,123 @@ fn leak_thru_children() { dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); dom.work_with_deadline(|| false); } + +#[test] +fn test_pass_thru() { + #[inline_props] + fn Router<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { + div { + &cx.props.children + } + }) + } + + #[inline_props] + fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { + header { + nav { + &cx.props.children + } + } + }) + } + + fn NavMenu(cx: Scope) -> Element { + rsx!(cx, + NavBrand {} + div { + NavStart {} + NavEnd {} + } + ) + } + + fn NavBrand(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + fn NavStart(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + fn NavEnd(cx: Scope) -> Element { + rsx!(cx, 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", + &cx.props.nav, + &cx.props.body, + &cx.props.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 x 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/passthru.rs b/packages/core/tests/passthru.rs new file mode 100644 index 000000000..e00aa545c --- /dev/null +++ b/packages/core/tests/passthru.rs @@ -0,0 +1,108 @@ +#![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::*, DomEdit, ScopeId}; +use dioxus_core as dioxus; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; + +mod test_logging; + +fn new_dom() -> VirtualDom { + const IS_LOGGING_ENABLED: bool = false; + test_logging::set_up_logging(IS_LOGGING_ENABLED); + VirtualDom::new(|cx| rsx!(cx, "hi")) +} + +use 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/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs index 3f04aa154..70e823569 100644 --- a/packages/core/tests/sharedstate.rs +++ b/packages/core/tests/sharedstate.rs @@ -1,6 +1,6 @@ #![allow(unused, non_upper_case_globals)] -use dioxus::{prelude::*, DomEdit, Mutations}; +use dioxus::{prelude::*, DomEdit, Mutations, SchedulerMsg, ScopeId}; use dioxus_core as dioxus; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; @@ -29,11 +29,102 @@ fn shared_state_test() { assert_eq!( edits, [ - CreateTextNode { - root: 1, - text: "Hello, world!" - }, + 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(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/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index 5d1dbcdf8..83b4ff35f 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -68,15 +68,9 @@ fn conditional_rendering() { mutations.edits, [ CreateElement { root: 1, tag: "h1" }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "span" - }, + CreateElement { root: 3, tag: "span" }, CreateTextNode { root: 4, text: "a" }, AppendChildren { many: 1 }, CreatePlaceholder { root: 5 }, diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index 245d05623..65657ab37 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -5,6 +5,76 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Documentation + + - fix web doc example and use &mut for builders everywhere + +### New Features + + - default asset server + - remove dioxus id on non-event elements + - also hide placeholder node + - wire up both desktop and web + - bool attr white list + - setup a typescript build + +### Bug Fixes + + - custom protocol receiver type + - clippy + - wry pathing + - cursor jumping and use set instead of lsit + - format code + - check `href` null + - add exclusion list + - check `href` null + - prevent `submit` default + +### Commit Statistics + + + + - 26 commits contributed to the release over the course of 11 calendar days. + - 17 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +

view details + + * **Uncategorized** + - custom protocol receiver type ([`22308eb`](https://github.comgit//DioxusLabs/dioxus/commit/22308eb26a9ea48b14f5f5abb833aa90a4e3fc40)) + - default asset server ([`430cde7`](https://github.comgit//DioxusLabs/dioxus/commit/430cde7068d308f2783e33d278fd0c0efa659c1b)) + - fix web doc example and use &mut for builders everywhere ([`a239d2b`](https://github.comgit//DioxusLabs/dioxus/commit/a239d2ba6ac7f1f3d09de16c022ce8ca52cf0f63)) + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - clippy ([`6bc45b1`](https://github.comgit//DioxusLabs/dioxus/commit/6bc45b1c5064a4e2b04d452c52a8167ad179691e)) + - Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401)) + - wry pathing ([`bad3616`](https://github.comgit//DioxusLabs/dioxus/commit/bad36162af764291f5a031b6233d151f61d745a4)) + - remove dioxus id on non-event elements ([`95e93ed`](https://github.comgit//DioxusLabs/dioxus/commit/95e93ed0bcf6c69990f4cf3c6448b2bf5da96c36)) + - also hide placeholder node ([`eb13884`](https://github.comgit//DioxusLabs/dioxus/commit/eb138848ec7a8978f0ed7c717374684d2315dc03)) + - drag and drop support ([`9ae981a`](https://github.comgit//DioxusLabs/dioxus/commit/9ae981a1af4b5474ce16e27e070794d59128c12a)) + - feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9)) + - cursor jumping and use set instead of lsit ([`be614e6`](https://github.comgit//DioxusLabs/dioxus/commit/be614e6535e6e13e6ff93e9c6a171c1c002e6b01)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3)) + - Merge pull request #101 from alexkirsz/ci ([`29bf424`](https://github.comgit//DioxusLabs/dioxus/commit/29bf424b0976b95ff645bb128d0e758cf0186614)) + - Merge pull request #139 from DioxusLabs/jk/provide-context-any ([`70f2ef4`](https://github.comgit//DioxusLabs/dioxus/commit/70f2ef43db5b6737bd9bcbfc1aa21c834ce4b395)) + - Merge branch 'master' into jk/unify ([`824defa`](https://github.comgit//DioxusLabs/dioxus/commit/824defa2dbcc16d66588b3976699d89b65a8a068)) + - wire up both desktop and web ([`05331dd`](https://github.comgit//DioxusLabs/dioxus/commit/05331ddd8033f6997d4916179b62f4d62f832988)) + - format code ([`9256161`](https://github.comgit//DioxusLabs/dioxus/commit/92561612c727e73356d7d36e16af39aacf02a56d)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - bool attr white list ([`8f4aa84`](https://github.comgit//DioxusLabs/dioxus/commit/8f4aa84f1a4f2443b34d81ee42490564e168de53)) + - setup a typescript build ([`5bf6c96`](https://github.comgit//DioxusLabs/dioxus/commit/5bf6c96f9fed04de949403202bafcbeadb5d2030)) + - check `href` null ([`2073b40`](https://github.comgit//DioxusLabs/dioxus/commit/2073b400df55f0c6d8bed7371b2313be6c064e6e)) + - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) + - check `href` null ([`327f901`](https://github.comgit//DioxusLabs/dioxus/commit/327f9015481809d8e5b9e69f26202e8d66dd198e)) + - prevent `submit` default ([`8089023`](https://github.comgit//DioxusLabs/dioxus/commit/8089023a6c3a54957af9c9c05c9dee6088b059ef)) +
+ ## v0.1.5 (2022-01-08) ### Documentation @@ -37,6 +107,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - omg what a dumb mistake - bless up, no more segfaults - wire up event delegator for webview + - link open in browser + - move `rpc` to handler + - `open_browser` bool attribute + - link open in browser + - link open in browser ### Bug Fixes @@ -48,13 +123,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - tags - desktop and mobile - messed up how lifetimes worked, need to render once per component + - error pattern + - format code ### Commit Statistics - - 91 commits contributed to the release over the course of 151 calendar days. - - 78 commits where understood as [conventional](https://www.conventionalcommits.org). + - 98 commits contributed to the release over the course of 151 calendar days. + - 84 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages ### Commit Details @@ -64,11 +141,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** - - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) + - Release dioxus-desktop v0.1.5 ([`cd0dcac`](https://github.comgit//DioxusLabs/dioxus/commit/cd0dcacaf2862f26d29acb21d98f75d41b940e3f)) - handle bool attrs properly ([`8d685f4`](https://github.comgit//DioxusLabs/dioxus/commit/8d685f40b7e0ef6521c60310d8687291e9b9c48a)) + - link open in browser ([`46fd6ac`](https://github.comgit//DioxusLabs/dioxus/commit/46fd6ac3450ca5ebf9aecb2d59a5a92b2a68bdd0)) + - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) + - error pattern ([`62b637f`](https://github.comgit//DioxusLabs/dioxus/commit/62b637f8b0eaf616c49461fa23b9251a79abc147)) + - move `rpc` to handler ([`f006f50`](https://github.comgit//DioxusLabs/dioxus/commit/f006f50317f4b75fac353bc988db057a281ba7f8)) + - `open_browser` bool attribute ([`9e04ce5`](https://github.comgit//DioxusLabs/dioxus/commit/9e04ce5342850d2e0a01dde169807d6f6eb16566)) + - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - format code ([`5233ee9`](https://github.comgit//DioxusLabs/dioxus/commit/5233ee97d9314f7f0e0bdf05c56d2a9e4201a596)) + - link open in browser ([`c737c42`](https://github.comgit//DioxusLabs/dioxus/commit/c737c424b05ad8453e8770a14a0d210fb0c7c2fe)) - Merge pull request #89 from DioxusLabs/jk/simplify-example-run ([`8b6aa8b`](https://github.comgit//DioxusLabs/dioxus/commit/8b6aa8b880b6cb5c95e0c0743aad4e4e74388e05)) + - link open in browser ([`a0f6015`](https://github.comgit//DioxusLabs/dioxus/commit/a0f60152bc7e5866f114ed469809ce8be70d17d4)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) - Merge pull request #80 from DioxusLabs/jk/router2dotoh ([`cdc2d8e`](https://github.comgit//DioxusLabs/dioxus/commit/cdc2d8ec6d123245c2ea5f6d10af02b6a6833994)) - clear warnigns ([`175a6a1`](https://github.comgit//DioxusLabs/dioxus/commit/175a6a199c6738d8d0c7646ba0ec3fc4406c6535)) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index eb9425c1f..0d393e1ef 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-desktop" -version = "0.1.5" +version = "0.1.6" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for a remote webview instance" @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7", features = ["serialize"] } +dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } argh = "0.1.4" serde = "1.0.120" serde_json = "1.0.61" @@ -27,9 +27,10 @@ tokio = { version = "1.12.0", features = [ "rt", "time", ], optional = true, default-features = false } -dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"} -dioxus-html = { path = "../html", features = ["serialize"], version ="^0.1.4"} +dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } +dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" +mime_guess = "2.0.3" [features] default = ["tokio_runtime"] diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index ad85cfc0f..5baa46f5d 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -71,7 +71,7 @@ impl DesktopConfig { self } - pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self + pub fn with_custom_protocol(&mut self, name: String, handler: F) -> &mut Self where F: Fn(&HttpRequest) -> WryResult + 'static, { diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 48c82f5a8..9c0404477 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,100 +1,67 @@ -use std::cell::RefCell; +use std::rc::Rc; -use dioxus::prelude::Scope; -use dioxus_core as dioxus; -use dioxus_core::{Context, Element, LazyNodes, NodeFactory, Properties}; -use dioxus_core_macro::Props; +use dioxus_core::ScopeState; +use wry::application::event_loop::EventLoopProxy; -/* -This module provides a set of Dioxus components to easily manage windows, tabs, etc. +use crate::UserWindowEvent; -Windows can be created anywhere in the tree, making them very flexible for things like modals, etc. +type ProxyType = EventLoopProxy; -*/ -pub struct DesktopContext {} +/// Desktop-Window handle api context +/// +/// you can use this context control some window event +/// +/// you can use `cx.consume_context::` to get this context +/// +/// ```rust +/// let desktop = cx.consume_context::().unwrap(); +/// ``` +#[derive(Clone)] +pub struct DesktopContext { + proxy: ProxyType, +} impl DesktopContext { - fn add_window(&mut self) { - // + pub(crate) fn new(proxy: ProxyType) -> Self { + Self { proxy } } - fn close_window(&mut self) { - // + + /// trigger the drag-window event + /// + /// Moves the window with the left mouse button until the button is released. + /// + /// you need use it in `onmousedown` event: + /// ```rust + /// onmousedown: move |_| { desktop.drag_window(); } + /// ``` + pub fn drag(&self) { + let _ = self.proxy.send_event(UserWindowEvent::DragWindow); + } + + /// set window minimize state + pub fn minimize(&self, minimized: bool) { + let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized)); + } + + /// set window maximize state + pub fn maximize(&self, maximized: bool) { + let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); + } + + /// close window + pub fn close(&self) { + let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); + } + + /// set window to focus + pub fn focus(&self) { + let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } } -enum WindowHandlers { - Resized(Box), - Moved(Box), - CloseRequested(Box), - Destroyed(Box), - DroppedFile(Box), - HoveredFile(Box), - HoverFileCancelled(Box), - ReceivedTimeText(Box), - Focused(Box), -} - -#[derive(Props)] -pub struct WebviewWindowProps<'a> { - onclose: &'a dyn FnMut(()), - - onopen: &'a dyn FnMut(()), - - /// focuse me - onfocused: &'a dyn FnMut(()), - - children: Element, -} - -/// A handle to a -/// -/// -/// -/// -/// -/// -/// -/// -/// -pub fn WebviewWindow(cx: Scope) -> Element { - let dtcx = cx.consume_state::>()?; - - cx.use_hook(|_| {}); - - // render the children directly - todo!() - // cx.render(LazyNodes::new(move |f: NodeFactory| { - // f.fragment_from_iter(cx.children()) - // })) -} - -pub struct WindowHandle {} - -/// Get a handle to the current window from inside a component -pub fn use_current_window(cx: Scope) -> Option { - todo!() -} - -#[test] -fn syntax_works() { - use dioxus_core as dioxus; - use dioxus_core::prelude::*; - use dioxus_core_macro::*; - use dioxus_hooks::*; - use dioxus_html as dioxus_elements; - - static App: Component = |cx| { - cx.render(rsx! { - // left window - WebviewWindow { - onclose: move |evt| {} - onopen: move |evt| {} - onfocused: move |evt| {} - - div { - - } - } - }) - }; +/// use this function can get the `DesktopContext` context. +pub fn use_window(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| cx.consume_context::()) + .as_ref() + .unwrap() } diff --git a/packages/desktop/src/interpreter.js b/packages/desktop/src/interpreter.js new file mode 100644 index 000000000..503ea7247 --- /dev/null +++ b/packages/desktop/src/interpreter.js @@ -0,0 +1,550 @@ +export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +export class Interpreter { + root; + stack; + listeners; + handlers; + lastNodeWasText; + nodes; + constructor(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + top() { + return this.stack[this.stack.length - 1]; + } + pop() { + return this.stack.pop(); + } + PushRoot(root) { + const node = this.nodes[root]; + this.stack.push(node); + } + AppendChildren(many) { + let root = this.stack[this.stack.length - (1 + many)]; + let to_add = this.stack.splice(this.stack.length - many); + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + } + ReplaceWith(root_id, m) { + let root = this.nodes[root_id]; + let els = this.stack.splice(this.stack.length - m); + root.replaceWith(...els); + } + InsertAfter(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + InsertBefore(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + Remove(root) { + let node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + } + CreateTextNode(text, root) { + // todo: make it so the types are okay + const node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + } + CreateElement(tag, root) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + this.nodes[root] = el; + this.stack.push(el); + } + CreateElementNs(tag, root, ns) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + CreatePlaceholder(root) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + NewEventListener(event_name, root, handler) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } else { + this.listeners[event_name]++; + } + } + RemoveEventListener(root, event_name) { + const element = this.nodes[root]; + element.removeAttribute(`data-dioxus-id`); + this.listeners[event_name]--; + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; + } + } + SetText(root, text) { + this.nodes[root].textContent = text; + } + SetAttribute(root, field, value, ns) { + const name = field; + const node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root, name) { + const node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + } + handleEdits(edits) { + this.stack.push(this.root); + for (let edit of edits) { + this.handleEdit(edit); + } + } + handleEdit(edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event) => { + let target = event.target; + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + let shouldPreventDefault = target.getAttribute( + `dioxus-prevent-default` + ); + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + console.log("click", event); + console.log("clickeded", event.target); + console.log("clickeded", event.target.tagName); + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.rpc.call("browser_open", { href }); + } + } + } + } + + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + + shouldPreventDefault = target.getAttribute( + `dioxus-prevent-default` + ); + + let contents = serialize_event(event); + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + if (event.type == "submit") { + event.preventDefault(); + } + if (realId == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} +function serialize_event(event) { + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; + } + case "compositionend": + case "compositionstart": + case "compositionupdate": { + let { data } = event; + return { + data, + }; + } + case "keydown": + case "keypress": + case "keyup": { + let { + charCode, + key, + altKey, + ctrlKey, + metaKey, + keyCode, + shiftKey, + location, + repeat, + which, + } = event; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location, + repeat: repeat, + which: which, + locale: "locale", + }; + } + case "focus": + case "blur": { + return {}; + } + case "change": { + let target = event.target; + let value; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } else { + value = target.value ?? target.textContent; + } + return { + value: value, + }; + } + case "input": + case "invalid": + case "reset": + case "submit": { + let target = event.target; + let value = target.value ?? target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + const { + altKey, + button, + buttons, + clientX, + clientY, + ctrlKey, + metaKey, + pageX, + pageY, + screenX, + screenY, + shiftKey, + } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + const { + altKey, + button, + buttons, + clientX, + clientY, + ctrlKey, + metaKey, + pageX, + pageY, + screenX, + screenY, + shiftKey, + pointerId, + width, + height, + pressure, + tangentialPressure, + tiltX, + tiltY, + twist, + pointerType, + isPrimary, + } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary, + }; + } + case "select": { + return {}; + } + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + const { altKey, ctrlKey, metaKey, shiftKey } = event; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, + }; + } + case "scroll": { + return {}; + } + case "wheel": { + const { deltaX, deltaY, deltaZ, deltaMode } = event; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + const { animationName, elapsedTime, pseudoElement } = event; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + const { propertyName, elapsedTime, pseudoElement } = event; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": { + return {}; + } + case "toggle": { + return {}; + } + default: { + return {}; + } + } +} +const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, +}; diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index bc9a84e42..22acc4f85 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -51,10 +51,13 @@ //! Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! pub mod cfg; +pub mod desktop_context; pub mod escape; pub mod events; use cfg::DesktopConfig; +pub use desktop_context::use_window; +use desktop_context::DesktopContext; use dioxus_core::*; use std::{ collections::{HashMap, VecDeque}, @@ -149,7 +152,7 @@ pub fn launch_with_props( props: P, builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig, ) { - let mut cfg = DesktopConfig::new(); + let mut cfg = DesktopConfig::default(); builder(&mut cfg); let event_loop = EventLoop::with_user_event(); @@ -188,6 +191,7 @@ pub fn launch_with_props( let _ = proxy.send_event(UserWindowEvent::Update); } "browser_open" => { + println!("browser_open"); let data = req.params.unwrap(); log::trace!("Open browser: {:?}", data); if let Some(arr) = data.as_array() { @@ -210,18 +214,43 @@ pub fn launch_with_props( // For now, we only serve two pieces of content which get included as bytes into the final binary. let path = request.uri().replace("dioxus://", ""); - if path.trim_end_matches('/') == "index.html" { + // all assets shouldbe called from index.html + let trimmed = path.trim_start_matches("index.html/"); + + if trimmed.is_empty() { wry::http::ResponseBuilder::new() .mimetype("text/html") .body(include_bytes!("./index.html").to_vec()) - } else if path.trim_end_matches('/') == "index.html/index.js" { + } else if trimmed == "index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") - .body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec()) + .body(include_bytes!("./interpreter.js").to_vec()) } else { - wry::http::ResponseBuilder::new() - .status(wry::http::status::StatusCode::NOT_FOUND) - .body(format!("Not found: {}", path).as_bytes().to_vec()) + // Read the file content from file path + use std::fs::read; + + let path_buf = std::path::Path::new(trimmed).canonicalize()?; + let cur_path = std::path::Path::new(".").canonicalize()?; + + if !path_buf.starts_with(cur_path) { + return wry::http::ResponseBuilder::new() + .status(wry::http::status::StatusCode::FORBIDDEN) + .body(String::from("Forbidden").into_bytes()); + } + + if !path_buf.exists() { + return wry::http::ResponseBuilder::new() + .status(wry::http::status::StatusCode::NOT_FOUND) + .body(String::from("Not Found").into_bytes()); + } + + let mime = mime_guess::from_path(&path_buf).first_or_octet_stream(); + + // do not let path searching to go two layers beyond the caller level + let data = read(path_buf)?; + let meta = format!("{}", mime); + + wry::http::ResponseBuilder::new().mimetype(&meta).body(data) } }) .with_file_drop_handler(move |window, evet| { @@ -257,6 +286,41 @@ pub fn launch_with_props( // match _evt { UserWindowEvent::Update => desktop.try_load_ready_webviews(), + UserWindowEvent::DragWindow => { + // this loop just run once, because dioxus-desktop is unsupport multi-window. + for webview in desktop.webviews.values() { + let window = webview.window(); + // start to drag the window. + // if the drag_window have any err. we don't do anything. + let _ = window.drag_window(); + } + } + UserWindowEvent::CloseWindow => { + // close window + *control_flow = ControlFlow::Exit; + } + UserWindowEvent::Minimize(state) => { + // this loop just run once, because dioxus-desktop is unsupport multi-window. + for webview in desktop.webviews.values() { + let window = webview.window(); + // change window minimized state. + window.set_minimized(state); + } + } + UserWindowEvent::Maximize(state) => { + // this loop just run once, because dioxus-desktop is unsupport multi-window. + for webview in desktop.webviews.values() { + let window = webview.window(); + // change window maximized state. + window.set_maximized(state); + } + } + UserWindowEvent::FocusWindow => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_focus(); + } + } } } Event::MainEventsCleared => {} @@ -271,6 +335,11 @@ pub fn launch_with_props( pub enum UserWindowEvent { Update, + DragWindow, + CloseWindow, + FocusWindow, + Minimize(bool), + Maximize(bool), } pub struct DesktopController { @@ -297,6 +366,7 @@ impl DesktopController { let return_sender = sender.clone(); let proxy = evt.clone(); + let desktop_context_proxy = proxy.clone(); std::thread::spawn(move || { // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads let runtime = tokio::runtime::Builder::new_multi_thread() @@ -308,6 +378,10 @@ impl DesktopController { 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); + let edits = dom.rebuild(); edit_queue diff --git a/packages/hooks/CHANGELOG.md b/packages/hooks/CHANGELOG.md index 07e15ed64..253768068 100644 --- a/packages/hooks/CHANGELOG.md +++ b/packages/hooks/CHANGELOG.md @@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Bug Fixes + + - use_state + - exampels + +### Commit Statistics + + + + - 9 commits contributed to the release over the course of 21 calendar days. + - 3 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a)) + - Merge pull request #158 from DioxusLabs/jk/router-onchange ([`08988e1`](https://github.comgit//DioxusLabs/dioxus/commit/08988e1bfec35eadb30e50b5d677dbb36af91b9b)) + - use_state ([`c092bd4`](https://github.comgit//DioxusLabs/dioxus/commit/c092bd43edf1891c427722bc9ca04e11c9359069)) + - exampels ([`a8952a9`](https://github.comgit//DioxusLabs/dioxus/commit/a8952a9ee8d8831fa911cef4e7035293c3a9f88b)) + - Merge branch 'master' into jk/update-hooks ([`5c4bd08`](https://github.comgit//DioxusLabs/dioxus/commit/5c4bd0881bc10572440d9272ceb8ca774431c406)) + - modify usestate to be borrowed ([`58839f4`](https://github.comgit//DioxusLabs/dioxus/commit/58839f47bae3c49cadd4b41da9a5debebb1def99)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) +
+ ## v0.1.6 (2022-01-08) ### Documentation @@ -52,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 93 commits contributed to the release over the course of 358 calendar days. + - 94 commits contributed to the release over the course of 358 calendar days. - 82 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -63,6 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 375fef087..7d8e4d26a 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-hooks" -version = "0.1.6" +version = "0.1.7" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for a remote webview instance" @@ -12,4 +12,4 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../../packages/core", version ="^0.1.7"} +dioxus-core = { path = "../../packages/core", version = "^0.1.9" } diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 139e05245..cb4afd297 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -16,45 +16,25 @@ pub use usefuture::*; mod usesuspense; pub use usesuspense::*; -// #[macro_export] -// macro_rules! to_owned { -// ($($es:ident),+) => {$( -// #[allow(unused_mut)] -// let mut $es = $es.to_owned(); -// )*} -// } - -// /// Calls `for_async` on the series of paramters. -// /// -// /// If the type is Clone, then it will be cloned. However, if the type is not `clone` -// /// then it must have a `for_async` method for Rust to lower down into. -// /// -// /// See: how use_state implements `for_async` but *not* through the trait. -// #[macro_export] -// macro_rules! for_async { -// ($($es:ident),+) => {$( -// #[allow(unused_mut)] -// let mut $es = $es.for_async(); -// )*} -// } - -// /// This is a marker trait that uses decoherence. -// /// -// /// It is *not* meant for hooks to actually implement, but rather defer to their -// /// underlying implementation if they *don't* implement the trait. -// /// -// /// -// pub trait AsyncHook { -// type Output; -// fn for_async(self) -> Self::Output; -// } - -// impl AsyncHook for T -// where -// T: ToOwned, -// { -// type Output = T; -// fn for_async(self) -> Self::Output { -// self -// } -// } +#[macro_export] +/// A helper macro for using hooks in async environements. +/// +/// # Usage +/// +/// +/// ``` +/// 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/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 9b59601b5..a05e99d7d 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -33,7 +33,7 @@ impl ProvidedStateInner { /// This hook provides some relatively light ergonomics around shared state. /// /// It is not a substitute for a proper state management system, but it is capable enough to provide use_state - type -/// ergonimics in a pinch, with zero cost. +/// ergonomics in a pinch, with zero cost. /// /// # Example /// diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs new file mode 100644 index 000000000..7e8892be9 --- /dev/null +++ b/packages/hooks/src/usestate.rs @@ -0,0 +1,346 @@ +#![warn(clippy::pedantic)] + +use dioxus_core::prelude::*; +use std::{ + cell::{RefCell, RefMut}, + fmt::{Debug, Display}, + rc::Rc, +}; + +/// Store state between component renders. +/// +/// ## Dioxus equivalent of useState, designed for Rust +/// +/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and +/// modify state between component renders. When the state is updated, the component will re-render. +/// +/// +/// ```ignore +/// const Example: Component = |cx| { +/// let (count, set_count) = use_state(&cx, || 0); +/// +/// cx.render(rsx! { +/// div { +/// h1 { "Count: {count}" } +/// button { onclick: move |_| set_count(a - 1), "Increment" } +/// button { onclick: move |_| set_count(a + 1), "Decrement" } +/// } +/// )) +/// } +/// ``` +pub fn use_state<'a, T: 'static>( + cx: &'a ScopeState, + initial_state_fn: impl FnOnce() -> T, +) -> (&'a T, &'a UseState) { + let hook = cx.use_hook(move |_| { + let current_val = Rc::new(initial_state_fn()); + let update_callback = cx.schedule_update(); + let slot = Rc::new(RefCell::new(current_val.clone())); + let setter = Rc::new({ + crate::to_owned![update_callback, slot]; + move |new| { + { + let mut slot = slot.borrow_mut(); + + // if there's only one reference (weak or otherwise), we can just swap the values + // Typically happens when the state is set multiple times - we don't want to create a new Rc for each new value + if let Some(val) = Rc::get_mut(&mut slot) { + *val = new; + } else { + *slot = Rc::new(new); + } + } + update_callback(); + } + }); + + UseState { + current_val, + update_callback, + setter, + slot, + } + }); + + hook.current_val = hook.slot.borrow().clone(); + + (hook.current_val.as_ref(), hook) +} + +pub struct UseState { + pub(crate) current_val: Rc, + pub(crate) update_callback: Rc, + pub(crate) setter: Rc, + pub(crate) slot: Rc>>, +} + +impl UseState { + /// Get the current value of the state by cloning its container Rc. + /// + /// This is useful when you are dealing with state in async contexts but need + /// to know the current value. You are not given a reference to the state. + /// + /// # Examples + /// An async context might need to know the current value: + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (count, set_count) = use_state(&cx, || 0); + /// cx.spawn({ + /// let set_count = set_count.to_owned(); + /// async move { + /// let current = set_count.current(); + /// } + /// }) + /// } + /// ``` + #[must_use] + pub fn current(&self) -> Rc { + self.slot.borrow().clone() + } + + /// Get the `setter` function directly without the `UseState` wrapper. + /// + /// This is useful for passing the setter function to other components. + /// + /// However, for most cases, calling `to_owned` o`UseState`te is the + /// preferred way to get "anoth`set_state`tate handle. + /// + /// + /// # Examples + /// A component might require an `Rc` as an input to set a value. + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// rsx!{ + /// Component { + /// handler: set_val.setter() + /// } + /// } + /// } + /// ``` + #[must_use] + pub fn setter(&self) -> Rc { + self.setter.clone() + } + + /// Set the state to a new value, using the current state value as a reference. + /// + /// This is similar to passing a closure to React's `set_value` function. + /// + /// # Examples + /// + /// Basic usage: + /// ```rust + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// // to increment the value + /// set_value.modify(|v| v + 1); + /// + /// // usage in async + /// cx.spawn({ + /// let set_value = set_value.to_owned(); + /// async move { + /// set_value.modify(|v| v + 1); + /// } + /// }); + /// + /// # todo!() + /// } + /// ``` + pub fn modify(&self, f: impl FnOnce(&T) -> T) { + let new_val = { + let current = self.slot.borrow(); + f(current.as_ref()) + }; + (self.setter)(new_val); + } + + /// Get the value of the state when this handle was created. + /// + /// This method is useful when you want an `Rc` around the data to cheaply + /// pass it around your app. + /// + /// ## Warning + /// + /// This will return a stale value if used within async contexts. + /// + /// Try `current` to get the real current value of the state. + /// + /// ## Example + /// + /// ```rust, ignore + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// let as_rc = set_value.get(); + /// assert_eq!(as_rc.as_ref(), &0); + /// + /// # todo!() + /// } + /// ``` + #[must_use] + pub fn get(&self) -> &Rc { + &self.current_val + } + + /// Mark the component that create this [`UseState`] as dirty, forcing it to re-render. + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (count, set_count) = use_state(&cx, || 0); + /// cx.spawn({ + /// let set_count = set_count.to_owned(); + /// async move { + /// // for the component to re-render + /// set_count.needs_update(); + /// } + /// }) + /// } + /// ``` + pub fn needs_update(&self) { + (self.update_callback)(); + } +} + +impl UseState { + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// If you are comfortable dealing with `RefMut`, then you can use `make_mut` to get + /// the underlying slot. However, be careful with `RefMut` since you might panic + /// if the `RefCell` is left open. + /// + /// # Examples + /// + /// ``` + /// let (val, set_val) = use_state(&cx, || 0); + /// + /// set_val.with_mut(|v| *v = 1); + /// ``` + pub fn with_mut(&self, apply: impl FnOnce(&mut T)) { + let mut slot = self.slot.borrow_mut(); + let mut inner = slot.as_ref().to_owned(); + + apply(&mut inner); + + if let Some(new) = Rc::get_mut(&mut slot) { + *new = inner; + } else { + *slot = Rc::new(inner); + } + + self.needs_update(); + } + + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// # Warning + /// Be careful with `RefMut` since you might panic if the `RefCell` is left open! + /// + /// # Examples + /// + /// ``` + /// let (val, set_val) = use_state(&cx, || 0); + /// + /// set_val.with_mut(|v| *v = 1); + /// ``` + #[must_use] + pub fn make_mut(&self) -> RefMut { + let mut slot = self.slot.borrow_mut(); + + self.needs_update(); + + if Rc::strong_count(&*slot) > 0 { + *slot = Rc::new(slot.as_ref().to_owned()); + } + + RefMut::map(slot, |rc| Rc::get_mut(rc).expect("the hard count to be 0")) + } +} + +impl Clone for UseState { + fn clone(&self) -> Self { + UseState { + current_val: self.current_val.clone(), + update_callback: self.update_callback.clone(), + setter: self.setter.clone(), + slot: self.slot.clone(), + } + } +} + +impl<'a, T: 'static + Display> std::fmt::Display for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.current_val) + } +} + +impl PartialEq> for UseState { + fn eq(&self, other: &UseState) -> bool { + // some level of memoization for UseState + Rc::ptr_eq(&self.slot, &other.slot) + } +} + +impl Debug for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.current_val) + } +} + +impl<'a, T> std::ops::Deref for UseState { + type Target = Rc; + + fn deref(&self) -> &Self::Target { + &self.setter + } +} + +#[test] +fn api_makes_sense() { + #[allow(unused)] + fn app(cx: Scope) -> Element { + let (val, set_val) = use_state(&cx, || 0); + + set_val(0); + set_val.modify(|v| v + 1); + let real_current = set_val.current(); + + match val { + 10 => { + set_val(20); + set_val.modify(|v| v + 1); + } + 20 => {} + _ => { + println!("{real_current}"); + } + } + + cx.spawn({ + crate::to_owned![set_val]; + async move { + set_val.modify(|f| f + 1); + } + }); + + cx.render(LazyNodes::new(|f| f.static_text("asd"))) + } +} diff --git a/packages/hooks/src/usestate/handle.rs b/packages/hooks/src/usestate/handle.rs deleted file mode 100644 index 2638feaa8..000000000 --- a/packages/hooks/src/usestate/handle.rs +++ /dev/null @@ -1,215 +0,0 @@ -use super::owned::UseStateOwned; -use std::{ - cell::{Ref, RefMut}, - fmt::{Debug, Display}, - rc::Rc, -}; - -pub struct UseState<'a, T: 'static>(pub(crate) &'a UseStateOwned); - -impl Copy for UseState<'_, T> {} - -impl<'a, T: 'static> Clone for UseState<'a, T> { - fn clone(&self) -> Self { - UseState(self.0) - } -} - -impl Debug for UseState<'_, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.0.current_val) - } -} - -impl<'a, T: 'static> UseState<'a, T> { - /// Tell the Dioxus Scheduler that we need to be processed - pub fn needs_update(&self) { - if !self.0.update_scheuled.get() { - self.0.update_scheuled.set(true); - (self.0.update_callback)(); - } - } - - pub fn set(&self, new_val: T) { - *self.0.wip.borrow_mut() = Some(new_val); - self.needs_update(); - } - - pub fn get(&self) -> &'a T { - &self.0.current_val - } - - pub fn get_rc(&self) -> &'a Rc { - &self.0.current_val - } - - /// Get the current status of the work-in-progress data - pub fn get_wip(&self) -> Ref> { - self.0.wip.borrow() - } - - /// Get the current status of the work-in-progress data - pub fn get_wip_mut(&self) -> RefMut> { - self.0.wip.borrow_mut() - } - - pub fn classic(self) -> (&'a T, Rc) { - (&self.0.current_val, self.setter()) - } - - pub fn setter(&self) -> Rc { - let slot = self.0.wip.clone(); - let callback = self.0.update_callback.clone(); - Rc::new(move |new| { - callback(); - *slot.borrow_mut() = Some(new) - }) - } - - pub fn wtih(&self, f: impl FnOnce(&mut T)) { - let mut val = self.0.wip.borrow_mut(); - - if let Some(inner) = val.as_mut() { - f(inner); - } - } - - pub fn for_async(&self) -> UseStateOwned { - let UseStateOwned { - current_val, - wip, - update_callback, - update_scheuled, - } = self.0; - - UseStateOwned { - current_val: current_val.clone(), - wip: wip.clone(), - update_callback: update_callback.clone(), - update_scheuled: update_scheuled.clone(), - } - } -} - -impl<'a, T: 'static + ToOwned> UseState<'a, T> { - /// Gain mutable access to the new value via [`RefMut`]. - /// - /// If `modify` is called, then the component will re-render. - /// - /// This method is only available when the value is a `ToOwned` type. - /// - /// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value. - /// - /// To get a reference to the current value, use `.get()` - pub fn modify(self) -> RefMut<'a, T> { - // make sure we get processed - self.needs_update(); - - // Bring out the new value, cloning if it we need to - // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this - RefMut::map(self.0.wip.borrow_mut(), |slot| { - if slot.is_none() { - *slot = Some(self.0.current_val.as_ref().to_owned()); - } - slot.as_mut().unwrap() - }) - } - - pub fn inner(self) -> T { - self.0.current_val.as_ref().to_owned() - } -} - -impl<'a, T> std::ops::Deref for UseState<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.get() - } -} - -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.current_val) - } -} -impl<'a, V, T: PartialEq> PartialEq for UseState<'a, T> { - fn eq(&self, other: &V) -> bool { - self.get() == other - } -} -impl<'a, O, T: std::ops::Not + Copy> std::ops::Not for UseState<'a, T> { - type Output = O; - - fn not(self) -> Self::Output { - !*self.get() - } -} - -/* - -Convenience methods for UseState. - -Note! - -This is not comprehensive. -This is *just* meant to make common operations easier. -*/ - -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; - -impl<'a, T: Copy + Add> Add for UseState<'a, T> { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.0.current_val.add(rhs) - } -} -impl<'a, T: Copy + Add> AddAssign for UseState<'a, T> { - fn add_assign(&mut self, rhs: T) { - self.set(self.0.current_val.add(rhs)); - } -} - -/// Sub -impl<'a, T: Copy + Sub> Sub for UseState<'a, T> { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.0.current_val.sub(rhs) - } -} -impl<'a, T: Copy + Sub> SubAssign for UseState<'a, T> { - fn sub_assign(&mut self, rhs: T) { - self.set(self.0.current_val.sub(rhs)); - } -} - -/// MUL -impl<'a, T: Copy + Mul> Mul for UseState<'a, T> { - type Output = T; - - fn mul(self, rhs: T) -> Self::Output { - self.0.current_val.mul(rhs) - } -} -impl<'a, T: Copy + Mul> MulAssign for UseState<'a, T> { - fn mul_assign(&mut self, rhs: T) { - self.set(self.0.current_val.mul(rhs)); - } -} - -/// DIV -impl<'a, T: Copy + Div> Div for UseState<'a, T> { - type Output = T; - - fn div(self, rhs: T) -> Self::Output { - self.0.current_val.div(rhs) - } -} -impl<'a, T: Copy + Div> DivAssign for UseState<'a, T> { - fn div_assign(&mut self, rhs: T) { - self.set(self.0.current_val.div(rhs)); - } -} diff --git a/packages/hooks/src/usestate/mod.rs b/packages/hooks/src/usestate/mod.rs deleted file mode 100644 index cf125c2a4..000000000 --- a/packages/hooks/src/usestate/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -mod handle; -mod owned; -pub use handle::*; -pub use owned::*; - -use dioxus_core::prelude::*; -use std::{ - cell::{Cell, RefCell}, - rc::Rc, -}; - -/// Store state between component renders! -/// -/// ## Dioxus equivalent of useState, designed for Rust -/// -/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and -/// modify state between component renders. When the state is updated, the component will re-render. -/// -/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. -/// -/// [`use_state`] exposes a few helper methods to modify the underlying state: -/// - `.set(new)` allows you to override the "work in progress" value with a new value -/// - `.get_mut()` allows you to modify the WIP value -/// - `.get_wip()` allows you to access the WIP value -/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) -/// -/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations -/// will automatically be called on the WIP value. -/// -/// ## Combinators -/// -/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: -/// - `.classic()` and `.split()` convert the hook into the classic React-style hook -/// ```rust -/// let (state, set_state) = use_state(&cx, || 10).split() -/// ``` -/// -/// -/// Usage: -/// -/// ```ignore -/// const Example: Component = |cx| { -/// let counter = use_state(&cx, || 0); -/// -/// cx.render(rsx! { -/// div { -/// h1 { "Counter: {counter}" } -/// button { onclick: move |_| counter += 1, "Increment" } -/// button { onclick: move |_| counter -= 1, "Decrement" } -/// } -/// )) -/// } -/// ``` -pub fn use_state<'a, T: 'static>( - cx: &'a ScopeState, - initial_state_fn: impl FnOnce() -> T, -) -> UseState<'a, T> { - let hook = cx.use_hook(move |_| UseStateOwned { - current_val: Rc::new(initial_state_fn()), - update_callback: cx.schedule_update(), - wip: Rc::new(RefCell::new(None)), - update_scheuled: Cell::new(false), - }); - - hook.update_scheuled.set(false); - let mut new_val = hook.wip.borrow_mut(); - - if new_val.is_some() { - // if there's only one reference (weak or otherwise), we can just swap the values - if let Some(val) = Rc::get_mut(&mut hook.current_val) { - *val = new_val.take().unwrap(); - } else { - hook.current_val = Rc::new(new_val.take().unwrap()); - } - } - - UseState(hook) -} diff --git a/packages/hooks/src/usestate/owned.rs b/packages/hooks/src/usestate/owned.rs deleted file mode 100644 index a1a6bdb73..000000000 --- a/packages/hooks/src/usestate/owned.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::{ - cell::{Cell, Ref, RefCell, RefMut}, - fmt::{Debug, Display}, - rc::Rc, -}; -pub struct UseStateOwned { - // this will always be outdated - pub(crate) current_val: Rc, - pub(crate) wip: Rc>>, - pub(crate) update_callback: Rc, - pub(crate) update_scheuled: Cell, -} - -impl UseStateOwned { - pub fn get(&self) -> Ref> { - self.wip.borrow() - } - - pub fn set(&self, new_val: T) { - *self.wip.borrow_mut() = Some(new_val); - (self.update_callback)(); - } - - pub fn modify(&self) -> RefMut> { - (self.update_callback)(); - self.wip.borrow_mut() - } -} - -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; - -impl Debug for UseStateOwned { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.current_val) - } -} - -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseStateOwned { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.current_val) - } -} - -impl<'a, T: Copy + Add> Add for UseStateOwned { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.current_val.add(rhs) - } -} - -impl<'a, T: Copy + Add> AddAssign for UseStateOwned { - fn add_assign(&mut self, rhs: T) { - self.set(self.current_val.add(rhs)); - } -} - -/// Sub -impl<'a, T: Copy + Sub> Sub for UseStateOwned { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.current_val.sub(rhs) - } -} -impl<'a, T: Copy + Sub> SubAssign for UseStateOwned { - fn sub_assign(&mut self, rhs: T) { - self.set(self.current_val.sub(rhs)); - } -} - -/// MUL -impl<'a, T: Copy + Mul> Mul for UseStateOwned { - type Output = T; - - fn mul(self, rhs: T) -> Self::Output { - self.current_val.mul(rhs) - } -} -impl<'a, T: Copy + Mul> MulAssign for UseStateOwned { - fn mul_assign(&mut self, rhs: T) { - self.set(self.current_val.mul(rhs)); - } -} - -/// DIV -impl<'a, T: Copy + Div> Div for UseStateOwned { - type Output = T; - - fn div(self, rhs: T) -> Self::Output { - self.current_val.div(rhs) - } -} -impl<'a, T: Copy + Div> DivAssign for UseStateOwned { - fn div_assign(&mut self, rhs: T) { - self.set(self.current_val.div(rhs)); - } -} diff --git a/packages/html/CHANGELOG.md b/packages/html/CHANGELOG.md index 9648c0c8f..48c7f622c 100644 --- a/packages/html/CHANGELOG.md +++ b/packages/html/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Bug Fixes + + - rustfmt + +### Commit Statistics + + + + - 7 commits contributed to the release over the course of 19 calendar days. + - 1 commit where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - rustfmt ([`43e78d5`](https://github.comgit//DioxusLabs/dioxus/commit/43e78d56f72e23ada939f5688a6bd2fdd9b8292d)) + - Add gap and row_gap to style_trait_methods ([`5f4a724`](https://github.comgit//DioxusLabs/dioxus/commit/5f4a72446e63441628e18bb19fd9b2d9f65f9d52)) + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401)) + - feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9)) + - feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3)) +
+ ## v0.1.4 (2022-01-08) ### Documentation @@ -42,7 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 60 commits contributed to the release over the course of 184 calendar days. + - 61 commits contributed to the release over the course of 184 calendar days. - 52 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -53,8 +83,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** - - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - enable prevent_default everywhere ([`9dff700`](https://github.comgit//DioxusLabs/dioxus/commit/9dff700c220dd9e0da2ee028900e82fd24f9d0dd)) + - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) - memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c)) - new versions of everything ([`4ea5c99`](https://github.comgit//DioxusLabs/dioxus/commit/4ea5c990d72b1645724ab0a88ffea2baf28e2835)) diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 3c0f07b9b..293d4c465 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-html" -version = "0.1.4" +version = "0.1.6" authors = ["Jonathan Kelley"] edition = "2018" description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" @@ -11,7 +11,7 @@ documentation = "https://docs.rs/dioxus" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7"} +dioxus-core = { path = "../core", version = "^0.1.9" } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } diff --git a/packages/html/README.md b/packages/html/README.md index 46f1b2078..cbc03b0f2 100644 --- a/packages/html/README.md +++ b/packages/html/README.md @@ -42,7 +42,7 @@ The HTML namespace is defined mostly with macros. However, the expanded form wou struct base; impl DioxusElement for base { const TAG_NAME: &'static str = "base"; - const NAME_SPACE: Option<&'static str> = None; + const NAME_SPACE: Option<&'static str> = None; } impl base { #[inline] @@ -60,7 +60,7 @@ Because attributes are defined as methods on the unit struct, they guard the att ## How to extend it: -Whenever the rsx! macro is called, it relies on a module `dioxus_elements` to be in scope. When you enable the `html` feature in dioxus, this module gets imported in the prelude. However, you can extend this with your own set of custom elements by making your own `dioxus_elements` module and re-exporting the html namespace. +Whenever the rsx! macro is called, it relies on a module `dioxus_elements` to be in scope. When you enable the `html` feature in dioxus, this module gets imported in the prelude. However, you can extend this with your own set of custom elements by making your own `dioxus_elements` module and re-exporting the html namespace. ```rust mod dioxus_elements { @@ -68,13 +68,13 @@ mod dioxus_elements { struct my_element; impl DioxusElement for my_element { const TAG_NAME: &'static str = "base"; - const NAME_SPACE: Option<&'static str> = None; + const NAME_SPACE: Option<&'static str> = None; } } ``` ## Limitations: -- +- ## How to work around it: If an attribute in Dioxus is invalid (defined incorrectly) - first, make an issue - but then, you can work around it. The raw builder API is actually somewhat ergonomic to work with, and the NodeFactory type exposes a bunch of methods to make any type of tree - even invalid ones! So obviously, be careful, but there's basically anything you can do. @@ -87,19 +87,19 @@ cx.render(rsx!{ {LazyNodes::new(move |f| { f.raw_element( // tag name - "custom_element", + "custom_element", // attributes - &[f.attr("billy", format_args!("goat"))], + &[f.attr("billy", format_args!("goat"))], // listeners - &[f.listener(onclick(move |_| {}))], + &[f.listener(onclick(move |_| {}))], // children - &[cx.render(rsx!(div {} ))], + &[cx.render(rsx!(div {} ))], // key - None + None ) })} } diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index c40eabd31..1368f87e3 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -399,6 +399,9 @@ pub trait GlobalAttributes { /// Specify the font weight of the text. font_weight: "font-weight", + /// Sets gaps (gutters) between rows and columns. Shorthand for row_gap and column_gap. + gap: "gap", + /// Specify the height of an element. height: "height", @@ -531,6 +534,9 @@ pub trait GlobalAttributes { /// Specify the location of the right edge of the positioned element. right: "right", + /// Specifies the gap between the rows in a multi_column element. + row_gap: "row-gap", + /// Specifies the length of the tab character. tab_size: "tab-size", diff --git a/packages/liveview/README.md b/packages/liveview/README.md index 1d708eab2..00a8ce2d6 100644 --- a/packages/liveview/README.md +++ b/packages/liveview/README.md @@ -20,7 +20,7 @@ async fn main() -> tide::Result<()> { } ``` -Dioxus LiveView runs your Dioxus apps on the server +Dioxus LiveView runs your Dioxus apps on the server @@ -36,7 +36,7 @@ async fn main() { async fn order_shoes(mut req: WebsocketRequest) -> Response { let stream = req.upgrade(); - dioxus::liveview::launch(App, stream).await; + dioxus::liveview::launch(App, stream).await; } fn App(cx: Scope) -> Element { diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index 284068810..4486eb279 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Commit Statistics + + + + - 1 commit contributed to the release over the course of 7 calendar days. + - 0 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) +
+ ## v0.0.3 (2022-01-08) ### Documentation @@ -28,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 32 commits contributed to the release over the course of 151 calendar days. + - 33 commits contributed to the release over the course of 151 calendar days. - 21 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -39,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/mobile/Cargo.toml b/packages/mobile/Cargo.toml index 373814260..9da440343 100644 --- a/packages/mobile/Cargo.toml +++ b/packages/mobile/Cargo.toml @@ -12,4 +12,4 @@ license = "MIT/Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-desktop = { path = "../desktop", version ="^0.1.5"} +dioxus-desktop = { path = "../desktop", version = "^0.1.6" } diff --git a/packages/mobile/README.md b/packages/mobile/README.md index 5a41ffe60..aa3470761 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo mobile init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. diff --git a/packages/router/CHANGELOG.md b/packages/router/CHANGELOG.md index 7e257f854..86b157737 100644 --- a/packages/router/CHANGELOG.md +++ b/packages/router/CHANGELOG.md @@ -5,6 +5,54 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### New Features + + - enable use_router + - connect an onchange listener + - flatten props attrs + +### Bug Fixes + + - attach router listener to subscriber list + - use_route should subscribe to changes to the route + +### Commit Statistics + + + + - 18 commits contributed to the release over the course of 14 calendar days. + - 7 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a)) + - enable use_router ([`e24957f`](https://github.comgit//DioxusLabs/dioxus/commit/e24957fc191476184816fe5dee249f691170d4ae)) + - add docs to router UseRouteListener ([`79e0993`](https://github.comgit//DioxusLabs/dioxus/commit/79e09934aa685d03d6e0b323723bc1cd537d74d9)) + - Merge pull request #166 from DioxusLabs/jk/default-assets-desktop ([`ccbb955`](https://github.comgit//DioxusLabs/dioxus/commit/ccbb955b7b24bd4e1c5aa40e813a2872ae474a69)) + - rustfmt ([`9da46eb`](https://github.comgit//DioxusLabs/dioxus/commit/9da46eb7bc207997ca7779c58fcb2a9645dfa9d0)) + - Make log message in Link component trace level, not debug ([`72c6bb3`](https://github.comgit//DioxusLabs/dioxus/commit/72c6bb3d0b7253f084f7e3bcf55d458cb4adeedb)) + - attach router listener to subscriber list ([`5ee9d6c`](https://github.comgit//DioxusLabs/dioxus/commit/5ee9d6c4348a2f51adac827f715fb138918d1dc6)) + - connect an onchange listener ([`29ed7eb`](https://github.comgit//DioxusLabs/dioxus/commit/29ed7ebece26e9d53925af55f2f34a8fd8241405)) + - use_route should subscribe to changes to the route ([`a21e7d4`](https://github.comgit//DioxusLabs/dioxus/commit/a21e7d4dd168129da06f535f9dc4b1de724617cb)) + - Merge pull request #95 from DioxusLabs/jk/filedragindrop ([`ca0dd4a`](https://github.comgit//DioxusLabs/dioxus/commit/ca0dd4aa7192d483a195d420363f39d771f3e471)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - flatten props attrs ([`d237271`](https://github.comgit//DioxusLabs/dioxus/commit/d2372717bd01fcff50af0572360e3f763d4c869d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - Merge pull request #138 from mrxiaozhuox/master ([`8c7473d`](https://github.comgit//DioxusLabs/dioxus/commit/8c7473d1943dd133f388ec36116c9d8295861b97)) + - Add a warning when Link it called outside of a Router context ([`6408058`](https://github.comgit//DioxusLabs/dioxus/commit/64080588d02c54a1d380116cbecdd17de16d2392)) + - Merge pull request #133 from mrxiaozhuox/master ([`887f69d`](https://github.comgit//DioxusLabs/dioxus/commit/887f69d5b47bdcde4fe0eab094c0cd0de23e4f3f)) + - Fix handling of re-renders in the Router ([`81c094e`](https://github.comgit//DioxusLabs/dioxus/commit/81c094ed29fcdc5c6099492dd6ab09a59b79252c)) +
+ ## v0.1.0 (2022-01-08) ### Documentation @@ -33,7 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 42 commits contributed to the release over the course of 352 calendar days. + - 50 commits contributed to the release over the course of 352 calendar days. - 35 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -44,8 +92,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) + - More WIP router implementation ([`e06eac1`](https://github.comgit//DioxusLabs/dioxus/commit/e06eac1ce5c6bb0a6680482574d82b16a141a626)) + - Implement UseRoute segment method ([`c9408da`](https://github.comgit//DioxusLabs/dioxus/commit/c9408da7310423b1676fab6d41635f9a8000d89e)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) + - Implement router matching for path parameters ([`f8a7e1c`](https://github.comgit//DioxusLabs/dioxus/commit/f8a7e1cd8255fd7d0116384247e0e305bb73bf3d)) + - Commit WIP on router ([`3c6142f`](https://github.comgit//DioxusLabs/dioxus/commit/3c6142fb9d8b5f715adf0fb30c2f3534b9ecd923)) + - Add more trace messages to the RouterService code ([`3a5b417`](https://github.comgit//DioxusLabs/dioxus/commit/3a5b417ad1639a31e9aac9f41b05d8b3f074b128)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - Fix typo in RouterService struct's "registered_routes" field name ([`d367e0f`](https://github.comgit//DioxusLabs/dioxus/commit/d367e0f89f1b31484bde42efaf10c3531aed1d64)) + - Add title prop to Link ([`e22ba5b`](https://github.comgit//DioxusLabs/dioxus/commit/e22ba5b1e52fe00e08e0b6ac0abae29b6068b8f0)) - add prevent default attribute and upgrade router ([`427b126`](https://github.comgit//DioxusLabs/dioxus/commit/427b126bc17336d5d14d56eb7fddb8e07752495f)) - memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c)) - bump all versions ([`4f92ba4`](https://github.comgit//DioxusLabs/dioxus/commit/4f92ba41602d706449c1bddabd49829873ee72eb)) diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 032dbe3bf..1c6ed24d2 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-router" -version = "0.1.0" +version = "0.1.1" edition = "2018" description = "Dioxus VirtualDOM renderer for the web browser using websys" license = "MIT/Apache-2.0" @@ -11,9 +11,9 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7", default-features = false } -dioxus-html = { path = "../html", version ="^0.1.4", default-features = false } -dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"} +dioxus-core = { path = "../core", version = "^0.1.9", default-features = false } +dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } +dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } serde = "1" url = "2.2.2" diff --git a/packages/router/README.md b/packages/router/README.md index c32441888..6fbc652eb 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -5,7 +5,7 @@ DioxusRouter adds React-Router style routing to your Dioxus apps. Works in brows ```rust fn app() { cx.render(rsx! { - Routes { + Router { Route { to: "/", Component {} }, Route { to: "/blog", Blog {} }, Route { to: "/blog/:id", BlogPost {} }, @@ -16,7 +16,7 @@ fn app() { Then, in your route, you can choose to parse the Route any way you want through `use_route`. ```rust -let id: usize = use_route(&cx).path("id")?; +let id: usize = use_route(&cx).segment("id")?; let state: CustomState = use_route(&cx).parse()?; ``` diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 6a1b671ab..18e9fd92e 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -11,7 +11,7 @@ pub struct LinkProps<'a> { /// The url that gets pushed to the history stack /// - /// You can either put it your own inline method or just autoderive the route using `derive(Routable)` + /// You can either put in your own inline method or just autoderive the route using `derive(Routable)` /// /// ```rust, ignore /// @@ -41,7 +41,7 @@ pub struct LinkProps<'a> { } pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - log::debug!("render Link to {}", cx.props.to); + // log::trace!("render Link to {}", cx.props.to); if let Some(service) = cx.consume_context::() { return cx.render(rsx! { a { diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index 129953eea..e4dd2f9c8 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -30,7 +30,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { Some(ctx) => ctx.total_route.to_string(), None => cx.props.to.to_string(), }; - log::trace!("total route for {} is {}", cx.props.to, total_route); + // log::trace!("total route for {} is {}", cx.props.to, total_route); // provide our route context let route_context = cx.provide_context(RouteContext { @@ -48,7 +48,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { Some(RouteInner {}) }); - log::trace!("Checking route {}", cx.props.to); + // log::trace!("Checking route {}", cx.props.to); if router_root.should_render(cx.scope_id()) { cx.render(rsx!(&cx.props.children)) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 3d9f0c83a..bf23645f5 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -11,17 +11,27 @@ use crate::RouterService; pub struct RouterProps<'a> { children: Element<'a>, - #[props(default, strip_option)] - onchange: Option<&'a dyn Fn(&'a str)>, + #[props(default)] + onchange: EventHandler<'a, String>, } #[allow(non_snake_case)] pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { - cx.use_hook(|_| { + log::debug!("running router {:?}", cx.scope_id()); + let svc = cx.use_hook(|_| { let update = cx.schedule_update_any(); cx.provide_context(RouterService::new(update, cx.scope_id())) }); + let any_pending = svc.pending_events.borrow().len() > 0; + svc.pending_events.borrow_mut().clear(); + + if any_pending { + let location = svc.current_location(); + let path = location.path(); + cx.props.onchange.call(path.to_string()); + } + cx.render(rsx!( div { &cx.props.children } )) diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 97dd6c048..31a157a8f 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,4 +1,4 @@ -use dioxus_core::ScopeState; +use dioxus_core::{ScopeId, ScopeState}; use gloo::history::{HistoryResult, Location}; use serde::de::DeserializeOwned; use std::{rc::Rc, str::FromStr}; @@ -8,6 +8,7 @@ use crate::RouterService; /// This struct provides is a wrapper around the internal router /// implementation, with methods for getting information about the current /// route. +#[derive(Clone)] pub struct UseRoute { router: Rc, } @@ -74,10 +75,40 @@ impl UseRoute { /// This hook provides access to information about the current location in the /// context of a [`Router`]. If this function is called outside of a `Router` /// component it will panic. -pub fn use_route(cx: &ScopeState) -> UseRoute { - let router = cx - .consume_context::() - .expect("Cannot call use_route outside the scope of a Router component") - .clone(); - UseRoute { router } +pub fn use_route(cx: &ScopeState) -> &UseRoute { + &cx.use_hook(|_| { + let router = cx + .consume_context::() + .expect("Cannot call use_route outside the scope of a Router component"); + + router.subscribe_onchange(cx.scope_id()); + + UseRouteListener { + router: UseRoute { router }, + scope: cx.scope_id(), + } + }) + .router +} + +// The entire purpose of this struct is to unubscribe this component when it is unmounted. +// The UseRoute can be cloned into async contexts, so we can't rely on its drop to unubscribe. +// Instead, we hide the drop implementation on this private type exclusive to the hook, +// and reveal our cached version of UseRoute to the component. +struct UseRouteListener { + router: UseRoute, + scope: ScopeId, +} +impl Drop for UseRouteListener { + fn drop(&mut self) { + self.router.router.unsubscribe_onchange(self.scope) + } +} + +/// This hook provides access to the `RouterService` for the app. +pub fn use_router(cx: &ScopeState) -> &RouterService { + cx.use_hook(|_| { + cx.consume_context::() + .expect("Cannot call use_route outside the scope of a Router component") + }) } diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index b6ffb5971..309cef905 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -8,22 +8,9 @@ //! //! ```rust //! fn app(cx: Scope) -> Element { +//! //! } -//! -//! -//! -//! -//! //! ``` -//! -//! -//! -//! -//! -//! -//! -//! -//! mod hooks { mod use_route; diff --git a/packages/router/src/platform/mod.rs b/packages/router/src/platform/mod.rs index 7afc1316a..ec59a4703 100644 --- a/packages/router/src/platform/mod.rs +++ b/packages/router/src/platform/mod.rs @@ -1,9 +1,4 @@ pub trait RouterProvider { fn get_current_route(&self) -> String; - fn subscribe_to_route_changes(&self, callback: Box); -} - -enum RouteChange { - LinkTo(String), - Back(String), + fn listen(&self, callback: Box); } diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 00642414f..a4ab4dc0d 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,21 +1,33 @@ use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ cell::{Cell, Ref, RefCell}, - collections::HashMap, + collections::{HashMap, HashSet}, rc::Rc, }; use dioxus_core::ScopeId; +use crate::platform::RouterProvider; + pub struct RouterService { pub(crate) regen_route: Rc, - history: Rc>, + pub(crate) pending_events: Rc>>, slots: Rc>>, + onchange_listeners: Rc>>, root_found: Rc>>, cur_path_params: Rc>>, + + // history: Rc, + history: Rc>, listener: HistoryListener, } +pub enum RouteEvent { + Change, + Pop, + Push, +} + enum RouteSlot { Routes { // the partial route @@ -35,58 +47,66 @@ impl RouterService { let location = history.location(); let path = location.path(); + let onchange_listeners = Rc::new(RefCell::new(HashSet::new())); let slots: Rc>> = Default::default(); - - let _slots = slots.clone(); - + let pending_events: Rc>> = Default::default(); let root_found = Rc::new(Cell::new(None)); - let regen = regen_route.clone(); - let _root_found = root_found.clone(); - let listener = history.listen(move || { - _root_found.set(None); - // checking if the route is valid is cheap, so we do it - for (slot, root) in _slots.borrow_mut().iter().rev() { - log::trace!("regenerating slot {:?} for root '{}'", slot, root); - regen(*slot); + + let listener = history.listen({ + let pending_events = pending_events.clone(); + let regen_route = regen_route.clone(); + let root_found = root_found.clone(); + let slots = slots.clone(); + let onchange_listeners = onchange_listeners.clone(); + move || { + root_found.set(None); + // checking if the route is valid is cheap, so we do it + for (slot, root) in slots.borrow_mut().iter().rev() { + regen_route(*slot); + } + + for listener in onchange_listeners.borrow_mut().iter() { + regen_route(*listener); + } + + // also regenerate the root + regen_route(root_scope); + + pending_events.borrow_mut().push(RouteEvent::Change) } }); Self { + listener, root_found, history: Rc::new(RefCell::new(history)), regen_route, slots, + pending_events, + onchange_listeners, cur_path_params: Rc::new(RefCell::new(HashMap::new())), - listener, } } pub fn push_route(&self, route: &str) { - log::trace!("Pushing route: {}", route); self.history.borrow_mut().push(route); } pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { let clean = clean_route(route); - log::trace!("Registered route '{}' with scope id {:?}", clean, scope); self.slots.borrow_mut().push((scope, clean)); } pub fn should_render(&self, scope: ScopeId) -> bool { - log::trace!("Should render scope id {:?}?", scope); if let Some(root_id) = self.root_found.get() { - log::trace!(" we already found a root with scope id {:?}", root_id); if root_id == scope { - log::trace!(" yes - it's a match"); return true; } - log::trace!(" no - it's not a match"); return false; } let location = self.history.borrow().location(); let path = location.path(); - log::trace!(" current path is '{}'", path); let roots = self.slots.borrow(); @@ -95,23 +115,15 @@ impl RouterService { // fallback logic match root { Some((id, route)) => { - log::trace!( - " matched given scope id {:?} with route root '{}'", - scope, - route, - ); if let Some(params) = route_matches_path(route, path) { - log::trace!(" and it matches the current path '{}'", path); self.root_found.set(Some(*id)); *self.cur_path_params.borrow_mut() = params; true } else { if route == "" { - log::trace!(" and the route is the root, so we will use that without a better match"); self.root_found.set(Some(*id)); true } else { - log::trace!(" and the route '{}' is not the root nor does it match the current path", route); false } } @@ -127,6 +139,14 @@ impl RouterService { pub fn current_path_params(&self) -> Ref> { self.cur_path_params.borrow() } + + pub fn subscribe_onchange(&self, id: ScopeId) { + self.onchange_listeners.borrow_mut().insert(id); + } + + pub fn unsubscribe_onchange(&self, id: ScopeId) { + self.onchange_listeners.borrow_mut().remove(&id); + } } fn clean_route(route: String) -> String { @@ -147,36 +167,20 @@ fn route_matches_path(route: &str, path: &str) -> Option let route_pieces = route.split('/').collect::>(); let path_pieces = clean_path(path).split('/').collect::>(); - log::trace!( - " checking route pieces {:?} vs path pieces {:?}", - route_pieces, - path_pieces, - ); - if route_pieces.len() != path_pieces.len() { - log::trace!(" the routes are different lengths"); return None; } let mut matches = HashMap::new(); for (i, r) in route_pieces.iter().enumerate() { - log::trace!(" checking route piece '{}' vs path", r); // If this is a parameter then it matches as long as there's // _any_thing in that spot in the path. if r.starts_with(':') { - log::trace!( - " route piece '{}' starts with a colon so it matches anything", - r, - ); let param = &r[1..]; matches.insert(param.to_string(), path_pieces[i].to_string()); continue; } - log::trace!( - " route piece '{}' must be an exact match for path piece '{}'", - r, - path_pieces[i], - ); + if path_pieces[i] != *r { return None; } diff --git a/packages/ssr/CHANGELOG.md b/packages/ssr/CHANGELOG.md index 73cee67b0..43e1960c1 100644 --- a/packages/ssr/CHANGELOG.md +++ b/packages/ssr/CHANGELOG.md @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Bug Fixes + + - ssr respects bool attrs + +### Commit Statistics + + + + - 5 commits contributed to the release over the course of 21 calendar days. + - 1 commit where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - ssr respects bool attrs ([`255f58a`](https://github.comgit//DioxusLabs/dioxus/commit/255f58af63e002339b02bf91c5b85cc5ec917428)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) +
+ ## v0.1.2 (2022-01-08) ### Documentation @@ -47,7 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 84 commits contributed to the release over the course of 357 calendar days. + - 85 commits contributed to the release over the course of 357 calendar days. - 75 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -58,6 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 754fbbb78..02f51f453 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-ssr" -version = "0.1.2" +version = "0.1.3" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus render-to-string" @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7", features = ["serialize"] } +dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } [dev-dependencies] diff --git a/packages/ssr/README.md b/packages/ssr/README.md index d6c199fc8..7a7f1c4be 100644 --- a/packages/ssr/README.md +++ b/packages/ssr/README.md @@ -52,7 +52,7 @@ let content = dioxus::ssr::render_vdom(&dom); ``` ## Configuring output -It's possible to configure the output of the generated HTML. +It's possible to configure the output of the generated HTML. ```rust, ignore let content = dioxus::ssr::render_vdom(&dom, |config| config.pretty(true).prerender(true)); @@ -82,7 +82,7 @@ buf.write_fmt!(format_args!("{}", args)); -## Usage in pre-rendering +## Usage in pre-rendering This crate is particularly useful in pre-generating pages server-side and then selectively loading dioxus client-side to pick up the reactive elements. diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index b81747e95..3c27f3f94 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -5,6 +5,60 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Documentation + + - fix web doc example and use &mut for builders everywhere + +### New Features + + - add panic hook by default + - remove dioxus id on non-event elements + - wire up both desktop and web + - bool attr white list + +### Bug Fixes + + - webconfig should take &mut self + - format code + - ssr + hydration event listeners + - add exclusion list + +### Commit Statistics + + + + - 17 commits contributed to the release over the course of 21 calendar days. + - 10 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - webconfig should take &mut self ([`ae676d9`](https://github.comgit//DioxusLabs/dioxus/commit/ae676d9d816fcbf88d110157124443a5b3aef9eb)) + - fix web doc example and use &mut for builders everywhere ([`a239d2b`](https://github.comgit//DioxusLabs/dioxus/commit/a239d2ba6ac7f1f3d09de16c022ce8ca52cf0f63)) + - add panic hook by default ([`1406c90`](https://github.comgit//DioxusLabs/dioxus/commit/1406c9020bb3102635737dc2d13e5b3171499a18)) + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401)) + - remove dioxus id on non-event elements ([`95e93ed`](https://github.comgit//DioxusLabs/dioxus/commit/95e93ed0bcf6c69990f4cf3c6448b2bf5da96c36)) + - feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9)) + - feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3)) + - Merge pull request #101 from alexkirsz/ci ([`29bf424`](https://github.comgit//DioxusLabs/dioxus/commit/29bf424b0976b95ff645bb128d0e758cf0186614)) + - Merge pull request #139 from DioxusLabs/jk/provide-context-any ([`70f2ef4`](https://github.comgit//DioxusLabs/dioxus/commit/70f2ef43db5b6737bd9bcbfc1aa21c834ce4b395)) + - Merge branch 'master' into jk/unify ([`824defa`](https://github.comgit//DioxusLabs/dioxus/commit/824defa2dbcc16d66588b3976699d89b65a8a068)) + - wire up both desktop and web ([`05331dd`](https://github.comgit//DioxusLabs/dioxus/commit/05331ddd8033f6997d4916179b62f4d62f832988)) + - format code ([`9256161`](https://github.comgit//DioxusLabs/dioxus/commit/92561612c727e73356d7d36e16af39aacf02a56d)) + - ssr + hydration event listeners ([`c1a5d4e`](https://github.comgit//DioxusLabs/dioxus/commit/c1a5d4e11f5277e6ee644f670dc6fdd68f2202f1)) + - web now links against the js interprter code ([`10db6ad`](https://github.comgit//DioxusLabs/dioxus/commit/10db6ad65bd23dabf60d0afbe6ff82cda8220a8b)) + - bool attr white list ([`8f4aa84`](https://github.comgit//DioxusLabs/dioxus/commit/8f4aa84f1a4f2443b34d81ee42490564e168de53)) + - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) +
+ ## v0.0.4 (2022-01-08) ### Documentation @@ -84,7 +138,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 203 commits contributed to the release over the course of 358 calendar days. - - 190 commits where understood as [conventional](https://www.conventionalcommits.org). + - 189 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages ### Commit Details @@ -94,10 +148,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) + - handle bool attrs properly ([`8d685f4`](https://github.comgit//DioxusLabs/dioxus/commit/8d685f40b7e0ef6521c60310d8687291e9b9c48a)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) - - handle bool attrs properly ([`8d685f4`](https://github.comgit//DioxusLabs/dioxus/commit/8d685f40b7e0ef6521c60310d8687291e9b9c48a)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) - Merge pull request #80 from DioxusLabs/jk/router2dotoh ([`cdc2d8e`](https://github.comgit//DioxusLabs/dioxus/commit/cdc2d8ec6d123245c2ea5f6d10af02b6a6833994)) - memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c)) diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 6acdb5366..7e51d3e64 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-web" -version = "0.0.4" +version = "0.0.5" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for the web browser using websys" @@ -11,8 +11,8 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version = "^0.1.7" } -dioxus-html = { path = "../html", version = "^0.1.4" } +dioxus-core = { path = "../core", version = "^0.1.9" } +dioxus-html = { path = "../html", version = "^0.1.6" } js-sys = "0.3" wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } lazy_static = "1.4.0" @@ -20,7 +20,7 @@ wasm-bindgen-futures = "0.4.20" log = { version = "0.4.14", features = ["release_max_level_off"] } fxhash = "0.2.1" wasm-logger = "0.2.0" -console_error_panic_hook = "0.1.6" +console_error_panic_hook = { version = "0.1.7", optional = true } wasm-bindgen-test = "0.3.21" once_cell = "1.8" async-channel = "1.6.1" @@ -70,22 +70,11 @@ features = [ "IdleDeadline", ] - -# [lib] -# crate-type = ["cdylib", "rlib"] +[features] +default = ["panic_hook"] +panic_hook = ["console_error_panic_hook"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } wasm-bindgen-test = "0.3.28" dioxus-ssr = { path = "../ssr" } -# im-rc = "15.0.0" -# separator = "0.4.1" -# uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } -# serde = { version = "1.0.126", features = ["derive"] } -# reqwest = { version = "0.11", features = ["json"] } -# dioxus-hooks = { path = "../hooks" } -# rand = { version = "0.8.4", features = ["small_rng"] } - -# [dev-dependencies.getrandom] -# version = "0.2" -# features = ["js"] diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index 9b0c2ebfb..fa96e3230 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -2,7 +2,7 @@ use js_sys::Function; use wasm_bindgen::prelude::*; use web_sys::{Element, Node}; -#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")] +#[wasm_bindgen(module = "/src/interpreter.js")] extern "C" { pub type Interpreter; diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index cc5d18bf7..591868c34 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -30,7 +30,7 @@ impl WebConfig { /// work and suspended nodes. /// /// Dioxus will load up all the elements with the `dio_el` data attribute into memory when the page is loaded. - pub fn hydrate(mut self, f: bool) -> Self { + pub fn hydrate(&mut self, f: bool) -> &mut Self { self.hydrate = f; self } @@ -38,7 +38,7 @@ impl WebConfig { /// 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 rootname(mut self, name: impl Into) -> Self { + pub fn rootname(&mut self, name: impl Into) -> &mut Self { self.rootname = name.into(); self } @@ -46,7 +46,7 @@ impl WebConfig { /// 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_string_cache(mut self, cache: Vec) -> Self { + pub fn with_string_cache(&mut self, cache: Vec) -> &mut Self { self.cached_strings = cache; self } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index b60f2485d..112154938 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -28,17 +28,54 @@ impl WebsysDom { pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { // eventually, we just want to let the interpreter do all the work of decoding events into our event type let callback: Box = Box::new(move |event: &web_sys::Event| { - if let Ok(synthetic_event) = decode_trigger(event) { + let mut target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + + let typ = event.type_(); + + let decoded: anyhow::Result = loop { + match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { + Some(Ok(id)) => { + break Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: Some(ElementId(id)), + scope_id: None, + priority: dioxus_core::EventPriority::Medium, + }); + } + Some(Err(e)) => { + break Err(e.into()); + } + None => { + // walk the tree upwards until we actually find an event target + if let Some(parent) = target.parent_element() { + target = parent; + } else { + break Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: None, + scope_id: None, + priority: dioxus_core::EventPriority::Low, + }); + } + } + } + }; + + if let Ok(synthetic_event) = decoded { // Try to prevent default if the attribute is set - if let Some(target) = event.target() { - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); } } } @@ -275,47 +312,6 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc anyhow::Result { - let mut target = event - .target() - .expect("missing target") - .dyn_into::() - .expect("not a valid element"); - - let typ = event.type_(); - - loop { - match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { - Some(Ok(id)) => { - return Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: Some(ElementId(id)), - scope_id: None, - priority: dioxus_core::EventPriority::Medium, - }); - } - Some(Err(e)) => return Err(e.into()), - None => { - // walk the tree upwards until we actually find an event target - if let Some(parent) = target.parent_element() { - target = parent; - } else { - return Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: None, - scope_id: None, - priority: dioxus_core::EventPriority::Low, - }); - } - } - } - } -} - pub(crate) fn load_document() -> Document { web_sys::window() .expect("should have access to the Window") diff --git a/packages/web/src/interpreter.js b/packages/web/src/interpreter.js new file mode 100644 index 000000000..ea50ec3d3 --- /dev/null +++ b/packages/web/src/interpreter.js @@ -0,0 +1,499 @@ +export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +export class Interpreter { + root; + stack; + listeners; + handlers; + lastNodeWasText; + nodes; + constructor(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + top() { + return this.stack[this.stack.length - 1]; + } + pop() { + return this.stack.pop(); + } + PushRoot(root) { + const node = this.nodes[root]; + this.stack.push(node); + } + AppendChildren(many) { + let root = this.stack[this.stack.length - (1 + many)]; + let to_add = this.stack.splice(this.stack.length - many); + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + } + ReplaceWith(root_id, m) { + let root = this.nodes[root_id]; + let els = this.stack.splice(this.stack.length - m); + root.replaceWith(...els); + } + InsertAfter(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + InsertBefore(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + Remove(root) { + let node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + } + CreateTextNode(text, root) { + // todo: make it so the types are okay + const node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + } + CreateElement(tag, root) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + this.nodes[root] = el; + this.stack.push(el); + } + CreateElementNs(tag, root, ns) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + CreatePlaceholder(root) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + NewEventListener(event_name, root, handler) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + else { + this.listeners[event_name]++; + } + } + RemoveEventListener(root, event_name) { + const element = this.nodes[root]; + element.removeAttribute(`data-dioxus-id`); + this.listeners[event_name]--; + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; + } + } + SetText(root, text) { + this.nodes[root].textContent = text; + } + SetAttribute(root, field, value, ns) { + const name = field; + const node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } + else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } + else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } + else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root, name) { + const node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + } + handleEdits(edits) { + this.stack.push(this.root); + for (let edit of edits) { + this.handleEdit(edit); + } + } + handleEdit(edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event) => { + let target = event.target; + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + let contents = serialize_event(event); + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + if (event.type == "submit") { + event.preventDefault(); + } + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + if (realId == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} +function serialize_event(event) { + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; + } + case "compositionend": + case "compositionstart": + case "compositionupdate": { + let { data } = event; + return { + data, + }; + } + case "keydown": + case "keypress": + case "keyup": { + let { charCode, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location, + repeat: repeat, + which: which, + locale: "locale", + }; + } + case "focus": + case "blur": { + return {}; + } + case "change": { + let target = event.target; + let value; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } + else { + value = target.value ?? target.textContent; + } + return { + value: value, + }; + } + case "input": + case "invalid": + case "reset": + case "submit": { + let target = event.target; + let value = target.value ?? target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, pointerId, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, pointerType, isPrimary, } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary, + }; + } + case "select": { + return {}; + } + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + const { altKey, ctrlKey, metaKey, shiftKey, } = event; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, + }; + } + case "scroll": { + return {}; + } + case "wheel": { + const { deltaX, deltaY, deltaZ, deltaMode, } = event; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + const { animationName, elapsedTime, pseudoElement, } = event; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + const { propertyName, elapsedTime, pseudoElement, } = event; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": { + return {}; + } + case "toggle": { + return {}; + } + default: { + return {}; + } + } +} +const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, +}; diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 8c0c0c3b4..f4266a7c8 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -92,6 +92,29 @@ pub fn launch(root_component: Component) { launch_with_props(root_component, (), |c| c); } +/// Launch your app and run the event loop, with configuration. +/// +/// This function will start your web app on the main web thread. +/// +/// You can configure the WebView window with a configuration closure +/// +/// ```rust +/// use dioxus::prelude::*; +/// +/// fn main() { +/// dioxus_web::launch_with_props(App, |config| config.pre_render(true)); +/// } +/// +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello world!"} +/// }) +/// } +/// ``` +pub fn launch_cfg(root: Component, config_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig) { + launch_with_props(root, (), config_builder) +} + /// Launches the VirtualDOM from the specified component function and props. /// /// This method will block the thread with `spawn_local` @@ -100,7 +123,11 @@ pub fn launch(root_component: Component) { /// /// ```rust, ignore /// fn main() { -/// dioxus_web::launch_with_props(App, RootProps { name: String::from("joe") }); +/// dioxus_web::launch_with_props( +/// App, +/// RootProps { name: String::from("joe") }, +/// |config| config +/// ); /// } /// /// #[derive(ParitalEq, Props)] @@ -112,15 +139,19 @@ pub fn launch(root_component: Component) { /// rsx!(cx, div {"hello {cx.props.name}"}) /// } /// ``` -pub fn launch_with_props( +pub fn launch_with_props( root_component: Component, root_properties: T, - configuration_builder: F, + configuration_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig, ) where T: Send + 'static, - F: FnOnce(WebConfig) -> WebConfig, { - let config = configuration_builder(WebConfig::default()); + if cfg!(feature = "panic_hook") { + console_error_panic_hook::set_once(); + } + + let mut config = WebConfig::default(); + configuration_builder(&mut config); wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config)); } diff --git a/src/lib.rs b/src/lib.rs index c0a984abb..d7b479a07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ //! //! # Resources //! -//! This overview is provides a brief introduction to Dioxus. For a more in-depth guide, make sure to check out: +//! This overview provides a brief introduction to Dioxus. For a more in-depth guide, make sure to check out: //! - [Getting Started](https://dioxuslabs.com/getting-started) //! - [Book](https://dioxuslabs.com/book) //! - [Reference](https://dioxuslabs.com/reference) @@ -53,7 +53,7 @@ //! //! ## Elements & your first component //! -//! To assemble UI trees with Diouxs, you need to use the `render` function on +//! To assemble UI trees with Dioxus, you need to use the `render` function on //! something called `LazyNodes`. To produce `LazyNodes`, you can use the `rsx!` //! macro or the NodeFactory API. For the most part, you want to use the `rsx!` //! macro. @@ -74,7 +74,7 @@ //! ) //! ``` //! -//! The rsx macro accepts attributes in "struct form" and then will parse the rest +//! The `rsx!` macro accepts attributes in "struct form" and will parse the rest //! of the body as child elements and rust expressions. Any rust expression that //! implements `IntoIterator` will be parsed as a child. //! @@ -87,7 +87,7 @@ //! //! ``` //! -//! Used within components, the rsx! macro must be rendered into an `Element` with +//! Used within components, the `rsx!` macro must be rendered into an `Element` with //! the `render` function on Scope. //! //! If we want to omit the boilerplate of `cx.render`, we can simply pass in @@ -104,7 +104,7 @@ //! } //! ``` //! -//! Putting everything together, we can write a simple component that a list of +//! Putting everything together, we can write a simple component that renders a list of //! elements: //! //! ```rust, ignore @@ -146,8 +146,8 @@ //! } //! ``` //! -//! Our `Header` component takes in a `title` and a `color` property, which we -//! delcare on an explicit `HeaderProps` struct. +//! Our `Header` component takes a `title` and a `color` property, which we +//! declare on an explicit `HeaderProps` struct. //! //! ```rust, ignore //! // The `Props` derive macro lets us add additional functionality to how props are interpreted. @@ -203,8 +203,8 @@ //! } //! ``` //! -//! Components that beging with an uppercase letter may be called through -//! traditional curly-brace syntax like so: +//! Components that begin with an uppercase letter may be called with +//! the traditional (for React) curly-brace syntax like so: //! //! ```rust, ignore //! rsx!( @@ -224,7 +224,7 @@ //! ## Hooks //! //! While components are reusable forms of UI elements, hooks are reusable forms -//! of logic. Hooks provide us a way of retrieving state from `Scope` and using +//! of logic. Hooks provide us a way of retrieving state from the `Scope` and using //! it to render UI elements. //! //! By convention, all hooks are functions that should start with `use_`. We can @@ -245,7 +245,7 @@ //! - Functions with "use_" should not be called in loops or conditionals //! //! In a sense, hooks let us add a field of state to our component without declaring -//! an explicit struct. However, this means we need to "load" the struct in the right +//! an explicit state struct. However, this means we need to "load" the struct in the right //! order. If that order is wrong, then the hook will pick the wrong state and panic. //! //! Most hooks you'll write are simply composition of other hooks: @@ -256,8 +256,8 @@ //! users.get(&id).map(|user| user.logged_in).ok_or(false) //! } //! ``` -//! -//! To create entirely new foundational hooks, we can use the `use_hook` method on ScopeState. +//! +//! To create entirely new foundational hooks, we can use the `use_hook` method on `ScopeState`. //! //! ```rust, ignore //! fn use_mut_string(cx: &ScopeState) -> &mut String { @@ -316,9 +316,9 @@ //! //! Alternatives to Dioxus include: //! - Yew: supports function components and web, but no SSR, borrowed data, or bump allocation. Rather slow at times. -//! - Percy: supports function components, web, ssr, but lacks in state management +//! - Percy: supports function components, web, ssr, but lacks state management //! - Sycamore: supports function components, web, ssr, but closer to SolidJS than React -//! - MoonZoom/Seed: opionated in the Elm model (message, update) - no hooks +//! - MoonZoom/Seed: opinionated frameworks based on the Elm model (message, update) - no hooks //! //! We've put a lot of work into making Dioxus ergonomic and *familiar*. //! Our target audience is TypeSrcipt developers looking to switch to Rust for the web - so we need to be comparabale to React.