Merge branch 'master' into jk/docs
This commit is contained in:
commit
0592cdb135
101 files changed
+3845
-1983
No files matched your search
@@ -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
|
||||||
+20
-20
@@ -15,7 +15,7 @@ jobs:
|
|||||||
override: true
|
override: true
|
||||||
- uses: Swatinem/rust-cache@v1
|
- uses: Swatinem/rust-cache@v1
|
||||||
- run: sudo apt-get update
|
- run: sudo apt-get update
|
||||||
- run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev libappindicator3-dev
|
- run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev
|
||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: check
|
command: check
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
override: true
|
override: true
|
||||||
- uses: Swatinem/rust-cache@v1
|
- uses: Swatinem/rust-cache@v1
|
||||||
- run: sudo apt-get update
|
- run: sudo apt-get update
|
||||||
- run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev libappindicator3-dev
|
- run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev
|
||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
@@ -55,21 +55,21 @@ jobs:
|
|||||||
command: fmt
|
command: fmt
|
||||||
args: --all -- --check
|
args: --all -- --check
|
||||||
|
|
||||||
# clippy:
|
clippy:
|
||||||
# name: Clippy
|
name: Clippy
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# steps:
|
steps:
|
||||||
# - uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
# - uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
# with:
|
with:
|
||||||
# profile: minimal
|
profile: minimal
|
||||||
# toolchain: stable
|
toolchain: stable
|
||||||
# override: true
|
override: true
|
||||||
# - uses: Swatinem/rust-cache@v1
|
- uses: Swatinem/rust-cache@v1
|
||||||
# - run: rustup component add clippy
|
- run: sudo apt-get update
|
||||||
# - uses: actions-rs/cargo@v1
|
- run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev
|
||||||
# with:
|
- run: rustup component add clippy
|
||||||
# command: clippy
|
- uses: actions-rs/cargo@v1
|
||||||
# args: -- -D warnings
|
with:
|
||||||
#
|
command: clippy
|
||||||
# sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev libappindicator3-dev
|
args: -- -D warnings
|
||||||
@@ -35,11 +35,12 @@
|
|||||||
</a>
|
</a>
|
||||||
<!-- Discord -->
|
<!-- Discord -->
|
||||||
<a href="https://discord.gg/XgGxMSkvUM">
|
<a href="https://discord.gg/XgGxMSkvUM">
|
||||||
<img src="https://badgen.net/discord/members/XgGxMSkvUM" alt="Awesome Page" />
|
<img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h3>
|
<h3>
|
||||||
<a href="https://dioxuslabs.com"> Website </a>
|
<a href="https://dioxuslabs.com"> Website </a>
|
||||||
@@ -50,18 +51,26 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<h4>
|
||||||
|
<a href="https://github.com/DioxusLabs/dioxus/blob/master/README.md"> English </a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://github.com/DioxusLabs/dioxus/blob/master/notes/README/ZH_CN.md"> 中文 </a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust.
|
Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let mut count = use_state(&cx, || 0);
|
let (count, set_count) = use_state(&cx, || 0);
|
||||||
|
|
||||||
cx.render(rsx!(
|
cx.render(rsx!(
|
||||||
h1 { "High-Five counter: {count}" }
|
h1 { "High-Five counter: {count}" }
|
||||||
button { onclick: move |_| count += 1, "Up high!" }
|
button { onclick: move |_| set_count(count + 1), "Up high!" }
|
||||||
button { onclick: move |_| count -= 1, "Down low!" }
|
button { onclick: move |_| set_count(count - 1), "Down low!" }
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -76,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.
|
- Comprehensive inline documentation - hover and guides for all HTML elements, listeners, and events.
|
||||||
- Extremely memory efficient - 0 global allocations for steady-state components.
|
- Extremely memory efficient - 0 global allocations for steady-state components.
|
||||||
- Multi-channel asynchronous scheduler for first-class async support.
|
- 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
|
### Examples
|
||||||
@@ -108,7 +117,7 @@ cargo run --example EXAMPLE
|
|||||||
| [](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) |
|
| [](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) |
|
||||||
|
|
||||||
|
|
||||||
See the awesome-dioxus page for a curated list of content in the Dioxus Ecosystem.
|
See the [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) page for a curated list of content in the Dioxus Ecosystem.
|
||||||
|
|
||||||
|
|
||||||
## Why Dioxus and why Rust?
|
## Why Dioxus and why Rust?
|
||||||
@@ -133,76 +142,56 @@ By using Rust, we gain:
|
|||||||
|
|
||||||
Specifically, Dioxus provides us many other assurances:
|
Specifically, Dioxus provides us many other assurances:
|
||||||
|
|
||||||
- Proper use of immutable datastructures
|
- Proper use of immutable data structures
|
||||||
- Guaranteed error handling (so you can sleep easy at night not worrying about `cannot read property of undefined`)
|
- Guaranteed error handling (so you can sleep easy at night not worrying about `cannot read property of undefined`)
|
||||||
- Native performance on mobile
|
- Native performance on mobile
|
||||||
- Direct access to system IO
|
- 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 shouldn't use Dioxus if:
|
||||||
|
|
||||||
- You don't like the React Hooks approach to frontend
|
- You don't like the React Hooks approach to frontend
|
||||||
- You need a no-std renderer
|
- You need a no-std renderer
|
||||||
- You want to support browsers where Wasm or asm.js are not supported.
|
- 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
|
## Comparison with other Rust UI frameworks
|
||||||
Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**.
|
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, no SSR.
|
- [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.
|
- [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.
|
- [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 |
|
Dioxus is unique in the Rust ecosystem in that it supports:
|
||||||
| ------------------------- | ------ | ----- | -------------------------------------------------------------------- |
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
- ✅ = implemented and working
|
- Components with props that borrow from their parent
|
||||||
- 🛠 = actively being worked on
|
- Server-side-rendering with client-side hydration
|
||||||
- 👀 = not yet implemented or being worked on
|
- Support for desktop applications
|
||||||
- ❓ = not sure if will or can implement
|
|
||||||
|
|
||||||
|
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
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ edition = "2018"
|
|||||||
[output.html]
|
[output.html]
|
||||||
mathjax-support = true
|
mathjax-support = true
|
||||||
site-url = "/mdBook/"
|
site-url = "/mdBook/"
|
||||||
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
|
git-repository-url = "https://github.com/DioxusLabs/dioxus/edit/master/docs/guide"
|
||||||
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
edit-url-template = "https://github.com/DioxusLabs/dioxus/edit/master/docs/guide/{path}"
|
||||||
|
|
||||||
[output.html.playground]
|
[output.html.playground]
|
||||||
editable = true
|
editable = true
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ In general, Dioxus and React share many functional similarities. If this guide i
|
|||||||
|
|
||||||
## Multiplatform
|
## 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:
|
Right now, we have several 1st-party renderers:
|
||||||
- WebSys (for WASM)
|
- WebSys (for WASM)
|
||||||
@@ -33,8 +33,7 @@ Right now, we have several 1st-party renderers:
|
|||||||
|
|
||||||
### Web Support
|
### Web Support
|
||||||
---
|
---
|
||||||
|
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).
|
||||||
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).
|
|
||||||
|
|
||||||
Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features.
|
Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features.
|
||||||
|
|
||||||
@@ -45,6 +44,7 @@ Examples:
|
|||||||
- [ECommerce](https://github.com/DioxusLabs/example-projects/tree/master/ecommerce-site)
|
- [ECommerce](https://github.com/DioxusLabs/example-projects/tree/master/ecommerce-site)
|
||||||
|
|
||||||
[](https://github.com/DioxusLabs/example-projects/blob/master/todomvc)
|
[](https://github.com/DioxusLabs/example-projects/blob/master/todomvc)
|
||||||
|
|
||||||
### SSR Support
|
### SSR Support
|
||||||
---
|
---
|
||||||
Dioxus supports server-side rendering!
|
Dioxus supports server-side rendering!
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- [Introduction](README.md)
|
- [Introduction](README.md)
|
||||||
- [Roadmap](ROADMAP.md)
|
- [Roadmap](ROADMAP.md)
|
||||||
- [Getting Setup](setup.md)
|
- [Getting Set Up](setup.md)
|
||||||
- [Hello, World!](hello_world.md)
|
- [Hello, World!](hello_world.md)
|
||||||
- [Describing the UI](elements/index.md)
|
- [Describing the UI](elements/index.md)
|
||||||
- [Intro to Elements](elements/vnodes.md)
|
- [Intro to Elements](elements/vnodes.md)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Core Topics
|
# 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.
|
At a very high level, Dioxus is simply a Rust framework for _declaring_ user interfaces and _reacting_ to changes.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
Dioxus differs slightly from other UI virtual doms in some subtle ways due to its memory allocator.
|
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.
|
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/
|
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
|
```rust
|
||||||
fn test() -> DomTree {
|
fn test() -> DomTree {
|
||||||
@@ -40,7 +40,7 @@ fn test_component(cx: Scope, name: String) -> Element {
|
|||||||
|
|
||||||
"This is different than React, why differ?".
|
"This is different than React, why differ?".
|
||||||
|
|
||||||
Take a component likes this:
|
Take a component like this:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn test(cx: Scope) -> DomTree {
|
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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Signals: Skipping the Diff
|
# 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.
|
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
|
```rust
|
||||||
let mut a = use_signal(&cx, || 0);
|
let mut a = use_signal(&cx, || 0);
|
||||||
@@ -88,7 +88,7 @@ let mut a = use_signal(&cx, || 0);
|
|||||||
let c = *a + *b;
|
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
|
## 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.
|
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
|
```rust
|
||||||
const DICT: AtomFamily<String, String> = |_| {};
|
const DICT: AtomFamily<String, String> = |_| {};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ For reference, check out the WebSys renderer as a starting point for your custom
|
|||||||
|
|
||||||
## Trait implementation and DomEdits
|
## 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
|
```rust
|
||||||
pub trait RealDom<'a> {
|
pub trait RealDom<'a> {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ In this chapter, you'll learn about:
|
|||||||
|
|
||||||
## The use case
|
## 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 `<a>` 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 `<a>` tag, like so:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
rsx!(
|
rsx!(
|
||||||
@@ -98,7 +98,7 @@ struct ClickableProps<'a> {
|
|||||||
children: Element<'a>
|
children: Element<'a>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
fn Clickable(cx: Scope<ClickableProps>) -> Element {
|
||||||
cx.render(rsx!(
|
cx.render(rsx!(
|
||||||
a {
|
a {
|
||||||
href: "{cx.props.href}",
|
href: "{cx.props.href}",
|
||||||
|
|||||||
@@ -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
|
- Upvote/Downvote
|
||||||
- View comments
|
- View comments
|
||||||
@@ -53,7 +53,7 @@ If we look at the layout of the component, we notice quite a few buttons and fun
|
|||||||
- View article
|
- 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:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ fn VoteButton(cx: Scope<VoteButtonProps>) -> Element {
|
|||||||
|
|
||||||
## Borrowing
|
## 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<str>`.
|
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<str>`.
|
||||||
|
|
||||||
Because we're working in Rust, we can choose to either use `Rc<str>`, 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<str>`, clone `Title` on every re-render of `Post`, or simply borrow it. In most cases, you'll just want to let `Title` be cloned.
|
||||||
|
|
||||||
@@ -156,6 +156,42 @@ For users of React: Dioxus knows *not* to memoize components that borrow propert
|
|||||||
|
|
||||||
This means that during the render process, a newer version of `TitleCardProps` will never be compared with a previous version, saving some clock cycles.
|
This means that during the render process, a newer version of `TitleCardProps` will never be compared with a previous version, saving some clock cycles.
|
||||||
|
|
||||||
|
## The inline_props macro
|
||||||
|
|
||||||
|
Yes - *another* macro! However, this one is entirely optional.
|
||||||
|
|
||||||
|
For internal components, we provide the `inline_props` macro, which will let you embed your `Props` definition right into the function arguments of your component.
|
||||||
|
|
||||||
|
Our title card above would be transformed from:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Props, PartialEq)]
|
||||||
|
struct TitleCardProps {
|
||||||
|
title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn TitleCard(cx: Scope<TitleCardProps>) -> Element {
|
||||||
|
cx.render(rsx!{
|
||||||
|
h1 { "{cx.props.title}" }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[inline_props]
|
||||||
|
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.
|
||||||
|
|
||||||
|
However, it's great for quickly throwing together an app without dealing with *any* extra boilerplate.
|
||||||
|
|
||||||
## The `Scope` object
|
## 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.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Now that we have a "logged_in" flag accessible in our props, we can render two d
|
|||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn App(cx: Scope<AppProps>) -> Element {
|
fn App(cx: Scope<AppProps>) -> Element {
|
||||||
if props.logged_in {
|
if cx.props.logged_in {
|
||||||
cx.render(rsx!{
|
cx.render(rsx!{
|
||||||
DashboardScreen {}
|
DashboardScreen {}
|
||||||
})
|
})
|
||||||
@@ -39,7 +39,7 @@ fn App(cx: Scope<AppProps>) -> 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
|
## Using match statements
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ fn App(Scope) -> Element {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```rust
|
||||||
// src/post/mod.rs
|
// src/post/mod.rs
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Core Topics
|
# 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.
|
At a very high level, Dioxus is simply a Rust framework for _declaring_ user interfaces and _reacting_ to changes.
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,41 @@ The HTML-rendered version of this list would follow what you would expect:
|
|||||||
</ul>
|
</ul>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Rendering our posts with a PostList component
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Props, PartialEq)]
|
||||||
|
struct PostListProps<'a> {
|
||||||
|
posts: &'a [PostData]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Next, we're going to define our component:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn App(cx: Scope<PostList>) -> Element {
|
||||||
|
// First, we create a new iterator by mapping the post array
|
||||||
|
let posts = cx.props.posts.iter().map(|post| rsx!{
|
||||||
|
Post {
|
||||||
|
title: post.title,
|
||||||
|
age: post.age,
|
||||||
|
original_poster: post.original_poster
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finally, we render the post list inside of a container
|
||||||
|
cx.render(rsx!{
|
||||||
|
ul { class: "post-list"
|
||||||
|
{posts}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Filtering Iterators
|
## Filtering Iterators
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ $ tree
|
|||||||
|
|
||||||
We are greeted with a pre-initialized git repository, our code folder (`src`) and our project file (`Cargo.toml`).
|
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
|
```shell
|
||||||
$ more src/main.rs
|
$ more src/main.rs
|
||||||
|
|||||||
@@ -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.
|
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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -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
|
```rust
|
||||||
fn App(cx: Scope<PostProps>) -> Element {
|
fn App(cx: Scope<PostProps>) -> Element {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Overview
|
# 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:
|
We'll learn about:
|
||||||
- Installing the Rust programming language
|
- Installing the Rust programming language
|
||||||
@@ -15,7 +15,6 @@ For platform-specific guides, check out the [Platform Specific Guides](../platfo
|
|||||||
Dioxus requires a few main things to get up and running:
|
Dioxus requires a few main things to get up and running:
|
||||||
|
|
||||||
- The [Rust compiler](https://www.rust-lang.org) and associated build tooling
|
- 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)
|
- 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.
|
Dioxus integrates very well with the Rust-Analyzer IDE plugin which will provide appropriate syntax highlighting, code navigation, folding, and more.
|
||||||
@@ -24,7 +23,7 @@ Dioxus integrates very well with the Rust-Analyzer IDE plugin which will provide
|
|||||||
|
|
||||||
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
|
rustup target add wasm32-unknown-unknown
|
||||||
@@ -32,7 +31,7 @@ rustup target add wasm32-unknown-unknown
|
|||||||
|
|
||||||
## Platform-Specific Dependencies
|
## 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.
|
||||||
|
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
@@ -64,13 +63,13 @@ Currently - everything for macOS is built right in! However, you might run into
|
|||||||
|
|
||||||
## Dioxus-CLI for dev server, bundling, etc.
|
## 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
|
$ 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
|
$ cargo install --force dioxus-cli
|
||||||
|
|||||||
@@ -24,3 +24,5 @@ In this chapter, we'll cover the various ways to manage state, the appropriate t
|
|||||||
|
|
||||||
|
|
||||||
## `provide_context` and `consume_context`
|
## `provide_context` and `consume_context`
|
||||||
|
|
||||||
|
## Terminology
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -1,5 +1,5 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body, pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
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.
|
to hand out an &mut T to children - all props are consumed by &P, so you'd only get an &&mut T.
|
||||||
|
|||||||
+21
-19
@@ -18,17 +18,19 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let display_value: UseState<String> = use_state(&cx, || String::from("0"));
|
let (display_value, set_display_value) = use_state(&cx, || String::from("0"));
|
||||||
|
|
||||||
let input_digit = move |num: u8| {
|
let input_digit = move |num: u8| {
|
||||||
if display_value.get() == "0" {
|
if display_value == "0" {
|
||||||
display_value.set(String::new());
|
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| {
|
let input_operator = move |key: &str| {
|
||||||
display_value.modify().push_str(key);
|
set_display_value.make_mut().push_str(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.render(rsx!(
|
cx.render(rsx!(
|
||||||
@@ -53,7 +55,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
KeyCode::Num9 => input_digit(9),
|
KeyCode::Num9 => input_digit(9),
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
if !display_value.len() != 0 {
|
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 {
|
button {
|
||||||
class: "calculator-key key-clear",
|
class: "calculator-key key-clear",
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
display_value.set(String::new());
|
set_display_value(String::new());
|
||||||
if display_value != "" {
|
if !display_value.is_empty(){
|
||||||
display_value.set("0".into());
|
set_display_value("0".into());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[if display_value == "" { "C" } else { "AC" }]
|
[if display_value.is_empty() { "C" } else { "AC" }]
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: "calculator-key key-sign",
|
class: "calculator-key key-sign",
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let temp = calc_val(display_value.get().clone());
|
let temp = calc_val(display_value.clone());
|
||||||
if temp > 0.0 {
|
if temp > 0.0 {
|
||||||
display_value.set(format!("-{}", temp));
|
set_display_value(format!("-{}", temp));
|
||||||
} else {
|
} else {
|
||||||
display_value.set(format!("{}", temp.abs()));
|
set_display_value(format!("{}", temp.abs()));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"±"
|
"±"
|
||||||
@@ -87,8 +89,8 @@ fn app(cx: Scope) -> Element {
|
|||||||
button {
|
button {
|
||||||
class: "calculator-key key-percent",
|
class: "calculator-key key-percent",
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
display_value.set(
|
set_display_value(
|
||||||
format!("{}", calc_val(display_value.get().clone()) / 100.0)
|
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),
|
button { class: "calculator-key key-0", onclick: move |_| input_digit(0),
|
||||||
"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!{
|
(1..10).map(|k| rsx!{
|
||||||
@@ -130,7 +132,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
}
|
}
|
||||||
button { class: "calculator-key key-equals",
|
button { class: "calculator-key key-equals",
|
||||||
onclick: move |_| {
|
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() {
|
for c in val[start_index..].chars() {
|
||||||
if c == '+' || c == '-' || c == '*' || c == '/' {
|
if c == '+' || c == '-' || c == '*' || c == '/' {
|
||||||
if temp != "" {
|
if !temp.is_empty() {
|
||||||
match &operation as &str {
|
match &operation as &str {
|
||||||
"+" => result += temp.parse::<f64>().unwrap(),
|
"+" => result += temp.parse::<f64>().unwrap(),
|
||||||
"-" => result -= temp.parse::<f64>().unwrap(),
|
"-" => result -= temp.parse::<f64>().unwrap(),
|
||||||
@@ -191,7 +193,7 @@ fn calc_val(val: String) -> f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if temp != "" {
|
if !temp.is_empty() {
|
||||||
match &operation as &str {
|
match &operation as &str {
|
||||||
"+" => result += temp.parse::<f64>().unwrap(),
|
"+" => result += temp.parse::<f64>().unwrap(),
|
||||||
"-" => result -= temp.parse::<f64>().unwrap(),
|
"-" => result -= temp.parse::<f64>().unwrap(),
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ fn Example(cx: Scope) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Props)]
|
#[derive(Props)]
|
||||||
struct BannerProps {
|
struct BannerProps<'a> {
|
||||||
children: Element<'a>,
|
children: Element<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Banner(cx: Scope) -> Element {
|
fn Banner<'a>(cx: Scope<'a, BannerProps<'a>>) -> Element<'a> {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
h1 { "This is a great banner!" }
|
h1 { "This is a great banner!" }
|
||||||
|
|||||||
+16
-16
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Tiny CRM: A port of the Yew CRM example to Dioxus.
|
Tiny CRM: A port of the Yew CRM example to Dioxus.
|
||||||
*/
|
*/
|
||||||
use dioxus::{events::FormEvent, prelude::*};
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::desktop::launch(app);
|
dioxus::desktop::launch(app);
|
||||||
@@ -20,11 +20,11 @@ pub struct Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let scene = use_state(&cx, || Scene::ClientsList);
|
|
||||||
let clients = use_ref(&cx, || vec![] as Vec<Client>);
|
let clients = use_ref(&cx, || vec![] as Vec<Client>);
|
||||||
let firstname = use_state(&cx, String::new);
|
let (scene, set_scene) = use_state(&cx, || Scene::ClientsList);
|
||||||
let lastname = use_state(&cx, String::new);
|
let (firstname, set_firstname) = use_state(&cx, String::new);
|
||||||
let description = 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!(
|
cx.render(rsx!(
|
||||||
body {
|
body {
|
||||||
@@ -38,7 +38,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
|
|
||||||
h1 {"Dioxus CRM Example"}
|
h1 {"Dioxus CRM Example"}
|
||||||
|
|
||||||
match *scene {
|
match scene {
|
||||||
Scene::ClientsList => rsx!(
|
Scene::ClientsList => rsx!(
|
||||||
div { class: "crm",
|
div { class: "crm",
|
||||||
h2 { margin_bottom: "10px", "List of clients" }
|
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 pure-button-primary", onclick: move |_| set_scene(Scene::NewClientForm), "Add New" }
|
||||||
button { class: "pure-button", onclick: move |_| scene.set(Scene::Settings), "Settings" }
|
button { class: "pure-button", onclick: move |_| set_scene(Scene::Settings), "Settings" }
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
Scene::NewClientForm => rsx!(
|
Scene::NewClientForm => rsx!(
|
||||||
@@ -63,19 +63,19 @@ fn app(cx: Scope) -> Element {
|
|||||||
class: "new-client firstname",
|
class: "new-client firstname",
|
||||||
placeholder: "First name",
|
placeholder: "First name",
|
||||||
value: "{firstname}",
|
value: "{firstname}",
|
||||||
oninput: move |e| firstname.set(e.value.clone())
|
oninput: move |e| set_firstname(e.value.clone())
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
class: "new-client lastname",
|
class: "new-client lastname",
|
||||||
placeholder: "Last name",
|
placeholder: "Last name",
|
||||||
value: "{lastname}",
|
value: "{lastname}",
|
||||||
oninput: move |e| lastname.set(e.value.clone())
|
oninput: move |e| set_lastname(e.value.clone())
|
||||||
}
|
}
|
||||||
textarea {
|
textarea {
|
||||||
class: "new-client description",
|
class: "new-client description",
|
||||||
placeholder: "Description",
|
placeholder: "Description",
|
||||||
value: "{description}",
|
value: "{description}",
|
||||||
oninput: move |e| description.set(e.value.clone())
|
oninput: move |e| set_description(e.value.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
@@ -86,13 +86,13 @@ fn app(cx: Scope) -> Element {
|
|||||||
first_name: (*firstname).clone(),
|
first_name: (*firstname).clone(),
|
||||||
last_name: (*lastname).clone(),
|
last_name: (*lastname).clone(),
|
||||||
});
|
});
|
||||||
description.set(String::new());
|
set_description(String::new());
|
||||||
firstname.set(String::new());
|
set_firstname(String::new());
|
||||||
lastname.set(String::new());
|
set_lastname(String::new());
|
||||||
},
|
},
|
||||||
"Add 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"
|
"Go Back"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: "pure-button pure-button-primary",
|
class: "pure-button pure-button-primary",
|
||||||
onclick: move |_| scene.set(Scene::ClientsList),
|
onclick: move |_| set_scene(Scene::ClientsList),
|
||||||
"Go Back"
|
"Go Back"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,12 +5,12 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let disabled = use_state(&cx, || false);
|
let (disabled, set_disabled) = use_state(&cx, || false);
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
button {
|
button {
|
||||||
onclick: move |_| disabled.set(!disabled.get()),
|
onclick: move |_| set_disabled(!disabled),
|
||||||
"click to " [if *disabled {"enable"} else {"disable"} ] " the lower button"
|
"click to " [if *disabled {"enable"} else {"disable"} ] " the lower button"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-6
@@ -1,5 +1,6 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
//! Render a bunch of doggos!
|
//! Render a bunch of doggos!
|
||||||
//!
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
|
||||||
let selected_breed = use_state(&cx, || None);
|
let (breed, set_breed) = use_state(&cx, || None);
|
||||||
|
|
||||||
match fut.value() {
|
match fut.value() {
|
||||||
Some(Ok(breeds)) => cx.render(rsx! {
|
Some(Ok(breeds)) => cx.render(rsx! {
|
||||||
@@ -35,14 +36,14 @@ fn app(cx: Scope) -> Element {
|
|||||||
breeds.message.keys().map(|breed| rsx!(
|
breeds.message.keys().map(|breed| rsx!(
|
||||||
li {
|
li {
|
||||||
button {
|
button {
|
||||||
onclick: move |_| selected_breed.set(Some(breed.clone())),
|
onclick: move |_| set_breed(Some(breed.clone())),
|
||||||
"{breed}"
|
"{breed}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
div { flex: "50%",
|
div { flex: "50%",
|
||||||
match &*selected_breed {
|
match breed {
|
||||||
Some(breed) => rsx!( Breed { breed: breed.clone() } ),
|
Some(breed) => rsx!( Breed { breed: breed.clone() } ),
|
||||||
None => rsx!("No Breed selected"),
|
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" }
|
div { "Error fetching breeds" }
|
||||||
}),
|
}),
|
||||||
None => cx.render(rsx! {
|
None => cx.render(rsx! {
|
||||||
@@ -61,7 +62,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
|
|
||||||
#[inline_props]
|
#[inline_props]
|
||||||
fn Breed(cx: Scope, breed: String) -> Element {
|
fn Breed(cx: Scope, breed: String) -> Element {
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct DogApi {
|
struct DogApi {
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
@@ -72,6 +73,12 @@ fn Breed(cx: Scope, breed: String) -> Element {
|
|||||||
reqwest::get(endpoint).await.unwrap().json::<DogApi>().await
|
reqwest::get(endpoint).await.unwrap().json::<DogApi>().await
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let (name, set_name) = use_state(&cx, || breed.clone());
|
||||||
|
if name != breed {
|
||||||
|
set_name(breed.clone());
|
||||||
|
fut.restart();
|
||||||
|
}
|
||||||
|
|
||||||
cx.render(match fut.value() {
|
cx.render(match fut.value() {
|
||||||
Some(Ok(resp)) => rsx! {
|
Some(Ok(resp)) => rsx! {
|
||||||
button {
|
button {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let files = use_ref(&cx, || Files::new());
|
let files = use_ref(&cx, Files::new);
|
||||||
|
|
||||||
rsx!(cx, div {
|
rsx!(cx, div {
|
||||||
link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", }
|
link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", }
|
||||||
@@ -28,8 +28,8 @@ fn app(cx: Scope) -> Element {
|
|||||||
}
|
}
|
||||||
main {
|
main {
|
||||||
files.read().path_names.iter().enumerate().map(|(dir_id, path)| {
|
files.read().path_names.iter().enumerate().map(|(dir_id, path)| {
|
||||||
let path_end = path.split('/').last().unwrap_or(path.as_str());
|
let path_end = path.split('/').last().unwrap_or_else(|| path.as_str());
|
||||||
let icon_type = if path_end.contains(".") {
|
let icon_type = if path_end.contains('.') {
|
||||||
"description"
|
"description"
|
||||||
} else {
|
} else {
|
||||||
"folder"
|
"folder"
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::desktop::launch_with_props(app, (), |c| {
|
||||||
|
c.with_file_drop_handler(|_w, e| {
|
||||||
|
println!("{:?}", e);
|
||||||
|
false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx!(
|
||||||
|
div {
|
||||||
|
h1 { "drag an file here" }
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
|
|
||||||
@@ -30,8 +32,8 @@ impl Label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let items = use_ref(&cx, || vec![]);
|
let items = use_ref(&cx, Vec::new);
|
||||||
let selected = use_state(&cx, || None);
|
let (selected, set_selected) = use_state(&cx, || None);
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div { class: "container",
|
div { class: "container",
|
||||||
@@ -69,7 +71,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
rsx!(tr { class: "{is_in_danger}",
|
rsx!(tr { class: "{is_in_danger}",
|
||||||
td { class:"col-md-1" }
|
td { class:"col-md-1" }
|
||||||
td { class:"col-md-1", "{item.key}" }
|
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 }
|
a { class: "lbl", item.labels }
|
||||||
}
|
}
|
||||||
td { class: "col-md-1",
|
td { class: "col-md-1",
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let val = use_state(&cx, || 0);
|
let (val, set_val) = use_state(&cx, || 0);
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
h1 { "hello world. Count: {val}" }
|
h1 { "hello world. Count: {val}" }
|
||||||
button {
|
button {
|
||||||
onclick: move |_| *val.modify() += 1,
|
onclick: move |_| set_val(val + 1),
|
||||||
"click to increment"
|
"click to increment"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
//!
|
//!
|
||||||
//! There is some conversion happening when input types are checkbox/radio/select/textarea etc.
|
//! There is some conversion happening when input types are checkbox/radio/select/textarea etc.
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use dioxus::{events::FormEvent, prelude::*};
|
use dioxus::{events::FormEvent, prelude::*};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! (
|
||||||
|
div {
|
||||||
|
p {
|
||||||
|
a {
|
||||||
|
href: "http://dioxuslabs.com/",
|
||||||
|
"default link"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
a {
|
||||||
|
href: "http://dioxuslabs.com/",
|
||||||
|
prevent_default: "onclick",
|
||||||
|
onclick: |_| {
|
||||||
|
println!("Hello Dioxus");
|
||||||
|
},
|
||||||
|
"custom event link",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
Example: Manual Edits
|
|
||||||
|
|
||||||
It's possible to manually provide a stream of DomEdits to a Dioxus Renderer. All renderers are designed to accept a stream
|
|
||||||
of DomEdits that abstract over a stack machine. This allows the VirtualDOM to exist entirely separately from the RealDOM,
|
|
||||||
though features like NodeRefs and NativeEvents might not work properly everywhere.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use dioxus::core::*;
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
use DomEdit::*;
|
|
||||||
|
|
||||||
let edits = vec![
|
|
||||||
// create a container and push it onto the stack
|
|
||||||
CreateElement {
|
|
||||||
tag: "div",
|
|
||||||
root: 0,
|
|
||||||
},
|
|
||||||
// create an element and push it onto the stack
|
|
||||||
CreateElement { tag: "h1", root: 2 },
|
|
||||||
// create a text node and push it onto the stack
|
|
||||||
CreateTextNode {
|
|
||||||
text: "hello world",
|
|
||||||
root: 3,
|
|
||||||
},
|
|
||||||
// append the text node to the h1 element
|
|
||||||
AppendChildren { many: 1 },
|
|
||||||
// append the h1 element to the container
|
|
||||||
AppendChildren { many: 1 },
|
|
||||||
// append the container to the default render element ("dioxusroot" if used with default config)
|
|
||||||
AppendChildren { many: 1 },
|
|
||||||
];
|
|
||||||
|
|
||||||
let app: Component = |cx| cx.render(rsx!(div { "some app" }));
|
|
||||||
|
|
||||||
dioxus_desktop::launch_with_props(app, (), |c| c.with_edits(edits));
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//! Nested Listeners
|
||||||
|
//!
|
||||||
|
//! This example showcases how to control event bubbling from child to parents.
|
||||||
|
//!
|
||||||
|
//! Both web and desktop support bubbling and bubble cancelation.
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
onclick: move |_| println!("clicked! top"),
|
||||||
|
button {
|
||||||
|
onclick: move |_| println!("clicked! bottom propoate"),
|
||||||
|
"Propogate"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |evt| {
|
||||||
|
println!("clicked! bottom no bubbling");
|
||||||
|
evt.cancel_bubble();
|
||||||
|
},
|
||||||
|
"Dont propogate"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
"Does not handle clicks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
//! Example: README.md showcase
|
||||||
|
//!
|
||||||
|
//! The example from the README.md.
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
Button {
|
||||||
|
a: "asd".to_string(),
|
||||||
|
c: Some("asd".to_string()),
|
||||||
|
d: "asd".to_string(),
|
||||||
|
e: "asd".to_string(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props, PartialEq)]
|
||||||
|
struct ButtonProps {
|
||||||
|
a: String,
|
||||||
|
|
||||||
|
#[props(default)]
|
||||||
|
b: Option<String>,
|
||||||
|
|
||||||
|
#[props(default)]
|
||||||
|
c: Option<String>,
|
||||||
|
|
||||||
|
#[props(default, strip_option)]
|
||||||
|
d: Option<String>,
|
||||||
|
|
||||||
|
#[props(optional)]
|
||||||
|
e: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Button(cx: Scope<ButtonProps>) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
button {
|
||||||
|
"{cx.props.a}"
|
||||||
|
"{cx.props.b:?}"
|
||||||
|
"{cx.props.c:?}"
|
||||||
|
"{cx.props.d:?}"
|
||||||
|
"{cx.props.e:?}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
//! Example: Calculator
|
//! Example: Calculator
|
||||||
//! -------------------
|
//! -------------------
|
||||||
//!
|
//!
|
||||||
@@ -30,7 +32,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let state = use_ref(&cx, || Calculator::new());
|
let state = use_ref(&cx, Calculator::new);
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
style { [include_str!("./assets/calculator.css")] }
|
style { [include_str!("./assets/calculator.css")] }
|
||||||
@@ -171,8 +173,8 @@ impl Calculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn input_dot(&mut self) {
|
fn input_dot(&mut self) {
|
||||||
if self.display_value.find(".").is_none() {
|
if !self.display_value.contains('.') {
|
||||||
self.display_value.push_str(".");
|
self.display_value.push('.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn perform_operation(&mut self) {
|
fn perform_operation(&mut self) {
|
||||||
@@ -190,8 +192,8 @@ impl Calculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn toggle_sign(&mut self) {
|
fn toggle_sign(&mut self) {
|
||||||
if self.display_value.starts_with("-") {
|
if self.display_value.starts_with('-') {
|
||||||
self.display_value = self.display_value.trim_start_matches("-").to_string();
|
self.display_value = self.display_value.trim_start_matches('-').to_string();
|
||||||
} else {
|
} else {
|
||||||
self.display_value = format!("-{}", self.display_value);
|
self.display_value = format!("-{}", self.display_value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let state = use_state(&cx, PlayerState::new);
|
let (state, set_state) = use_state(&cx, PlayerState::new);
|
||||||
|
|
||||||
cx.render(rsx!(
|
cx.render(rsx!(
|
||||||
div {
|
div {
|
||||||
h1 {"Select an option"}
|
h1 {"Select an option"}
|
||||||
h3 { "The radio is... " [state.is_playing()] "!" }
|
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"
|
"Pause"
|
||||||
}
|
}
|
||||||
button { onclick: move |_| state.modify().reduce(PlayerAction::Play),
|
button { onclick: move |_| set_state.make_mut().reduce(PlayerAction::Play),
|
||||||
"Play"
|
"Play"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -9,13 +9,13 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let mut count = use_state(&cx, || 0);
|
let (count, set_count) = use_state(&cx, || 0);
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
h1 { "High-Five counter: {count}" }
|
h1 { "High-Five counter: {count}" }
|
||||||
button { onclick: move |_| count += 1, "Up high!" }
|
button { onclick: move |_| set_count(count + 1), "Up high!" }
|
||||||
button { onclick: move |_| count -= 1, "Down low!" }
|
button { onclick: move |_| set_count(count - 1), "Down low!" }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
+18
-4
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus::router::{Link, Route, Router};
|
use dioxus::router::{Link, Route, Router};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::desktop::launch(app);
|
dioxus::desktop::launch(app);
|
||||||
@@ -18,7 +19,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
Route { to: "/", "Home" }
|
Route { to: "/", "Home" }
|
||||||
Route { to: "users",
|
Route { to: "users",
|
||||||
Route { to: "/", "User list" }
|
Route { to: "/", "User list" }
|
||||||
Route { to: ":name", BlogPost {} }
|
Route { to: ":name", User {} }
|
||||||
}
|
}
|
||||||
Route { to: "blog"
|
Route { to: "blog"
|
||||||
Route { to: "/", "Blog list" }
|
Route { to: "/", "Blog list" }
|
||||||
@@ -30,7 +31,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn BlogPost(cx: Scope) -> Element {
|
fn BlogPost(cx: Scope) -> Element {
|
||||||
let post = dioxus::router::use_route(&cx).last_segment()?;
|
let post = dioxus::router::use_route(&cx).last_segment();
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
@@ -40,14 +41,27 @@ fn BlogPost(cx: Scope) -> Element {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Query {
|
||||||
|
bold: bool,
|
||||||
|
}
|
||||||
|
|
||||||
fn User(cx: Scope) -> Element {
|
fn User(cx: Scope) -> Element {
|
||||||
let post = dioxus::router::use_route(&cx).last_segment()?;
|
let post = dioxus::router::use_route(&cx).last_segment();
|
||||||
let bold = dioxus::router::use_route(&cx).param::<bool>("bold");
|
let query = dioxus::router::use_route(&cx)
|
||||||
|
.query::<Query>()
|
||||||
|
.unwrap_or(Query { bold: false });
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
h1 { "Reading blog post: {post}" }
|
h1 { "Reading blog post: {post}" }
|
||||||
p { "example blog post" }
|
p { "example blog post" }
|
||||||
|
|
||||||
|
if query.bold {
|
||||||
|
rsx!{ b { "bold" } }
|
||||||
|
} else {
|
||||||
|
rsx!{ i { "italic" } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn example(cx: Scope) -> Element {
|
fn example(cx: Scope) -> Element {
|
||||||
let items = use_state(&cx, || {
|
let (items, _set_items) = use_state(&cx, || {
|
||||||
vec![Thing {
|
vec![Thing {
|
||||||
a: "asd".to_string(),
|
a: "asd".to_string(),
|
||||||
b: 10,
|
b: 10,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
//! Suspense in Dioxus
|
//! Suspense in Dioxus
|
||||||
//!
|
//!
|
||||||
//! Currently, `rsx!` does not accept futures as values. To achieve the functionality
|
//! Currently, `rsx!` does not accept futures as values. To achieve the functionality
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
//! Example: Basic Tailwind usage
|
//! Example: Basic Tailwind usage
|
||||||
//!
|
//!
|
||||||
//! This example shows how an app might be styled with TailwindCSS.
|
//! This example shows how an app might be styled with TailwindCSS.
|
||||||
|
|||||||
+7
-8
@@ -10,16 +10,15 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let count = use_state(&cx, || 0);
|
let (count, set_count) = use_state(&cx, || 0);
|
||||||
|
|
||||||
use_future(&cx, move || {
|
use_future(&cx, move || {
|
||||||
let count = UseState::for_async(&count);
|
let set_count = set_count.to_owned();
|
||||||
// for_async![count];
|
|
||||||
async move {
|
async move {
|
||||||
// loop {
|
loop {
|
||||||
// tokio::time::sleep(Duration::from_millis(1000)).await;
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||||
// count += 1;
|
set_count.modify(|f| f + 1);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
div {
|
div {
|
||||||
h1 { "Current count: {count}" }
|
h1 { "Current count: {count}" }
|
||||||
button {
|
button {
|
||||||
onclick: move |_| count.set(0),
|
onclick: move |_| set_count(0),
|
||||||
"Reset the count"
|
"Reset the count"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-36
@@ -19,15 +19,15 @@ pub struct TodoItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn app(cx: Scope<()>) -> Element {
|
pub fn app(cx: Scope<()>) -> Element {
|
||||||
let todos = use_state(&cx, || im_rc::HashMap::<u32, TodoItem>::default());
|
let (todos, set_todos) = use_state(&cx, im_rc::HashMap::<u32, TodoItem>::default);
|
||||||
let filter = use_state(&cx, || FilterState::All);
|
let (filter, set_filter) = use_state(&cx, || FilterState::All);
|
||||||
let draft = use_state(&cx, || "".to_string());
|
let (draft, set_draft) = use_state(&cx, || "".to_string());
|
||||||
let mut todo_id = use_state(&cx, || 0);
|
let (todo_id, set_todo_id) = use_state(&cx, || 0);
|
||||||
|
|
||||||
// Filter the todos based on the filter state
|
// Filter the todos based on the filter state
|
||||||
let mut filtered_todos = todos
|
let mut filtered_todos = todos
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, item)| match *filter {
|
.filter(|(_, item)| match filter {
|
||||||
FilterState::All => true,
|
FilterState::All => true,
|
||||||
FilterState::Active => !item.checked,
|
FilterState::Active => !item.checked,
|
||||||
FilterState::Completed => item.checked,
|
FilterState::Completed => item.checked,
|
||||||
@@ -54,27 +54,25 @@ pub fn app(cx: Scope<()>) -> Element {
|
|||||||
placeholder: "What needs to be done?",
|
placeholder: "What needs to be done?",
|
||||||
value: "{draft}",
|
value: "{draft}",
|
||||||
autofocus: "true",
|
autofocus: "true",
|
||||||
oninput: move |evt| draft.set(evt.value.clone()),
|
oninput: move |evt| set_draft(evt.value.clone()),
|
||||||
onkeydown: move |evt| {
|
onkeydown: move |evt| {
|
||||||
if evt.key == "Enter" {
|
if evt.key == "Enter" && !draft.is_empty() {
|
||||||
if !draft.is_empty() {
|
set_todos.make_mut().insert(
|
||||||
todos.modify().insert(
|
*todo_id,
|
||||||
*todo_id,
|
TodoItem {
|
||||||
TodoItem {
|
id: *todo_id,
|
||||||
id: *todo_id,
|
checked: false,
|
||||||
checked: false,
|
contents: draft.clone(),
|
||||||
contents: draft.get().clone(),
|
},
|
||||||
},
|
);
|
||||||
);
|
set_todo_id(todo_id + 1);
|
||||||
todo_id += 1;
|
set_draft("".to_string());
|
||||||
draft.set("".to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ul { class: "todo-list",
|
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!(
|
(!todos.is_empty()).then(|| rsx!(
|
||||||
footer { class: "footer",
|
footer { class: "footer",
|
||||||
@@ -83,14 +81,14 @@ pub fn app(cx: Scope<()>) -> Element {
|
|||||||
span {"{item_text} left"}
|
span {"{item_text} left"}
|
||||||
}
|
}
|
||||||
ul { class: "filters",
|
ul { class: "filters",
|
||||||
li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }}
|
li { class: "All", a { onclick: move |_| set_filter(FilterState::All), "All" }}
|
||||||
li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }}
|
li { class: "Active", a { onclick: move |_| set_filter(FilterState::Active), "Active" }}
|
||||||
li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }}
|
li { class: "Completed", a { onclick: move |_| set_filter(FilterState::Completed), "Completed" }}
|
||||||
}
|
}
|
||||||
(show_clear_completed).then(|| rsx!(
|
(show_clear_completed).then(|| rsx!(
|
||||||
button {
|
button {
|
||||||
class: "clear-completed",
|
class: "clear-completed",
|
||||||
onclick: move |_| todos.modify().retain(|_, todo| todo.checked == false),
|
onclick: move |_| set_todos.make_mut().retain(|_, todo| !todo.checked),
|
||||||
"Clear completed"
|
"Clear completed"
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
@@ -108,29 +106,43 @@ pub fn app(cx: Scope<()>) -> Element {
|
|||||||
|
|
||||||
#[derive(Props)]
|
#[derive(Props)]
|
||||||
pub struct TodoEntryProps<'a> {
|
pub struct TodoEntryProps<'a> {
|
||||||
todos: UseState<'a, im_rc::HashMap<u32, TodoItem>>,
|
set_todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
|
||||||
id: u32,
|
id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
||||||
let todo = &cx.props.todos[&cx.props.id];
|
let (is_editing, set_is_editing) = use_state(&cx, || false);
|
||||||
let is_editing = use_state(&cx, || false);
|
|
||||||
let completed = if todo.checked { "completed" } else { "" };
|
|
||||||
|
|
||||||
rsx!(cx, li { class: "{completed}",
|
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 { "editing" } else { "" };
|
||||||
|
|
||||||
|
rsx!(cx, li {
|
||||||
|
class: "{completed} {editing}",
|
||||||
|
onclick: move |_| set_is_editing(true),
|
||||||
|
onfocusout: move |_| set_is_editing(false),
|
||||||
div { class: "view",
|
div { class: "view",
|
||||||
input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{todo.checked}",
|
input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{todo.checked}",
|
||||||
onchange: move |evt| {
|
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}" }
|
label { r#for: "cbg-{todo.id}", pointer_events: "none", "{todo.contents}" }
|
||||||
is_editing.then(|| rsx!{
|
|
||||||
input {
|
|
||||||
value: "{todo.contents}",
|
|
||||||
oninput: move |evt| cx.props.todos.modify()[&cx.props.id].contents = evt.value.clone(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
is_editing.then(|| rsx!{
|
||||||
|
input {
|
||||||
|
class: "edit",
|
||||||
|
value: "{todo.contents}",
|
||||||
|
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" => set_is_editing(false),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let contents = use_state(&cx, || {
|
let (contents, set_contents) = use_state(&cx, || {
|
||||||
String::from("<script>alert(\"hello world\")</script>")
|
String::from("<script>alert(\"hello world\")</script>")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
input {
|
input {
|
||||||
value: "{contents}",
|
value: "{contents}",
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
oninput: move |e| contents.set(e.value.clone()),
|
oninput: move |e| set_contents(e.value.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<div align="center">
|
||||||
|
<h1>🌗🚀 Dioxus</h1>
|
||||||
|
<p>
|
||||||
|
<strong>Frontend that scales.</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<!-- Crates version -->
|
||||||
|
<a href="https://crates.io/crates/dioxus">
|
||||||
|
<img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
|
||||||
|
alt="Crates.io version" />
|
||||||
|
</a>
|
||||||
|
<!-- Downloads -->
|
||||||
|
<a href="https://crates.io/crates/dioxus">
|
||||||
|
<img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
|
||||||
|
alt="Download" />
|
||||||
|
</a>
|
||||||
|
<!-- docs -->
|
||||||
|
<a href="https://docs.rs/dioxus">
|
||||||
|
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
|
||||||
|
alt="docs.rs docs" />
|
||||||
|
</a>
|
||||||
|
<!-- CI -->
|
||||||
|
<a href="https://github.com/jkelleyrtp/dioxus/actions">
|
||||||
|
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
|
||||||
|
alt="CI status" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<!--Awesome -->
|
||||||
|
<a href="https://github.com/dioxuslabs/awesome-dioxus">
|
||||||
|
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Page" />
|
||||||
|
</a>
|
||||||
|
<!-- Discord -->
|
||||||
|
<a href="https://discord.gg/XgGxMSkvUM">
|
||||||
|
<img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<h3>
|
||||||
|
<a href="https://dioxuslabs.com"> 官网 </a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://dioxuslabs.com/guide"> 手册 </a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://github.com/DioxusLabs/example-projects"> 示例 </a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<h4>
|
||||||
|
<a href="https://github.com/DioxusLabs/dioxus/blob/master/README.md"> English </a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://github.com/DioxusLabs/dioxus/blob/master/README.md"> 中文 </a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
Dioxus 是一个可移植、高性能的框架,用于在 Rust 中构建跨平台的用户界面。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let mut count = use_state(&cx, || 0);
|
||||||
|
|
||||||
|
cx.render(rsx!(
|
||||||
|
h1 { "High-Five counter: {count}" }
|
||||||
|
button { onclick: move |_| count += 1, "Up high!" }
|
||||||
|
button { onclick: move |_| count -= 1, "Down low!" }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应用。
|
||||||
|
|
||||||
|
Dioxus 为不同的平台都提供了很好的开发文档。
|
||||||
|
|
||||||
|
如果你会使用 React ,那 Dioxus 对你来说会很简单。
|
||||||
|
|
||||||
|
### 项目特点:
|
||||||
|
- 对桌面应用的原生支持。
|
||||||
|
- 强大的状态管理工具。
|
||||||
|
- 支持所有 HTML 标签,监听器和事件。
|
||||||
|
- 超高的内存使用率,稳定的组件分配器。
|
||||||
|
- 多通道异步调度器,超强的异步支持。
|
||||||
|
- 更多信息请查阅: [版本发布文档](https://dioxuslabs.com/blog/introducing-dioxus/).
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
本项目中的所有例子都是 `桌面应用` 程序,请使用 `cargo run --example XYZ` 运行这些例子。
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --example EXAMPLE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 进入学习
|
||||||
|
|
||||||
|
<table style="width:100%" align="center">
|
||||||
|
<tr >
|
||||||
|
<th><a href="https://dioxuslabs.com/guide/">教程</a></th>
|
||||||
|
<th><a href="https://dioxuslabs.com/reference/web">网页端</a></th>
|
||||||
|
<th><a href="https://dioxuslabs.com/reference/desktop/">桌面端</a></th>
|
||||||
|
<th><a href="https://dioxuslabs.com/reference/ssr/">SSR</a></th>
|
||||||
|
<th><a href="https://dioxuslabs.com/reference/mobile/">移动端</a></th>
|
||||||
|
<th><a href="https://dioxuslabs.com/guide/concepts/managing_state.html">状态管理</a></th>
|
||||||
|
<tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
## Dioxus 项目
|
||||||
|
|
||||||
|
| 文件浏览器 (桌面应用) | WiFi 扫描器 (桌面应用) | Todo管理 (所有平台) | 商城系统 (SSR/liveview) |
|
||||||
|
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| [](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) |
|
||||||
|
|
||||||
|
|
||||||
|
查看 [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) 查看更多有趣(~~NiuBi~~)的项目!
|
||||||
|
|
||||||
|
## 为什么使用 Dioxus 和 Rust ?
|
||||||
|
|
||||||
|
TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScript。
|
||||||
|
|
||||||
|
TS 代码运行效率不高,而且有大量的配置项。
|
||||||
|
|
||||||
|
相比之下,Dioxus 使用 Rust 编写将大大的提高效能。
|
||||||
|
|
||||||
|
使用 Rust 开发,我们能获得:
|
||||||
|
|
||||||
|
- 静态类型支持。
|
||||||
|
- 变量默认不变性。
|
||||||
|
- 简单直观的模块系统。
|
||||||
|
- 内部集成的文档系统。
|
||||||
|
- 先进的模式匹配系统。
|
||||||
|
- 简洁、高效、强大的迭代器。
|
||||||
|
- 内置的 单元测试 / 集成测试。
|
||||||
|
- 优秀的异常处理系统。
|
||||||
|
- 强大且健全的标准库。
|
||||||
|
- 灵活的 `宏` 系统。
|
||||||
|
- 使用 `crates.io` 管理包。
|
||||||
|
|
||||||
|
Dioxus 能为开发者提供的:
|
||||||
|
|
||||||
|
- 安全使用数据结构。
|
||||||
|
- 安全的错误处理结果。
|
||||||
|
- 拥有原生移动端的性能。
|
||||||
|
- 直接访问系统的IO层。
|
||||||
|
|
||||||
|
Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让团队能在更短的时间内做出强大功能。
|
||||||
|
|
||||||
|
### 不建议使用 Dioxus 的情况?
|
||||||
|
|
||||||
|
您不该在这些情况下使用 Dioxus :
|
||||||
|
|
||||||
|
- 您不喜欢类似 React 的开发风格。
|
||||||
|
- 您需要一个 `no-std` 的渲染器。
|
||||||
|
- 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。
|
||||||
|
- 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。
|
||||||
|
|
||||||
|
|
||||||
|
## 协议
|
||||||
|
|
||||||
|
这个项目使用 [MIT 协议].
|
||||||
|
|
||||||
|
[MIT 协议]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||||
@@ -41,7 +41,7 @@ pub fn format_args_f_impl(input: IfmtInput) -> Result<TokenStream> {
|
|||||||
let mut out_format_literal = defer(&mut *out_format_literal, |it| {
|
let mut out_format_literal = defer(&mut *out_format_literal, |it| {
|
||||||
it.push(colon_or_closing_brace)
|
it.push(colon_or_closing_brace)
|
||||||
});
|
});
|
||||||
let out_format_literal: &mut String = &mut *out_format_literal;
|
let out_format_literal: &mut String = *out_format_literal;
|
||||||
let mut arg = s[i + 1..end].trim();
|
let mut arg = s[i + 1..end].trim();
|
||||||
if let Some("=") = arg.get(arg.len().saturating_sub(1)..) {
|
if let Some("=") = arg.get(arg.len().saturating_sub(1)..) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -267,28 +267,12 @@ mod field_info {
|
|||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct FieldBuilderAttr {
|
pub struct FieldBuilderAttr {
|
||||||
pub default: Option<syn::Expr>,
|
pub default: Option<syn::Expr>,
|
||||||
pub setter: SetterSettings,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SetterSettings {
|
|
||||||
pub doc: Option<syn::Expr>,
|
pub doc: Option<syn::Expr>,
|
||||||
pub skip: bool,
|
pub skip: bool,
|
||||||
pub auto_into: bool,
|
pub auto_into: bool,
|
||||||
pub strip_option: bool,
|
pub strip_option: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SetterSettings {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
doc: Default::default(),
|
|
||||||
skip: false,
|
|
||||||
auto_into: false,
|
|
||||||
strip_option: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FieldBuilderAttr {
|
impl FieldBuilderAttr {
|
||||||
pub fn with(mut self, attrs: &[syn::Attribute]) -> Result<Self, Error> {
|
pub fn with(mut self, attrs: &[syn::Attribute]) -> Result<Self, Error> {
|
||||||
let mut skip_tokens = None;
|
let mut skip_tokens = None;
|
||||||
@@ -316,12 +300,12 @@ mod field_info {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Stash its span for later (we don’t yet know if it’ll be an error)
|
// Stash its span for later (we don’t yet know if it’ll be an error)
|
||||||
if self.setter.skip && skip_tokens.is_none() {
|
if self.skip && skip_tokens.is_none() {
|
||||||
skip_tokens = Some(attr.tokens.clone());
|
skip_tokens = Some(attr.tokens.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.setter.skip && self.default.is_none() {
|
if self.skip && self.default.is_none() {
|
||||||
return Err(Error::new_spanned(
|
return Err(Error::new_spanned(
|
||||||
skip_tokens.unwrap(),
|
skip_tokens.unwrap(),
|
||||||
"#[props(skip)] must be accompanied by default or default_code",
|
"#[props(skip)] must be accompanied by default or default_code",
|
||||||
@@ -342,6 +326,10 @@ mod field_info {
|
|||||||
self.default = Some(*assign.right);
|
self.default = Some(*assign.right);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
"doc" => {
|
||||||
|
self.doc = Some(*assign.right);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
"default_code" => {
|
"default_code" => {
|
||||||
if let syn::Expr::Lit(syn::ExprLit {
|
if let syn::Expr::Lit(syn::ExprLit {
|
||||||
lit: syn::Lit::Str(code),
|
lit: syn::Lit::Str(code),
|
||||||
@@ -366,7 +354,7 @@ mod field_info {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// uh not sure
|
// #[props(default)]
|
||||||
syn::Expr::Path(path) => {
|
syn::Expr::Path(path) => {
|
||||||
let name = path_to_single_string(&path.path)
|
let name = path_to_single_string(&path.path)
|
||||||
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
|
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
|
||||||
@@ -376,39 +364,41 @@ mod field_info {
|
|||||||
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(Error::new_spanned(
|
|
||||||
&path,
|
|
||||||
format!("Unknown parameter {:?}", name),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
"optional" => {
|
||||||
syn::Expr::Call(call) => {
|
self.default =
|
||||||
let subsetting_name = if let syn::Expr::Path(path) = &*call.func {
|
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
||||||
path_to_single_string(&path.path)
|
self.strip_option = true;
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
.ok_or_else(|| {
|
|
||||||
let call_func = &call.func;
|
|
||||||
let call_func = quote!(#call_func);
|
|
||||||
Error::new_spanned(
|
|
||||||
&call.func,
|
|
||||||
format!("Illegal builder setting group {}", call_func),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
match subsetting_name.as_ref() {
|
|
||||||
"setter" => {
|
|
||||||
for arg in call.args {
|
|
||||||
self.setter.apply_meta(arg)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(Error::new_spanned(
|
|
||||||
&call.func,
|
_ => {
|
||||||
format!("Illegal builder setting group name {}", subsetting_name),
|
macro_rules! handle_fields {
|
||||||
)),
|
( $( $flag:expr, $field:ident, $already:expr; )* ) => {
|
||||||
|
match name.as_str() {
|
||||||
|
$(
|
||||||
|
$flag => {
|
||||||
|
if self.$field {
|
||||||
|
Err(Error::new(path.span(), concat!("Illegal setting - field is already ", $already)))
|
||||||
|
} else {
|
||||||
|
self.$field = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
_ => Err(Error::new_spanned(
|
||||||
|
&path,
|
||||||
|
format!("Unknown setter parameter {:?}", name),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handle_fields!(
|
||||||
|
"skip", skip, "skipped";
|
||||||
|
"into", auto_into, "calling into() on the argument";
|
||||||
|
"strip_option", strip_option, "putting the argument in Some(...)";
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,75 +415,6 @@ mod field_info {
|
|||||||
self.default = None;
|
self.default = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(Error::new_spanned(path, "Unknown setting".to_owned())),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Error::new_spanned(
|
|
||||||
expr,
|
|
||||||
"Expected simple identifier".to_owned(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(Error::new_spanned(expr, "Expected (<...>=<...>)")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SetterSettings {
|
|
||||||
fn apply_meta(&mut self, expr: syn::Expr) -> Result<(), Error> {
|
|
||||||
match expr {
|
|
||||||
syn::Expr::Assign(assign) => {
|
|
||||||
let name = expr_to_single_string(&assign.left)
|
|
||||||
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
|
|
||||||
match name.as_str() {
|
|
||||||
"doc" => {
|
|
||||||
self.doc = Some(*assign.right);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => Err(Error::new_spanned(
|
|
||||||
&assign,
|
|
||||||
format!("Unknown parameter {:?}", name),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
syn::Expr::Path(path) => {
|
|
||||||
let name = path_to_single_string(&path.path)
|
|
||||||
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
|
|
||||||
macro_rules! handle_fields {
|
|
||||||
( $( $flag:expr, $field:ident, $already:expr; )* ) => {
|
|
||||||
match name.as_str() {
|
|
||||||
$(
|
|
||||||
$flag => {
|
|
||||||
if self.$field {
|
|
||||||
Err(Error::new(path.span(), concat!("Illegal setting - field is already ", $already)))
|
|
||||||
} else {
|
|
||||||
self.$field = true;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
_ => Err(Error::new_spanned(
|
|
||||||
&path,
|
|
||||||
format!("Unknown setter parameter {:?}", name),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handle_fields!(
|
|
||||||
"skip", skip, "skipped";
|
|
||||||
"into", auto_into, "calling into() on the argument";
|
|
||||||
"strip_option", strip_option, "putting the argument in Some(...)";
|
|
||||||
)
|
|
||||||
}
|
|
||||||
syn::Expr::Unary(syn::ExprUnary {
|
|
||||||
op: syn::UnOp::Not(_),
|
|
||||||
expr,
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
if let syn::Expr::Path(path) = *expr {
|
|
||||||
let name = path_to_single_string(&path.path)
|
|
||||||
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
|
|
||||||
match name.as_str() {
|
|
||||||
"doc" => {
|
"doc" => {
|
||||||
self.doc = None;
|
self.doc = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -551,7 +472,7 @@ mod struct_info {
|
|||||||
|
|
||||||
impl<'a> StructInfo<'a> {
|
impl<'a> StructInfo<'a> {
|
||||||
pub fn included_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
|
pub fn included_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
|
||||||
self.fields.iter().filter(|f| !f.builder_attr.setter.skip)
|
self.fields.iter().filter(|f| !f.builder_attr.skip)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@@ -837,14 +758,14 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
syn::GenericArgument::Type(ty_generics_tuple.into()),
|
syn::GenericArgument::Type(ty_generics_tuple.into()),
|
||||||
);
|
);
|
||||||
let (impl_generics, _, where_clause) = generics.split_for_impl();
|
let (impl_generics, _, where_clause) = generics.split_for_impl();
|
||||||
let doc = match field.builder_attr.setter.doc {
|
let doc = match field.builder_attr.doc {
|
||||||
Some(ref doc) => quote!(#[doc = #doc]),
|
Some(ref doc) => quote!(#[doc = #doc]),
|
||||||
None => quote!(),
|
None => quote!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of
|
// NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of
|
||||||
// nesting is different so we have to do this little dance.
|
// nesting is different so we have to do this little dance.
|
||||||
let arg_type = if field.builder_attr.setter.strip_option {
|
let arg_type = if field.builder_attr.strip_option {
|
||||||
let internal_type = field.type_from_inside_option().ok_or_else(|| {
|
let internal_type = field.type_from_inside_option().ok_or_else(|| {
|
||||||
Error::new_spanned(
|
Error::new_spanned(
|
||||||
&field_type,
|
&field_type,
|
||||||
@@ -855,7 +776,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
} else {
|
} else {
|
||||||
field_type
|
field_type
|
||||||
};
|
};
|
||||||
let (arg_type, arg_expr) = if field.builder_attr.setter.auto_into {
|
let (arg_type, arg_expr) = if field.builder_attr.auto_into {
|
||||||
(
|
(
|
||||||
quote!(impl core::convert::Into<#arg_type>),
|
quote!(impl core::convert::Into<#arg_type>),
|
||||||
quote!(#field_name.into()),
|
quote!(#field_name.into()),
|
||||||
@@ -863,7 +784,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
} else {
|
} else {
|
||||||
(quote!(#arg_type), quote!(#field_name))
|
(quote!(#arg_type), quote!(#field_name))
|
||||||
};
|
};
|
||||||
let arg_expr = if field.builder_attr.setter.strip_option {
|
let arg_expr = if field.builder_attr.strip_option {
|
||||||
quote!(Some(#arg_expr))
|
quote!(Some(#arg_expr))
|
||||||
} else {
|
} else {
|
||||||
arg_expr
|
arg_expr
|
||||||
@@ -1087,7 +1008,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
let assignments = self.fields.iter().map(|field| {
|
let assignments = self.fields.iter().map(|field| {
|
||||||
let name = &field.name;
|
let name = &field.name;
|
||||||
if let Some(ref default) = field.builder_attr.default {
|
if let Some(ref default) = field.builder_attr.default {
|
||||||
if field.builder_attr.setter.skip {
|
if field.builder_attr.skip {
|
||||||
quote!(let #name = #default;)
|
quote!(let #name = #default;)
|
||||||
} else {
|
} else {
|
||||||
quote!(let #name = #helper_trait_name::into_value(#name, || #default);)
|
quote!(let #name = #helper_trait_name::into_value(#name, || #default);)
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ impl Parse for ComponentField {
|
|||||||
return Ok(Self { name, content });
|
return Ok(Self { name, content });
|
||||||
}
|
}
|
||||||
|
|
||||||
if name.to_string() == "key" {
|
if name == "key" {
|
||||||
let content = ContentField::ManExpr(input.parse()?);
|
let content = ContentField::ManExpr(input.parse()?);
|
||||||
return Ok(Self { name, content });
|
return Ok(Self { name, content });
|
||||||
}
|
}
|
||||||
@@ -221,7 +221,7 @@ fn is_literal_foramtted(lit: &LitStr) -> bool {
|
|||||||
while let Some(next) = chars.next() {
|
while let Some(next) = chars.next() {
|
||||||
if next == '{' {
|
if next == '{' {
|
||||||
let nen = chars.next();
|
let nen = chars.next();
|
||||||
if nen == Some('{') {
|
if nen != Some('{') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
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.
|
||||||
|
|
||||||
@@ -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
|
/// 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.
|
/// where each listener is checked and fired if the event name matches.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use std::{
|
|||||||
/// - the `rsx!` macro
|
/// - the `rsx!` macro
|
||||||
/// - the [`NodeFactory`] API
|
/// - the [`NodeFactory`] API
|
||||||
pub enum VNode<'src> {
|
pub enum VNode<'src> {
|
||||||
/// Text VNodes simply bump-allocated (or static) string slices
|
/// Text VNodes are simply bump-allocated (or static) string slices
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
@@ -351,31 +351,25 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>;
|
|||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
#[derive(Default)]
|
||||||
pub struct EventHandler<'bump, T = ()> {
|
pub struct EventHandler<'bump, T = ()> {
|
||||||
pub callback: &'bump RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
pub callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> EventHandler<'_, T> {
|
impl<T> EventHandler<'_, T> {
|
||||||
|
/// Call this event handler with the appropriate event type
|
||||||
pub fn call(&self, event: T) {
|
pub fn call(&self, event: T) {
|
||||||
if let Some(callback) = self.callback.borrow_mut().as_mut() {
|
if let Some(callback) = self.callback.borrow_mut().as_mut() {
|
||||||
callback(event);
|
callback(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forcibly drop the internal handler callback, releasing memory
|
||||||
pub fn release(&self) {
|
pub fn release(&self) {
|
||||||
self.callback.replace(None);
|
self.callback.replace(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Copy for EventHandler<'_, T> {}
|
|
||||||
impl<T> Clone for EventHandler<'_, T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
callback: self.callback,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Virtual Components for custom user-defined components
|
/// Virtual Components for custom user-defined components
|
||||||
/// Only supports the functional syntax
|
/// Only supports the functional syntax
|
||||||
pub struct VComponent<'src> {
|
pub struct VComponent<'src> {
|
||||||
@@ -405,7 +399,7 @@ impl<P> AnyProps for VComponentProps<P> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Safety:
|
// 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
|
// 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)
|
// 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 {
|
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool {
|
||||||
@@ -677,7 +671,7 @@ impl<'a> NodeFactory<'a> {
|
|||||||
pub fn event_handler<T>(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> {
|
pub fn event_handler<T>(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> {
|
||||||
let handler: &mut dyn FnMut(T) = self.bump.alloc(f);
|
let handler: &mut dyn FnMut(T) = self.bump.alloc(f);
|
||||||
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
|
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
|
||||||
let callback = self.bump.alloc(RefCell::new(Some(caller)));
|
let callback = RefCell::new(Some(caller));
|
||||||
EventHandler { callback }
|
EventHandler { callback }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-10
@@ -70,7 +70,7 @@ impl ScopeArena {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Safety:
|
/// 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
|
/// - Scopes use interior mutability when sharing data into components
|
||||||
pub(crate) fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
pub(crate) fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||||
unsafe { self.scopes.borrow().get(&id).map(|f| &**f) }
|
unsafe { self.scopes.borrow().get(&id).map(|f| &**f) }
|
||||||
@@ -101,7 +101,7 @@ impl ScopeArena {
|
|||||||
let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten();
|
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.
|
We try to minimize the new allocations for props/arenas.
|
||||||
|
|
||||||
However, this will probably lead to some sort of fragmentation.
|
However, this will probably lead to some sort of fragmentation.
|
||||||
@@ -198,12 +198,9 @@ impl ScopeArena {
|
|||||||
// run the hooks (which hold an &mut Reference)
|
// run the hooks (which hold an &mut Reference)
|
||||||
// recursively call ensure_drop_safety on all children
|
// recursively call ensure_drop_safety on all children
|
||||||
items.borrowed_props.drain(..).for_each(|comp| {
|
items.borrowed_props.drain(..).for_each(|comp| {
|
||||||
let scope_id = comp
|
if let Some(scope_id) = comp.scope.get() {
|
||||||
.scope
|
self.ensure_drop_safety(scope_id);
|
||||||
.get()
|
}
|
||||||
.expect("VComponents should be associated with a valid Scope");
|
|
||||||
|
|
||||||
self.ensure_drop_safety(scope_id);
|
|
||||||
|
|
||||||
drop(comp.props.take());
|
drop(comp.props.take());
|
||||||
});
|
});
|
||||||
@@ -670,6 +667,60 @@ impl ScopeState {
|
|||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Provide a context for the root component from anywhere in your app.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust, ignore
|
||||||
|
/// struct SharedState(&'static str);
|
||||||
|
///
|
||||||
|
/// static App: Component = |cx| {
|
||||||
|
/// cx.use_hook(|_| cx.provide_root_context(SharedState("world")));
|
||||||
|
/// rsx!(cx, Child {})
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// static Child: Component = |cx| {
|
||||||
|
/// let state = cx.consume_state::<SharedState>();
|
||||||
|
/// rsx!(cx, div { "hello {state.0}" })
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn provide_root_context<T: 'static>(&self, value: T) -> Rc<T> {
|
||||||
|
let value = Rc::new(value);
|
||||||
|
|
||||||
|
// if we *are* the root component, then we can just provide the context directly
|
||||||
|
if self.scope_id() == ScopeId(0) {
|
||||||
|
self.shared_contexts
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(TypeId::of::<T>(), value.clone())
|
||||||
|
.map(|f| f.downcast::<T>().ok())
|
||||||
|
.flatten();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut search_parent = self.parent_scope;
|
||||||
|
|
||||||
|
while let Some(parent) = search_parent.take() {
|
||||||
|
let parent = unsafe { &*parent };
|
||||||
|
|
||||||
|
if parent.scope_id() == ScopeId(0) {
|
||||||
|
let exists = parent
|
||||||
|
.shared_contexts
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(TypeId::of::<T>(), value.clone());
|
||||||
|
|
||||||
|
if exists.is_some() {
|
||||||
|
log::warn!("Context already provided to parent scope - replacing it");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
search_parent = parent.parent_scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable!("all apps have a root scope")
|
||||||
|
}
|
||||||
|
|
||||||
/// Try to retrieve a SharedState with type T from the any parent Scope.
|
/// Try to retrieve a SharedState with type T from the any parent Scope.
|
||||||
pub fn consume_context<T: 'static>(&self) -> Option<Rc<T>> {
|
pub fn consume_context<T: 'static>(&self) -> Option<Rc<T>> {
|
||||||
if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
||||||
@@ -700,6 +751,11 @@ impl ScopeState {
|
|||||||
self.tasks.push_fut(fut)
|
self.tasks.push_fut(fut)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawns the future but does not return the TaskId
|
||||||
|
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
|
||||||
|
self.push_future(fut);
|
||||||
|
}
|
||||||
|
|
||||||
// todo: attach some state to the future to know if we should poll it
|
// todo: attach some state to the future to know if we should poll it
|
||||||
pub fn remove_future(&self, id: TaskId) {
|
pub fn remove_future(&self, id: TaskId) {
|
||||||
self.tasks.remove_fut(id);
|
self.tasks.remove_fut(id);
|
||||||
@@ -745,6 +801,7 @@ impl ScopeState {
|
|||||||
/// use_hook(|| Rc::new(RefCell::new(initial_value())))
|
/// use_hook(|| Rc::new(RefCell::new(initial_value())))
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
#[allow(clippy::mut_from_ref)]
|
||||||
pub fn use_hook<'src, State: 'static>(
|
pub fn use_hook<'src, State: 'static>(
|
||||||
&'src self,
|
&'src self,
|
||||||
initializer: impl FnOnce(usize) -> State,
|
initializer: impl FnOnce(usize) -> State,
|
||||||
@@ -839,13 +896,15 @@ impl ScopeState {
|
|||||||
self.frames[0].reset();
|
self.frames[0].reset();
|
||||||
self.frames[1].reset();
|
self.frames[1].reset();
|
||||||
|
|
||||||
// Finally, free up the hook values
|
// Free up the hook values
|
||||||
self.hook_arena.reset();
|
|
||||||
self.hook_vals.get_mut().drain(..).for_each(|state| {
|
self.hook_vals.get_mut().drain(..).for_each(|state| {
|
||||||
let as_mut = unsafe { &mut *state };
|
let as_mut = unsafe { &mut *state };
|
||||||
let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) };
|
let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) };
|
||||||
drop(boxed);
|
drop(boxed);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Finally, clear the hook arena
|
||||||
|
self.hook_arena.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::innerlude::*;
|
|||||||
pub struct ElementIdIterator<'a> {
|
pub struct ElementIdIterator<'a> {
|
||||||
vdom: &'a VirtualDom,
|
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
|
// 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]>,
|
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use fxhash::FxHashSet;
|
|||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use std::{collections::VecDeque, iter::FromIterator, task::Poll};
|
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
|
/// ## Guide
|
||||||
@@ -233,7 +233,7 @@ impl VirtualDom {
|
|||||||
|
|
||||||
/// Get the [`Scope`] for the root component.
|
/// 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.
|
/// directly.
|
||||||
///
|
///
|
||||||
/// This method is equivalent to calling `get_scope(ScopeId(0))`
|
/// This method is equivalent to calling `get_scope(ScopeId(0))`
|
||||||
|
|||||||
@@ -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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { path = "../core", version ="^0.1.7", features = ["serialize"] }
|
dioxus-core = { path = "../core", version = "^0.1.7", features = ["serialize"] }
|
||||||
argh = "0.1.4"
|
argh = "0.1.4"
|
||||||
serde = "1.0.120"
|
serde = "1.0.120"
|
||||||
serde_json = "1.0.61"
|
serde_json = "1.0.61"
|
||||||
@@ -27,8 +27,10 @@ tokio = { version = "1.12.0", features = [
|
|||||||
"rt",
|
"rt",
|
||||||
"time",
|
"time",
|
||||||
], optional = true, default-features = false }
|
], optional = true, default-features = false }
|
||||||
dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"}
|
dioxus-core-macro = { path = "../core-macro", version = "^0.1.6" }
|
||||||
dioxus-html = { path = "../html", features = ["serialize"], version ="^0.1.4"}
|
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.4" }
|
||||||
|
webbrowser = "0.5.5"
|
||||||
|
mime_guess = "2.0.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["tokio_runtime"]
|
default = ["tokio_runtime"]
|
||||||
|
|||||||
+43
-13
@@ -1,17 +1,29 @@
|
|||||||
use dioxus_core::DomEdit;
|
|
||||||
use wry::{
|
use wry::{
|
||||||
application::{event_loop::EventLoop, window::WindowBuilder},
|
application::{
|
||||||
webview::WebView,
|
event_loop::EventLoop,
|
||||||
|
window::{Window, WindowBuilder},
|
||||||
|
},
|
||||||
|
http::{Request as HttpRequest, Response as HttpResponse},
|
||||||
|
webview::{FileDropEvent, WebView},
|
||||||
|
Result as WryResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct DesktopConfig<'a> {
|
pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
|
||||||
|
|
||||||
|
pub struct DesktopConfig {
|
||||||
pub window: WindowBuilder,
|
pub window: WindowBuilder,
|
||||||
pub(crate) manual_edits: Option<Vec<DomEdit<'a>>>,
|
pub file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
|
||||||
|
pub protocos: Vec<WryProtocl>,
|
||||||
pub(crate) pre_rendered: Option<String>,
|
pub(crate) pre_rendered: Option<String>,
|
||||||
pub(crate) event_handler: Option<Box<dyn Fn(&mut EventLoop<()>, &mut WebView)>>,
|
pub(crate) event_handler: Option<Box<DynEventHandlerFn>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DesktopConfig<'a> {
|
pub type WryProtocl = (
|
||||||
|
String,
|
||||||
|
Box<dyn Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl DesktopConfig {
|
||||||
/// Initializes a new `WindowBuilder` with default values.
|
/// Initializes a new `WindowBuilder` with default values.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@@ -19,16 +31,12 @@ impl<'a> DesktopConfig<'a> {
|
|||||||
Self {
|
Self {
|
||||||
event_handler: None,
|
event_handler: None,
|
||||||
window,
|
window,
|
||||||
|
protocos: Vec::new(),
|
||||||
|
file_drop_handler: None,
|
||||||
pre_rendered: None,
|
pre_rendered: None,
|
||||||
manual_edits: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_edits(&mut self, edits: Vec<DomEdit<'a>>) -> &mut Self {
|
|
||||||
self.manual_edits = Some(edits);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_prerendered(&mut self, content: String) -> &mut Self {
|
pub fn with_prerendered(&mut self, content: String) -> &mut Self {
|
||||||
self.pre_rendered = Some(content);
|
self.pre_rendered = Some(content);
|
||||||
self
|
self
|
||||||
@@ -54,4 +62,26 @@ impl<'a> DesktopConfig<'a> {
|
|||||||
self.event_handler = Some(Box::new(handler));
|
self.event_handler = Some(Box::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_file_drop_handler(
|
||||||
|
&mut self,
|
||||||
|
handler: impl Fn(&Window, FileDropEvent) -> bool + 'static,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.file_drop_handler = Some(Box::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_custom_protocol<F>(&mut self, name: String, handler: F) -> &mut Self
|
||||||
|
where
|
||||||
|
F: Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static,
|
||||||
|
{
|
||||||
|
self.protocos.push((name, Box::new(handler)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DesktopConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Sen
|
|||||||
let evt = serde_json::from_value::<KeyboardData>(val).unwrap();
|
let evt = serde_json::from_value::<KeyboardData>(val).unwrap();
|
||||||
Arc::new(evt)
|
Arc::new(evt)
|
||||||
}
|
}
|
||||||
"focus" | "blur" => {
|
"focus" | "blur" | "focusout" | "focusin" => {
|
||||||
//
|
//
|
||||||
Arc::new(FocusData {})
|
Arc::new(FocusData {})
|
||||||
}
|
}
|
||||||
@@ -117,6 +117,8 @@ fn event_name_from_typ(typ: &str) -> &'static str {
|
|||||||
"keypress" => "keypress",
|
"keypress" => "keypress",
|
||||||
"keyup" => "keyup",
|
"keyup" => "keyup",
|
||||||
"focus" => "focus",
|
"focus" => "focus",
|
||||||
|
"focusout" => "focusout",
|
||||||
|
"focusin" => "focusin",
|
||||||
"blur" => "blur",
|
"blur" => "blur",
|
||||||
"change" => "change",
|
"change" => "change",
|
||||||
"input" => "input",
|
"input" => "input",
|
||||||
|
|||||||
@@ -12,7 +12,11 @@
|
|||||||
<div id="main">
|
<div id="main">
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
<script type="text/javascript" src="index.js">
|
|
||||||
|
<script>
|
||||||
|
import("./index.js").then(function (module) {
|
||||||
|
module.main();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,443 +0,0 @@
|
|||||||
const bool_attrs = [
|
|
||||||
"allowfullscreen",
|
|
||||||
"allowpaymentrequest",
|
|
||||||
"async",
|
|
||||||
"autofocus",
|
|
||||||
"autoplay",
|
|
||||||
"checked",
|
|
||||||
"controls",
|
|
||||||
"default",
|
|
||||||
"defer",
|
|
||||||
"disabled",
|
|
||||||
"formnovalidate",
|
|
||||||
"hidden",
|
|
||||||
"ismap",
|
|
||||||
"itemscope",
|
|
||||||
"loop",
|
|
||||||
"multiple",
|
|
||||||
"muted",
|
|
||||||
"nomodule",
|
|
||||||
"novalidate",
|
|
||||||
"open",
|
|
||||||
"playsinline",
|
|
||||||
"readonly",
|
|
||||||
"required",
|
|
||||||
"reversed",
|
|
||||||
"selected",
|
|
||||||
"truespeed",
|
|
||||||
]
|
|
||||||
|
|
||||||
function serialize_event(event) {
|
|
||||||
switch (event.type) {
|
|
||||||
case "copy":
|
|
||||||
case "cut":
|
|
||||||
case "past":
|
|
||||||
return {};
|
|
||||||
|
|
||||||
case "compositionend":
|
|
||||||
case "compositionstart":
|
|
||||||
case "compositionupdate":
|
|
||||||
return {
|
|
||||||
data: event.data,
|
|
||||||
};
|
|
||||||
|
|
||||||
case "keydown":
|
|
||||||
case "keypress":
|
|
||||||
case "keyup":
|
|
||||||
return {
|
|
||||||
char_code: event.charCode,
|
|
||||||
key: event.key,
|
|
||||||
alt_key: event.altKey,
|
|
||||||
ctrl_key: event.ctrlKey,
|
|
||||||
meta_key: event.metaKey,
|
|
||||||
key_code: event.keyCode,
|
|
||||||
shift_key: event.shiftKey,
|
|
||||||
locale: "locale",
|
|
||||||
location: event.location,
|
|
||||||
repeat: event.repeat,
|
|
||||||
which: event.which,
|
|
||||||
// locale: event.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":
|
|
||||||
return {
|
|
||||||
alt_key: event.altKey,
|
|
||||||
button: event.button,
|
|
||||||
buttons: event.buttons,
|
|
||||||
client_x: event.clientX,
|
|
||||||
client_y: event.clientY,
|
|
||||||
ctrl_key: event.ctrlKey,
|
|
||||||
meta_key: event.metaKey,
|
|
||||||
page_x: event.pageX,
|
|
||||||
page_y: event.pageY,
|
|
||||||
screen_x: event.screenX,
|
|
||||||
screen_y: event.screenY,
|
|
||||||
shift_key: event.shiftKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
case "pointerdown":
|
|
||||||
case "pointermove":
|
|
||||||
case "pointerup":
|
|
||||||
case "pointercancel":
|
|
||||||
case "gotpointercapture":
|
|
||||||
case "lostpointercapture":
|
|
||||||
case "pointerenter":
|
|
||||||
case "pointerleave":
|
|
||||||
case "pointerover":
|
|
||||||
case "pointerout":
|
|
||||||
return {
|
|
||||||
alt_key: event.altKey,
|
|
||||||
button: event.button,
|
|
||||||
buttons: event.buttons,
|
|
||||||
client_x: event.clientX,
|
|
||||||
client_y: event.clientY,
|
|
||||||
ctrl_key: event.ctrlKey,
|
|
||||||
meta_key: event.metaKey,
|
|
||||||
page_x: event.pageX,
|
|
||||||
page_y: event.pageY,
|
|
||||||
screen_x: event.screenX,
|
|
||||||
screen_y: event.screenY,
|
|
||||||
shift_key: event.shiftKey,
|
|
||||||
pointer_id: event.pointerId,
|
|
||||||
width: event.width,
|
|
||||||
height: event.height,
|
|
||||||
pressure: event.pressure,
|
|
||||||
tangential_pressure: event.tangentialPressure,
|
|
||||||
tilt_x: event.tiltX,
|
|
||||||
tilt_y: event.tiltY,
|
|
||||||
twist: event.twist,
|
|
||||||
pointer_type: event.pointerType,
|
|
||||||
is_primary: event.isPrimary,
|
|
||||||
};
|
|
||||||
|
|
||||||
case "select":
|
|
||||||
return {};
|
|
||||||
|
|
||||||
case "touchcancel":
|
|
||||||
case "touchend":
|
|
||||||
case "touchmove":
|
|
||||||
case "touchstart":
|
|
||||||
return {
|
|
||||||
alt_key: event.altKey,
|
|
||||||
ctrl_key: event.ctrlKey,
|
|
||||||
meta_key: event.metaKey,
|
|
||||||
shift_key: event.shiftKey,
|
|
||||||
|
|
||||||
// changed_touches: event.changedTouches,
|
|
||||||
// target_touches: event.targetTouches,
|
|
||||||
// touches: event.touches,
|
|
||||||
};
|
|
||||||
|
|
||||||
case "scroll":
|
|
||||||
return {};
|
|
||||||
|
|
||||||
case "wheel":
|
|
||||||
return {
|
|
||||||
delta_x: event.deltaX,
|
|
||||||
delta_y: event.deltaY,
|
|
||||||
delta_z: event.deltaZ,
|
|
||||||
delta_mode: event.deltaMode,
|
|
||||||
};
|
|
||||||
|
|
||||||
case "animationstart":
|
|
||||||
case "animationend":
|
|
||||||
case "animationiteration":
|
|
||||||
return {
|
|
||||||
animation_name: event.animationName,
|
|
||||||
elapsed_time: event.elapsedTime,
|
|
||||||
pseudo_element: event.pseudoElement,
|
|
||||||
};
|
|
||||||
|
|
||||||
case "transitionend":
|
|
||||||
return {
|
|
||||||
property_name: event.propertyName,
|
|
||||||
elapsed_time: event.elapsedTime,
|
|
||||||
pseudo_element: event.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 {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Interpreter {
|
|
||||||
constructor(root) {
|
|
||||||
this.root = root;
|
|
||||||
this.stack = [root];
|
|
||||||
this.listeners = {
|
|
||||||
onclick: {},
|
|
||||||
};
|
|
||||||
this.lastNodeWasText = false;
|
|
||||||
this.nodes = [root];
|
|
||||||
}
|
|
||||||
|
|
||||||
top() {
|
|
||||||
return this.stack[this.stack.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
pop() {
|
|
||||||
return this.stack.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
PushRoot(edit) {
|
|
||||||
const id = edit.root;
|
|
||||||
const node = this.nodes[id];
|
|
||||||
this.stack.push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppendChildren(edit) {
|
|
||||||
let root = this.stack[this.stack.length - (1 + edit.many)];
|
|
||||||
|
|
||||||
let to_add = this.stack.splice(this.stack.length - edit.many);
|
|
||||||
|
|
||||||
for (let i = 0; i < edit.many; i++) {
|
|
||||||
root.appendChild(to_add[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReplaceWith(edit) {
|
|
||||||
let root = this.nodes[edit.root];
|
|
||||||
let els = this.stack.splice(this.stack.length - edit.m);
|
|
||||||
|
|
||||||
root.replaceWith(...els);
|
|
||||||
}
|
|
||||||
|
|
||||||
InsertAfter(edit) {
|
|
||||||
let old = this.nodes[edit.root];
|
|
||||||
let new_nodes = this.stack.splice(this.stack.length - edit.n);
|
|
||||||
old.after(...new_nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
InsertBefore(edit) {
|
|
||||||
let old = this.nodes[edit.root];
|
|
||||||
let new_nodes = this.stack.splice(this.stack.length - edit.n);
|
|
||||||
old.before(...new_nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
Remove(edit) {
|
|
||||||
let node = this.nodes[edit.root];
|
|
||||||
if (node !== undefined) {
|
|
||||||
node.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateTextNode(edit) {
|
|
||||||
const node = document.createTextNode(edit.text);
|
|
||||||
this.nodes[edit.root] = node;
|
|
||||||
this.stack.push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateElement(edit) {
|
|
||||||
const tagName = edit.tag;
|
|
||||||
const el = document.createElement(tagName);
|
|
||||||
this.nodes[edit.root] = el;
|
|
||||||
el.setAttribute("dioxus-id", edit.root);
|
|
||||||
this.stack.push(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateElementNs(edit) {
|
|
||||||
let el = document.createElementNS(edit.ns, edit.tag);
|
|
||||||
this.stack.push(el);
|
|
||||||
this.nodes[edit.root] = el;
|
|
||||||
}
|
|
||||||
|
|
||||||
CreatePlaceholder(edit) {
|
|
||||||
let el = document.createElement("pre");
|
|
||||||
this.stack.push(el);
|
|
||||||
this.nodes[edit.root] = el;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveEventListener(edit) {}
|
|
||||||
|
|
||||||
NewEventListener(edit) {
|
|
||||||
const event_name = edit.event_name;
|
|
||||||
const mounted_node_id = edit.root;
|
|
||||||
const scope = edit.scope;
|
|
||||||
|
|
||||||
const element = this.nodes[edit.root];
|
|
||||||
element.setAttribute(
|
|
||||||
`dioxus-event-${event_name}`,
|
|
||||||
`${scope}.${mounted_node_id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.listeners[event_name] === undefined) {
|
|
||||||
this.listeners[event_name] = true;
|
|
||||||
|
|
||||||
this.root.addEventListener(event_name, (event) => {
|
|
||||||
const target = event.target;
|
|
||||||
const real_id = target.getAttribute(`dioxus-id`);
|
|
||||||
|
|
||||||
const should_prevent_default = target.getAttribute(
|
|
||||||
`dioxus-prevent-default`
|
|
||||||
);
|
|
||||||
|
|
||||||
let contents = serialize_event(event);
|
|
||||||
|
|
||||||
if (should_prevent_default === `on${event.type}`) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (real_id == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc.call("user_event", {
|
|
||||||
event: event_name,
|
|
||||||
mounted_dom_id: parseInt(real_id),
|
|
||||||
contents: contents,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SetText(edit) {
|
|
||||||
this.nodes[edit.root].textContent = edit.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetAttribute(edit) {
|
|
||||||
// console.log("setting attr", edit);
|
|
||||||
const name = edit.field;
|
|
||||||
const value = edit.value;
|
|
||||||
const ns = edit.ns;
|
|
||||||
const node = this.nodes[edit.root];
|
|
||||||
|
|
||||||
if (ns == "style") {
|
|
||||||
node.style[name] = value;
|
|
||||||
} else if (ns != null || ns != undefined) {
|
|
||||||
node.setAttributeNS(ns, name, value);
|
|
||||||
} else {
|
|
||||||
switch (name) {
|
|
||||||
case "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.indexOf(name)) {
|
|
||||||
node.removeAttribute(name);
|
|
||||||
} else {
|
|
||||||
node.setAttribute(name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RemoveAttribute(edit) {
|
|
||||||
const name = edit.field;
|
|
||||||
const node = this.nodes[edit.root];
|
|
||||||
node.removeAttribute(name);
|
|
||||||
|
|
||||||
if (name === "value") {
|
|
||||||
node.value = null;
|
|
||||||
}
|
|
||||||
if (name === "checked") {
|
|
||||||
node.checked = false;
|
|
||||||
}
|
|
||||||
if (name === "selected") {
|
|
||||||
node.selected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEdits(edits) {
|
|
||||||
this.stack.push(this.root);
|
|
||||||
|
|
||||||
for (let x = 0; x < edits.length; x++) {
|
|
||||||
let edit = edits[x];
|
|
||||||
let f = this[edit.type];
|
|
||||||
f.call(this, edit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
let root = window.document.getElementById("main");
|
|
||||||
window.interpreter = new Interpreter(root);
|
|
||||||
rpc.call("initialize");
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
+73
-21
@@ -116,7 +116,7 @@ pub fn launch(root: Component) {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn launch_cfg(
|
pub fn launch_cfg(
|
||||||
root: Component,
|
root: Component,
|
||||||
config_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
|
config_builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig,
|
||||||
) {
|
) {
|
||||||
launch_with_props(root, (), config_builder)
|
launch_with_props(root, (), config_builder)
|
||||||
}
|
}
|
||||||
@@ -147,9 +147,9 @@ pub fn launch_cfg(
|
|||||||
pub fn launch_with_props<P: 'static + Send>(
|
pub fn launch_with_props<P: 'static + Send>(
|
||||||
root: Component<P>,
|
root: Component<P>,
|
||||||
props: P,
|
props: P,
|
||||||
builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
|
builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig,
|
||||||
) {
|
) {
|
||||||
let mut cfg = DesktopConfig::new();
|
let mut cfg = DesktopConfig::default();
|
||||||
builder(&mut cfg);
|
builder(&mut cfg);
|
||||||
|
|
||||||
let event_loop = EventLoop::with_user_event();
|
let event_loop = EventLoop::with_user_event();
|
||||||
@@ -170,9 +170,11 @@ pub fn launch_with_props<P: 'static + Send>(
|
|||||||
let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone());
|
let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone());
|
||||||
|
|
||||||
let proxy = proxy.clone();
|
let proxy = proxy.clone();
|
||||||
let webview = WebViewBuilder::new(window)
|
let file_handler = cfg.file_drop_handler.take();
|
||||||
|
|
||||||
|
let mut webview = WebViewBuilder::new(window)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_url("wry://index.html/")
|
.with_url("dioxus://index.html/")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_rpc_handler(move |_window: &Window, req: RpcRequest| {
|
.with_rpc_handler(move |_window: &Window, req: RpcRequest| {
|
||||||
match req.method.as_str() {
|
match req.method.as_str() {
|
||||||
@@ -185,30 +187,80 @@ pub fn launch_with_props<P: 'static + Send>(
|
|||||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
let _ = proxy.send_event(UserWindowEvent::Update);
|
let _ = proxy.send_event(UserWindowEvent::Update);
|
||||||
}
|
}
|
||||||
|
"browser_open" => {
|
||||||
|
let data = req.params.unwrap();
|
||||||
|
log::trace!("Open browser: {:?}", data);
|
||||||
|
if let Some(arr) = data.as_array() {
|
||||||
|
if let Some(temp) = arr[0].as_object() {
|
||||||
|
if temp.contains_key("href") {
|
||||||
|
let url = temp.get("href").unwrap().as_str().unwrap();
|
||||||
|
if let Err(e) = webbrowser::open(url) {
|
||||||
|
log::error!("Open Browser error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
.with_custom_protocol("wry".into(), move |request| {
|
.with_custom_protocol(String::from("dioxus"), move |request| {
|
||||||
// Any content that that uses the `wry://` scheme will be shuttled through this handler as a "special case"
|
// Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case"
|
||||||
// For now, we only serve two pieces of content which get included as bytes into the final binary.
|
// For now, we only serve two pieces of content which get included as bytes into the final binary.
|
||||||
let path = request.uri().replace("wry://", "");
|
let path = request.uri().replace("dioxus://", "");
|
||||||
let (data, meta) = match path.as_str() {
|
|
||||||
"index.html" | "index.html/" | "/index.html" => {
|
|
||||||
(include_bytes!("./index.html").to_vec(), "text/html")
|
|
||||||
}
|
|
||||||
"index.html/index.js" => {
|
|
||||||
(include_bytes!("./index.js").to_vec(), "text/javascript")
|
|
||||||
}
|
|
||||||
_ => (include_bytes!("./index.html").to_vec(), "text/html"),
|
|
||||||
};
|
|
||||||
|
|
||||||
wry::http::ResponseBuilder::new().mimetype(meta).body(data)
|
// 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 trimmed == "index.js" {
|
||||||
|
wry::http::ResponseBuilder::new()
|
||||||
|
.mimetype("text/javascript")
|
||||||
|
.body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec())
|
||||||
|
} else {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.with_file_drop_handler(move |window, evet| {
|
||||||
.unwrap();
|
file_handler
|
||||||
|
.as_ref()
|
||||||
|
.map(|handler| handler(window, evet))
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
|
||||||
desktop.webviews.insert(window_id, webview);
|
for (name, handler) in cfg.protocos.drain(..) {
|
||||||
|
webview = webview.with_custom_protocol(name, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
desktop.webviews.insert(window_id, webview.build().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::WindowEvent {
|
Event::WindowEvent {
|
||||||
|
|||||||
+22
-42
@@ -16,45 +16,25 @@ pub use usefuture::*;
|
|||||||
mod usesuspense;
|
mod usesuspense;
|
||||||
pub use usesuspense::*;
|
pub use usesuspense::*;
|
||||||
|
|
||||||
// #[macro_export]
|
#[macro_export]
|
||||||
// macro_rules! to_owned {
|
/// A helper macro for using hooks in async environements.
|
||||||
// ($($es:ident),+) => {$(
|
///
|
||||||
// #[allow(unused_mut)]
|
/// # Usage
|
||||||
// let mut $es = $es.to_owned();
|
///
|
||||||
// )*}
|
///
|
||||||
// }
|
/// ```
|
||||||
|
/// let (data) = use_ref(&cx, || {});
|
||||||
// /// Calls `for_async` on the series of paramters.
|
///
|
||||||
// ///
|
/// let handle_thing = move |_| {
|
||||||
// /// If the type is Clone, then it will be cloned. However, if the type is not `clone`
|
/// to_owned![data]
|
||||||
// /// then it must have a `for_async` method for Rust to lower down into.
|
/// cx.spawn(async move {
|
||||||
// ///
|
/// // do stuff
|
||||||
// /// See: how use_state implements `for_async` but *not* through the trait.
|
/// });
|
||||||
// #[macro_export]
|
/// }
|
||||||
// macro_rules! for_async {
|
/// ```
|
||||||
// ($($es:ident),+) => {$(
|
macro_rules! to_owned {
|
||||||
// #[allow(unused_mut)]
|
($($es:ident),+) => {$(
|
||||||
// let mut $es = $es.for_async();
|
#[allow(unused_mut)]
|
||||||
// )*}
|
let mut $es = $es.to_owned();
|
||||||
// }
|
)*}
|
||||||
|
}
|
||||||
// /// 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<T> AsyncHook for T
|
|
||||||
// where
|
|
||||||
// T: ToOwned<Owned = T>,
|
|
||||||
// {
|
|
||||||
// type Output = T;
|
|
||||||
// fn for_async(self) -> Self::Output {
|
|
||||||
// self
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -171,7 +171,7 @@ where
|
|||||||
///
|
///
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
pub fn use_context_provider<'a, T: 'static>(cx: &'a ScopeState, f: impl FnOnce() -> T) {
|
pub fn use_context_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) {
|
||||||
cx.use_hook(|_| {
|
cx.use_hook(|_| {
|
||||||
let state: ProvidedState<T> = RefCell::new(ProvidedStateInner {
|
let state: ProvidedState<T> = RefCell::new(ProvidedStateInner {
|
||||||
value: Rc::new(RefCell::new(f())),
|
value: Rc::new(RefCell::new(f())),
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ let g = use_coroutine(&cx, || {
|
|||||||
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
pub fn use_coroutine<'a, F>(
|
pub fn use_coroutine<F>(cx: &ScopeState, create_future: impl FnOnce() -> F) -> CoroutineHandle<'_>
|
||||||
cx: &'a ScopeState,
|
|
||||||
create_future: impl FnOnce() -> F,
|
|
||||||
) -> CoroutineHandle<'a>
|
|
||||||
where
|
where
|
||||||
F: Future<Output = ()> + 'static,
|
F: Future<Output = ()> + 'static,
|
||||||
{
|
{
|
||||||
@@ -98,6 +95,7 @@ impl Clone for CoroutineHandle<'_> {
|
|||||||
impl Copy for CoroutineHandle<'_> {}
|
impl Copy for CoroutineHandle<'_> {}
|
||||||
|
|
||||||
impl<'a> CoroutineHandle<'a> {
|
impl<'a> CoroutineHandle<'a> {
|
||||||
|
#[allow(clippy::needless_return)]
|
||||||
pub fn start(&self) {
|
pub fn start(&self) {
|
||||||
if self.is_running() {
|
if self.is_running() {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ use std::{
|
|||||||
use dioxus_core::ScopeState;
|
use dioxus_core::ScopeState;
|
||||||
|
|
||||||
pub fn use_ref<'a, T: 'static>(cx: &'a ScopeState, f: impl FnOnce() -> T) -> &'a UseRef<T> {
|
pub fn use_ref<'a, T: 'static>(cx: &'a ScopeState, f: impl FnOnce() -> T) -> &'a UseRef<T> {
|
||||||
let inner = cx.use_hook(|_| UseRef {
|
cx.use_hook(|_| UseRef {
|
||||||
update_callback: cx.schedule_update(),
|
update_callback: cx.schedule_update(),
|
||||||
value: Rc::new(RefCell::new(f())),
|
value: Rc::new(RefCell::new(f())),
|
||||||
});
|
})
|
||||||
|
|
||||||
inner
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UseRef<T> {
|
pub struct UseRef<T> {
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
#![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<T>) {
|
||||||
|
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.as_ref(), hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UseState<T: 'static> {
|
||||||
|
pub(crate) current_val: Rc<T>,
|
||||||
|
pub(crate) update_callback: Rc<dyn Fn()>,
|
||||||
|
pub(crate) setter: Rc<dyn Fn(T)>,
|
||||||
|
pub(crate) slot: Rc<RefCell<Rc<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> UseState<T> {
|
||||||
|
/// 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<T> {
|
||||||
|
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<dyn Fn(T)>` 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<dyn Fn(T)> {
|
||||||
|
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 curernt = self.slot.borrow();
|
||||||
|
let new_val = f(curernt.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<T> {
|
||||||
|
&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<T: Clone> UseState<T> {
|
||||||
|
/// 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<T> {
|
||||||
|
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<T: 'static> ToOwned for UseState<T> {
|
||||||
|
type Owned = UseState<T>;
|
||||||
|
|
||||||
|
fn to_owned(&self) -> Self::Owned {
|
||||||
|
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<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.current_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq<UseState<T>> for UseState<T> {
|
||||||
|
fn eq(&self, other: &UseState<T>) -> bool {
|
||||||
|
// some level of memoization for UseState
|
||||||
|
Rc::ptr_eq(&self.slot, &other.slot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug> Debug for UseState<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{:?}", self.current_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> std::ops::Deref for UseState<T> {
|
||||||
|
type Target = Rc<dyn Fn(T)>;
|
||||||
|
|
||||||
|
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")))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<T>);
|
|
||||||
|
|
||||||
impl<T> Copy for UseState<'_, T> {}
|
|
||||||
|
|
||||||
impl<'a, T: 'static> Clone for UseState<'a, T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
UseState(self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Debug> 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<T> {
|
|
||||||
&self.0.current_val
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current status of the work-in-progress data
|
|
||||||
pub fn get_wip(&self) -> Ref<Option<T>> {
|
|
||||||
self.0.wip.borrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current status of the work-in-progress data
|
|
||||||
pub fn get_wip_mut(&self) -> RefMut<Option<T>> {
|
|
||||||
self.0.wip.borrow_mut()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn classic(self) -> (&'a T, Rc<dyn Fn(T)>) {
|
|
||||||
(&self.0.current_val, self.setter())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setter(&self) -> Rc<dyn Fn(T)> {
|
|
||||||
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<T> {
|
|
||||||
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<Owned = T>> 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<V>> PartialEq<V> for UseState<'a, T> {
|
|
||||||
fn eq(&self, other: &V) -> bool {
|
|
||||||
self.get() == other
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a, O, T: std::ops::Not<Output = O> + 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<T, Output = T>> Add<T> 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<T, Output = T>> AddAssign<T> 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<T, Output = T>> Sub<T> 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<T, Output = T>> SubAssign<T> 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<T, Output = T>> Mul<T> 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<T, Output = T>> MulAssign<T> 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<T, Output = T>> Div<T> 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<T, Output = T>> DivAssign<T> for UseState<'a, T> {
|
|
||||||
fn div_assign(&mut self, rhs: T) {
|
|
||||||
self.set(self.0.current_val.div(rhs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
use std::{
|
|
||||||
cell::{Cell, Ref, RefCell, RefMut},
|
|
||||||
fmt::{Debug, Display},
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
pub struct UseStateOwned<T: 'static> {
|
|
||||||
// this will always be outdated
|
|
||||||
pub(crate) current_val: Rc<T>,
|
|
||||||
pub(crate) wip: Rc<RefCell<Option<T>>>,
|
|
||||||
pub(crate) update_callback: Rc<dyn Fn()>,
|
|
||||||
pub(crate) update_scheuled: Cell<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> UseStateOwned<T> {
|
|
||||||
pub fn get(&self) -> Ref<Option<T>> {
|
|
||||||
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<Option<T>> {
|
|
||||||
(self.update_callback)();
|
|
||||||
self.wip.borrow_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
|
|
||||||
|
|
||||||
impl<T: Debug> Debug for UseStateOwned<T> {
|
|
||||||
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<T> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.current_val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T: Copy + Add<T, Output = T>> Add<T> for UseStateOwned<T> {
|
|
||||||
type Output = T;
|
|
||||||
|
|
||||||
fn add(self, rhs: T) -> Self::Output {
|
|
||||||
self.current_val.add(rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T: Copy + Add<T, Output = T>> AddAssign<T> for UseStateOwned<T> {
|
|
||||||
fn add_assign(&mut self, rhs: T) {
|
|
||||||
self.set(self.current_val.add(rhs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sub
|
|
||||||
impl<'a, T: Copy + Sub<T, Output = T>> Sub<T> for UseStateOwned<T> {
|
|
||||||
type Output = T;
|
|
||||||
|
|
||||||
fn sub(self, rhs: T) -> Self::Output {
|
|
||||||
self.current_val.sub(rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a, T: Copy + Sub<T, Output = T>> SubAssign<T> for UseStateOwned<T> {
|
|
||||||
fn sub_assign(&mut self, rhs: T) {
|
|
||||||
self.set(self.current_val.sub(rhs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MUL
|
|
||||||
impl<'a, T: Copy + Mul<T, Output = T>> Mul<T> for UseStateOwned<T> {
|
|
||||||
type Output = T;
|
|
||||||
|
|
||||||
fn mul(self, rhs: T) -> Self::Output {
|
|
||||||
self.current_val.mul(rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a, T: Copy + Mul<T, Output = T>> MulAssign<T> for UseStateOwned<T> {
|
|
||||||
fn mul_assign(&mut self, rhs: T) {
|
|
||||||
self.set(self.current_val.mul(rhs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// DIV
|
|
||||||
impl<'a, T: Copy + Div<T, Output = T>> Div<T> for UseStateOwned<T> {
|
|
||||||
type Output = T;
|
|
||||||
|
|
||||||
fn div(self, rhs: T) -> Self::Output {
|
|
||||||
self.current_val.div(rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a, T: Copy + Div<T, Output = T>> DivAssign<T> for UseStateOwned<T> {
|
|
||||||
fn div_assign(&mut self, rhs: T) {
|
|
||||||
self.set(self.current_val.div(rhs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ pub fn use_suspense<R: 'static, F: Future<Output = R> + 'static>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some(value) = sus.value.as_ref() {
|
if let Some(value) = sus.value.as_ref() {
|
||||||
render(&value)
|
render(value)
|
||||||
} else {
|
} else {
|
||||||
// generate a placeholder node if the future isnt ready
|
// generate a placeholder node if the future isnt ready
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ pub mod on {
|
|||||||
/// onfocus
|
/// onfocus
|
||||||
onfocus
|
onfocus
|
||||||
|
|
||||||
|
// onfocusout
|
||||||
|
onfocusout
|
||||||
|
|
||||||
|
// onfocusin
|
||||||
|
onfocusin
|
||||||
|
|
||||||
/// onblur
|
/// onblur
|
||||||
onblur
|
onblur
|
||||||
];
|
];
|
||||||
@@ -1092,7 +1098,7 @@ pub(crate) fn _event_meta(event: &UserEvent) -> (bool, EventPriority) {
|
|||||||
"keydown" | "keypress" | "keyup" => (true, High),
|
"keydown" | "keypress" | "keyup" => (true, High),
|
||||||
|
|
||||||
// Focus
|
// Focus
|
||||||
"focus" | "blur" => (true, Low),
|
"focus" | "blur" | "focusout" | "focusin" => (true, Low),
|
||||||
|
|
||||||
// Form
|
// Form
|
||||||
"change" | "input" | "invalid" | "reset" | "submit" => (true, Medium),
|
"change" | "input" | "invalid" | "reset" | "submit" => (true, Medium),
|
||||||
|
|||||||
@@ -399,6 +399,9 @@ pub trait GlobalAttributes {
|
|||||||
/// Specify the font weight of the text.
|
/// Specify the font weight of the text.
|
||||||
font_weight: "font-weight",
|
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.
|
/// Specify the height of an element.
|
||||||
height: "height",
|
height: "height",
|
||||||
|
|
||||||
@@ -531,6 +534,9 @@ pub trait GlobalAttributes {
|
|||||||
/// Specify the location of the right edge of the positioned element.
|
/// Specify the location of the right edge of the positioned element.
|
||||||
right: "right",
|
right: "right",
|
||||||
|
|
||||||
|
/// Specifies the gap between the rows in a multi_column element.
|
||||||
|
row_gap: "row-gap",
|
||||||
|
|
||||||
/// Specifies the length of the tab character.
|
/// Specifies the length of the tab character.
|
||||||
tab_size: "tab-size",
|
tab_size: "tab-size",
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# JS Interpreter
|
||||||
|
|
||||||
|
After diffing old and new trees, the Dioxus VirtualDom produces patches that are used to modify the existing Dom. We can send these patches anywhere - including targets without WASM support.
|
||||||
|
|
||||||
|
In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets.
|
||||||
|
|
||||||
|
|
||||||
|
For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks.
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,680 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
let root = window.document.getElementById("main");
|
||||||
|
if (root != null) {
|
||||||
|
window.interpreter = new Interpreter(root);
|
||||||
|
window.rpc.call("initialize");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
interpreter: Interpreter;
|
||||||
|
rpc: { call: (method: string, args?: any) => void };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Interpreter {
|
||||||
|
root: Element;
|
||||||
|
stack: Element[];
|
||||||
|
listeners: { [key: string]: number };
|
||||||
|
handlers: { [key: string]: (evt: Event) => void };
|
||||||
|
lastNodeWasText: boolean;
|
||||||
|
nodes: Element[];
|
||||||
|
|
||||||
|
|
||||||
|
constructor(root: Element) {
|
||||||
|
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: number) {
|
||||||
|
const node = this.nodes[root];
|
||||||
|
this.stack.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendChildren(many: number) {
|
||||||
|
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: number, m: number) {
|
||||||
|
let root = this.nodes[root_id] as Element;
|
||||||
|
let els = this.stack.splice(this.stack.length - m);
|
||||||
|
|
||||||
|
root.replaceWith(...els);
|
||||||
|
}
|
||||||
|
|
||||||
|
InsertAfter(root: number, n: number) {
|
||||||
|
let old = this.nodes[root] as Element;
|
||||||
|
let new_nodes = this.stack.splice(this.stack.length - n);
|
||||||
|
old.after(...new_nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
InsertBefore(root: number, n: number) {
|
||||||
|
let old = this.nodes[root] as Element;
|
||||||
|
let new_nodes = this.stack.splice(this.stack.length - n);
|
||||||
|
old.before(...new_nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove(root: number) {
|
||||||
|
let node = this.nodes[root] as Element;
|
||||||
|
if (node !== undefined) {
|
||||||
|
node.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateTextNode(text: string, root: number) {
|
||||||
|
// todo: make it so the types are okay
|
||||||
|
const node = document.createTextNode(text) as any as Element;
|
||||||
|
this.nodes[root] = node;
|
||||||
|
this.stack.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateElement(tag: string, root: number) {
|
||||||
|
const el = document.createElement(tag);
|
||||||
|
// el.setAttribute("data-dioxus-id", `${root}`);
|
||||||
|
|
||||||
|
this.nodes[root] = el;
|
||||||
|
this.stack.push(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateElementNs(tag: string, root: number, ns: string) {
|
||||||
|
let el = document.createElementNS(ns, tag);
|
||||||
|
this.stack.push(el);
|
||||||
|
this.nodes[root] = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatePlaceholder(root: number) {
|
||||||
|
let el = document.createElement("pre");
|
||||||
|
el.hidden = true;
|
||||||
|
this.stack.push(el);
|
||||||
|
this.nodes[root] = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
NewEventListener(event_name: string, root: number, handler: (evt: Event) => void) {
|
||||||
|
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: number, event_name: string) {
|
||||||
|
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: number, text: string) {
|
||||||
|
this.nodes[root].textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetAttribute(root: number, field: string, value: string, ns: string | undefined) {
|
||||||
|
const name = field;
|
||||||
|
const node = this.nodes[root];
|
||||||
|
|
||||||
|
if (ns == "style") {
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
(node as HTMLElement).style[name] = value;
|
||||||
|
|
||||||
|
} else if (ns != null || ns != undefined) {
|
||||||
|
node.setAttributeNS(ns, name, value);
|
||||||
|
} else {
|
||||||
|
switch (name) {
|
||||||
|
case "value":
|
||||||
|
if (value != (node as HTMLInputElement).value) {
|
||||||
|
(node as HTMLInputElement).value = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "checked":
|
||||||
|
(node as HTMLInputElement).checked = value === "true";
|
||||||
|
break;
|
||||||
|
case "selected":
|
||||||
|
(node as HTMLOptionElement).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: number, name: string) {
|
||||||
|
|
||||||
|
const node = this.nodes[root];
|
||||||
|
node.removeAttribute(name);
|
||||||
|
|
||||||
|
if (name === "value") {
|
||||||
|
(node as HTMLInputElement).value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "checked") {
|
||||||
|
(node as HTMLInputElement).checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "selected") {
|
||||||
|
(node as HTMLOptionElement).selected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEdits(edits: DomEdit[]) {
|
||||||
|
this.stack.push(this.root);
|
||||||
|
|
||||||
|
for (let edit of edits) {
|
||||||
|
this.handleEdit(edit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEdit(edit: DomEdit) {
|
||||||
|
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: Event) => {
|
||||||
|
let target = event.target as Element | null;
|
||||||
|
|
||||||
|
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 as NewEventListener).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: Event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case "copy":
|
||||||
|
case "cut":
|
||||||
|
case "past": {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "compositionend":
|
||||||
|
case "compositionstart":
|
||||||
|
case "compositionupdate": {
|
||||||
|
let { data } = (event as CompositionEvent);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "keydown":
|
||||||
|
case "keypress":
|
||||||
|
case "keyup": {
|
||||||
|
let {
|
||||||
|
charCode,
|
||||||
|
key,
|
||||||
|
altKey,
|
||||||
|
ctrlKey,
|
||||||
|
metaKey,
|
||||||
|
keyCode,
|
||||||
|
shiftKey,
|
||||||
|
location,
|
||||||
|
repeat,
|
||||||
|
which,
|
||||||
|
} = (event as KeyboardEvent);
|
||||||
|
|
||||||
|
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 as HTMLInputElement;
|
||||||
|
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 as HTMLFormElement;
|
||||||
|
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 as MouseEvent;
|
||||||
|
|
||||||
|
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 as PointerEvent;
|
||||||
|
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 as TouchEvent;
|
||||||
|
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 as WheelEvent;
|
||||||
|
return {
|
||||||
|
delta_x: deltaX,
|
||||||
|
delta_y: deltaY,
|
||||||
|
delta_z: deltaZ,
|
||||||
|
delta_mode: deltaMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "animationstart":
|
||||||
|
case "animationend":
|
||||||
|
case "animationiteration": {
|
||||||
|
const {
|
||||||
|
animationName,
|
||||||
|
elapsedTime,
|
||||||
|
pseudoElement,
|
||||||
|
} = event as AnimationEvent;
|
||||||
|
return {
|
||||||
|
animation_name: animationName,
|
||||||
|
elapsed_time: elapsedTime,
|
||||||
|
pseudo_element: pseudoElement,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "transitionend": {
|
||||||
|
const {
|
||||||
|
propertyName,
|
||||||
|
elapsedTime,
|
||||||
|
pseudoElement,
|
||||||
|
} = event as TransitionEvent;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type PushRoot = { type: "PushRoot", root: number };
|
||||||
|
type AppendChildren = { type: "AppendChildren", many: number };
|
||||||
|
type ReplaceWith = { type: "ReplaceWith", root: number, m: number };
|
||||||
|
type InsertAfter = { type: "InsertAfter", root: number, n: number };
|
||||||
|
type InsertBefore = { type: "InsertBefore", root: number, n: number };
|
||||||
|
type Remove = { type: "Remove", root: number };
|
||||||
|
type CreateTextNode = { type: "CreateTextNode", text: string, root: number };
|
||||||
|
type CreateElement = { type: "CreateElement", tag: string, root: number };
|
||||||
|
type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string };
|
||||||
|
type CreatePlaceholder = { type: "CreatePlaceholder", root: number };
|
||||||
|
type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number };
|
||||||
|
type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number };
|
||||||
|
type SetText = { type: "SetText", root: number, text: string };
|
||||||
|
type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined };
|
||||||
|
type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string };
|
||||||
|
|
||||||
|
|
||||||
|
type DomEdit =
|
||||||
|
PushRoot |
|
||||||
|
AppendChildren |
|
||||||
|
ReplaceWith |
|
||||||
|
InsertAfter |
|
||||||
|
InsertBefore |
|
||||||
|
Remove |
|
||||||
|
CreateTextNode |
|
||||||
|
CreateElement |
|
||||||
|
CreateElementNs |
|
||||||
|
CreatePlaceholder |
|
||||||
|
NewEventListener |
|
||||||
|
RemoveEventListener |
|
||||||
|
SetText |
|
||||||
|
SetAttribute |
|
||||||
|
RemoveAttribute;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ES2015",
|
||||||
|
"lib": [
|
||||||
|
"es2015",
|
||||||
|
"es5",
|
||||||
|
"es6",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"strict": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ DioxusRouter adds React-Router style routing to your Dioxus apps. Works in brows
|
|||||||
```rust
|
```rust
|
||||||
fn app() {
|
fn app() {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
Routes {
|
Router {
|
||||||
Route { to: "/", Component {} },
|
Route { to: "/", Component {} },
|
||||||
Route { to: "/blog", Blog {} },
|
Route { to: "/blog", Blog {} },
|
||||||
Route { to: "/blog/:id", BlogPost {} },
|
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`.
|
Then, in your route, you can choose to parse the Route any way you want through `use_route`.
|
||||||
```rust
|
```rust
|
||||||
let id: usize = use_route(&cx).path("id")?;
|
let id: usize = use_route(&cx).segment("id")?;
|
||||||
|
|
||||||
let state: CustomState = use_route(&cx).parse()?;
|
let state: CustomState = use_route(&cx).parse()?;
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub struct LinkProps<'a> {
|
|||||||
|
|
||||||
/// The url that gets pushed to the history stack
|
/// 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
|
/// ```rust, ignore
|
||||||
///
|
///
|
||||||
@@ -22,15 +22,18 @@ pub struct LinkProps<'a> {
|
|||||||
/// Link { to: Route::Home, href: Route::as_url }
|
/// Link { to: Route::Home, href: Route::as_url }
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
#[props(default, setter(strip_option))]
|
#[props(default, strip_option)]
|
||||||
href: Option<&'a str>,
|
href: Option<&'a str>,
|
||||||
|
|
||||||
#[props(default, setter(strip_option))]
|
#[props(default, strip_option)]
|
||||||
class: Option<&'a str>,
|
class: Option<&'a str>,
|
||||||
|
|
||||||
#[props(default, setter(strip_option))]
|
#[props(default, strip_option)]
|
||||||
id: Option<&'a str>,
|
id: Option<&'a str>,
|
||||||
|
|
||||||
|
#[props(default, strip_option)]
|
||||||
|
title: Option<&'a str>,
|
||||||
|
|
||||||
children: Element<'a>,
|
children: Element<'a>,
|
||||||
|
|
||||||
#[props(default)]
|
#[props(default)]
|
||||||
@@ -38,17 +41,25 @@ pub struct LinkProps<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||||
let service = cx.consume_context::<RouterService>()?;
|
log::debug!("render Link to {}", cx.props.to);
|
||||||
cx.render(rsx! {
|
if let Some(service) = cx.consume_context::<RouterService>() {
|
||||||
a {
|
return cx.render(rsx! {
|
||||||
href: "{cx.props.to}",
|
a {
|
||||||
class: format_args!("{}", cx.props.class.unwrap_or("")),
|
href: "{cx.props.to}",
|
||||||
id: format_args!("{}", cx.props.id.unwrap_or("")),
|
class: format_args!("{}", cx.props.class.unwrap_or("")),
|
||||||
|
id: format_args!("{}", cx.props.id.unwrap_or("")),
|
||||||
|
title: format_args!("{}", cx.props.title.unwrap_or("")),
|
||||||
|
|
||||||
prevent_default: "onclick",
|
prevent_default: "onclick",
|
||||||
onclick: move |_| service.push_route(cx.props.to.clone()),
|
onclick: move |_| service.push_route(cx.props.to),
|
||||||
|
|
||||||
&cx.props.children
|
&cx.props.children
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
log::warn!(
|
||||||
|
"Attempted to create a Link to {} outside of a Router context",
|
||||||
|
cx.props.to,
|
||||||
|
);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
@@ -27,9 +27,10 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
|
|||||||
cx.use_hook(|_| {
|
cx.use_hook(|_| {
|
||||||
// create a bigger, better, longer route if one above us exists
|
// create a bigger, better, longer route if one above us exists
|
||||||
let total_route = match cx.consume_context::<RouteContext>() {
|
let total_route = match cx.consume_context::<RouteContext>() {
|
||||||
Some(ctx) => format!("{}", ctx.total_route.clone()),
|
Some(ctx) => ctx.total_route.to_string(),
|
||||||
None => format!("{}", cx.props.to.clone()),
|
None => cx.props.to.to_string(),
|
||||||
};
|
};
|
||||||
|
log::trace!("total route for {} is {}", cx.props.to, total_route);
|
||||||
|
|
||||||
// provide our route context
|
// provide our route context
|
||||||
let route_context = cx.provide_context(RouteContext {
|
let route_context = cx.provide_context(RouteContext {
|
||||||
|
|||||||
@@ -11,17 +11,26 @@ use crate::RouterService;
|
|||||||
pub struct RouterProps<'a> {
|
pub struct RouterProps<'a> {
|
||||||
children: Element<'a>,
|
children: Element<'a>,
|
||||||
|
|
||||||
#[props(default, setter(strip_option))]
|
#[props(default)]
|
||||||
onchange: Option<&'a Fn(&'a str)>,
|
onchange: EventHandler<'a, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
|
pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
|
||||||
cx.use_hook(|_| {
|
let svc = cx.use_hook(|_| {
|
||||||
let update = cx.schedule_update_any();
|
let update = cx.schedule_update_any();
|
||||||
cx.provide_context(RouterService::new(update, cx.scope_id()))
|
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!(
|
cx.render(rsx!(
|
||||||
div { &cx.props.children }
|
div { &cx.props.children }
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,30 +1,83 @@
|
|||||||
use dioxus_core::ScopeState;
|
use dioxus_core::ScopeState;
|
||||||
|
use gloo::history::{HistoryResult, Location};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::{rc::Rc, str::FromStr};
|
||||||
|
|
||||||
pub struct UseRoute<'a> {
|
use crate::RouterService;
|
||||||
cur_route: String,
|
|
||||||
cx: &'a ScopeState,
|
/// This struct provides is a wrapper around the internal router
|
||||||
|
/// implementation, with methods for getting information about the current
|
||||||
|
/// route.
|
||||||
|
pub struct UseRoute {
|
||||||
|
router: Rc<RouterService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> UseRoute<'a> {
|
impl UseRoute {
|
||||||
/// Parse the query part of the URL
|
/// This method simply calls the [`Location::query`] method.
|
||||||
pub fn param<T>(&self, param: &str) -> Option<&T> {
|
pub fn query<T>(&self) -> HistoryResult<T>
|
||||||
todo!()
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
self.current_location().query::<T>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nth_segment(&self, n: usize) -> Option<&str> {
|
/// Returns the nth segment in the path. Paths that end with a slash have
|
||||||
todo!()
|
/// the slash removed before determining the segments. If the path has
|
||||||
|
/// fewer segments than `n` then this method returns `None`.
|
||||||
|
pub fn nth_segment(&self, n: usize) -> Option<String> {
|
||||||
|
let mut segments = self.path_segments();
|
||||||
|
let len = segments.len();
|
||||||
|
if len - 1 < n {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(segments.remove(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn last_segment(&self) -> Option<&'a str> {
|
/// Returns the last segment in the path. Paths that end with a slash have
|
||||||
todo!()
|
/// the slash removed before determining the segments. The root path, `/`,
|
||||||
|
/// will return an empty string.
|
||||||
|
pub fn last_segment(&self) -> String {
|
||||||
|
let mut segments = self.path_segments();
|
||||||
|
segments.remove(segments.len() - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the segments of the URL, using named parameters (defined in your router)
|
/// Get the named parameter from the path, as defined in your router. The
|
||||||
pub fn segment<T>(&self, name: &str) -> Option<&T> {
|
/// value will be parsed into the type specified by `T` by calling
|
||||||
todo!()
|
/// `value.parse::<T>()`. This method returns `None` if the named
|
||||||
|
/// parameter does not exist in the current path.
|
||||||
|
pub fn segment<T>(&self, name: &str) -> Option<Result<T, T::Err>>
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
{
|
||||||
|
self.router
|
||||||
|
.current_path_params()
|
||||||
|
.get(name)
|
||||||
|
.and_then(|v| Some(v.parse::<T>()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [Location] for the current route.
|
||||||
|
pub fn current_location(&self) -> Location {
|
||||||
|
self.router.current_location()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_segments(&self) -> Vec<String> {
|
||||||
|
let location = self.router.current_location();
|
||||||
|
let path = location.path();
|
||||||
|
if path == "/" {
|
||||||
|
return vec![String::new()];
|
||||||
|
}
|
||||||
|
let stripped = &location.path()[1..];
|
||||||
|
stripped.split('/').map(str::to_string).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_route<'a>(cx: &'a ScopeState) -> UseRoute<'a> {
|
/// This hook provides access to information about the current location in the
|
||||||
todo!()
|
/// 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::<RouterService>()
|
||||||
|
.expect("Cannot call use_route outside the scope of a Router component")
|
||||||
|
.clone();
|
||||||
|
UseRoute { router }
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![allow(warnings)]
|
||||||
//! Dioxus-Router
|
//! Dioxus-Router
|
||||||
//!
|
//!
|
||||||
//! A simple match-based router and router service for most routing needs.
|
//! A simple match-based router and router service for most routing needs.
|
||||||
@@ -7,22 +8,9 @@
|
|||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! fn app(cx: Scope) -> Element {
|
//! fn app(cx: Scope) -> Element {
|
||||||
|
//!
|
||||||
//! }
|
//! }
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
|
|
||||||
mod hooks {
|
mod hooks {
|
||||||
mod use_route;
|
mod use_route;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use url::Url;
|
|
||||||
|
|
||||||
pub trait RouterProvider {
|
pub trait RouterProvider {
|
||||||
fn get_current_route(&self) -> String;
|
fn get_current_route(&self) -> String;
|
||||||
fn subscribe_to_route_changes(&self, callback: Box<dyn Fn(String)>);
|
fn subscribe_to_route_changes(&self, callback: Box<dyn Fn(String)>);
|
||||||
|
|||||||
+122
-28
@@ -1,6 +1,6 @@
|
|||||||
use gloo::history::{BrowserHistory, History, HistoryListener};
|
use gloo::history::{BrowserHistory, History, HistoryListener, Location};
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, Ref, RefCell},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
@@ -9,14 +9,20 @@ use dioxus_core::ScopeId;
|
|||||||
|
|
||||||
pub struct RouterService {
|
pub struct RouterService {
|
||||||
pub(crate) regen_route: Rc<dyn Fn(ScopeId)>,
|
pub(crate) regen_route: Rc<dyn Fn(ScopeId)>,
|
||||||
|
pub(crate) pending_events: Rc<RefCell<Vec<RouteEvent>>>,
|
||||||
history: Rc<RefCell<BrowserHistory>>,
|
history: Rc<RefCell<BrowserHistory>>,
|
||||||
registerd_routes: RefCell<RouteSlot>,
|
|
||||||
slots: Rc<RefCell<Vec<(ScopeId, String)>>>,
|
slots: Rc<RefCell<Vec<(ScopeId, String)>>>,
|
||||||
root_found: Rc<Cell<bool>>,
|
root_found: Rc<Cell<Option<ScopeId>>>,
|
||||||
cur_root: RefCell<String>,
|
cur_path_params: Rc<RefCell<HashMap<String, String>>>,
|
||||||
listener: HistoryListener,
|
listener: HistoryListener,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum RouteEvent {
|
||||||
|
Change,
|
||||||
|
Pop,
|
||||||
|
Push,
|
||||||
|
}
|
||||||
|
|
||||||
enum RouteSlot {
|
enum RouteSlot {
|
||||||
Routes {
|
Routes {
|
||||||
// the partial route
|
// the partial route
|
||||||
@@ -37,51 +43,66 @@ impl RouterService {
|
|||||||
let path = location.path();
|
let path = location.path();
|
||||||
|
|
||||||
let slots: Rc<RefCell<Vec<(ScopeId, String)>>> = Default::default();
|
let slots: Rc<RefCell<Vec<(ScopeId, String)>>> = Default::default();
|
||||||
|
let pending_events: Rc<RefCell<Vec<RouteEvent>>> = Default::default();
|
||||||
|
let root_found = Rc::new(Cell::new(None));
|
||||||
|
|
||||||
let _slots = slots.clone();
|
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();
|
||||||
|
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_route(*slot);
|
||||||
|
}
|
||||||
|
|
||||||
let root_found = Rc::new(Cell::new(false));
|
// also regenerate the root
|
||||||
let regen = regen_route.clone();
|
regen_route(root_scope);
|
||||||
let _root_found = root_found.clone();
|
|
||||||
let listener = history.listen(move || {
|
pending_events.borrow_mut().push(RouteEvent::Change)
|
||||||
_root_found.set(false);
|
|
||||||
// checking if the route is valid is cheap, so we do it
|
|
||||||
for (slot, _) in _slots.borrow_mut().iter().rev() {
|
|
||||||
log::trace!("regenerating slot {:?}", slot);
|
|
||||||
regen(*slot);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
registerd_routes: RefCell::new(RouteSlot::Routes {
|
listener,
|
||||||
partial: String::from("/"),
|
|
||||||
total: String::from("/"),
|
|
||||||
rest: Vec::new(),
|
|
||||||
}),
|
|
||||||
root_found,
|
root_found,
|
||||||
history: Rc::new(RefCell::new(history)),
|
history: Rc::new(RefCell::new(history)),
|
||||||
regen_route,
|
regen_route,
|
||||||
slots,
|
slots,
|
||||||
cur_root: RefCell::new(path.to_string()),
|
pending_events,
|
||||||
listener,
|
cur_path_params: Rc::new(RefCell::new(HashMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_route(&self, route: &str) {
|
pub fn push_route(&self, route: &str) {
|
||||||
|
log::trace!("Pushing route: {}", route);
|
||||||
self.history.borrow_mut().push(route);
|
self.history.borrow_mut().push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) {
|
pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) {
|
||||||
self.slots.borrow_mut().push((scope, route));
|
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 {
|
pub fn should_render(&self, scope: ScopeId) -> bool {
|
||||||
if self.root_found.get() {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = self.history.borrow().location();
|
let location = self.history.borrow().location();
|
||||||
let path = location.path();
|
let path = location.path();
|
||||||
|
log::trace!(" current path is '{}'", path);
|
||||||
|
|
||||||
let roots = self.slots.borrow();
|
let roots = self.slots.borrow();
|
||||||
|
|
||||||
@@ -89,15 +110,24 @@ impl RouterService {
|
|||||||
|
|
||||||
// fallback logic
|
// fallback logic
|
||||||
match root {
|
match root {
|
||||||
Some((_id, route)) => {
|
Some((id, route)) => {
|
||||||
if route == path {
|
log::trace!(
|
||||||
self.root_found.set(true);
|
" 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
|
true
|
||||||
} else {
|
} else {
|
||||||
if route == "" {
|
if route == "" {
|
||||||
self.root_found.set(true);
|
log::trace!(" and the route is the root, so we will use that without a better match");
|
||||||
|
self.root_found.set(Some(*id));
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
log::trace!(" and the route '{}' is not the root nor does it match the current path", route);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,6 +135,70 @@ impl RouterService {
|
|||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_location(&self) -> Location {
|
||||||
|
self.history.borrow().location().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_path_params(&self) -> Ref<HashMap<String, String>> {
|
||||||
|
self.cur_path_params.borrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_route(route: String) -> String {
|
||||||
|
if route.as_str() == "/" {
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
route.trim_end_matches('/').to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_path(path: &str) -> &str {
|
||||||
|
if path == "/" {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
path.trim_end_matches('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn route_matches_path(route: &str, path: &str) -> Option<HashMap<String, String>> {
|
||||||
|
let route_pieces = route.split('/').collect::<Vec<_>>();
|
||||||
|
let path_pieces = clean_path(path).split('/').collect::<Vec<_>>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(matches)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RouterCfg {
|
pub struct RouterCfg {
|
||||||
|
|||||||
+35
-4
@@ -24,7 +24,7 @@ impl SsrRenderer {
|
|||||||
|
|
||||||
pub fn render_lazy<'a>(&'a mut self, f: LazyNodes<'a, '_>) -> String {
|
pub fn render_lazy<'a>(&'a mut self, f: LazyNodes<'a, '_>) -> String {
|
||||||
let scope = self.vdom.base_scope();
|
let scope = self.vdom.base_scope();
|
||||||
let factory = NodeFactory::new(&scope);
|
let factory = NodeFactory::new(scope);
|
||||||
|
|
||||||
let root = f.into_vnode(factory);
|
let root = f.into_vnode(factory);
|
||||||
format!(
|
format!(
|
||||||
@@ -38,6 +38,7 @@ impl SsrRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_lifetimes)]
|
||||||
pub fn render_lazy<'a>(f: LazyNodes<'a, '_>) -> String {
|
pub fn render_lazy<'a>(f: LazyNodes<'a, '_>) -> String {
|
||||||
let vdom = VirtualDom::new(app);
|
let vdom = VirtualDom::new(app);
|
||||||
let scope: *const ScopeState = vdom.base_scope();
|
let scope: *const ScopeState = vdom.base_scope();
|
||||||
@@ -50,11 +51,11 @@ pub fn render_lazy<'a>(f: LazyNodes<'a, '_>) -> String {
|
|||||||
// When LazyNodes are provided, they are FnOnce, but do not come with a allocator selected to borrow from. The <'a>
|
// When LazyNodes are provided, they are FnOnce, but do not come with a allocator selected to borrow from. The <'a>
|
||||||
// lifetime is therefore longer than the lifetime of the allocator which doesn't exist... yet.
|
// lifetime is therefore longer than the lifetime of the allocator which doesn't exist... yet.
|
||||||
//
|
//
|
||||||
// Therefore, we cast our local bump alloactor into right lifetime. This is okay because our usage of the bump arena
|
// Therefore, we cast our local bump allocator to the right lifetime. This is okay because our usage of the bump
|
||||||
// is *definitely* shorter than the <'a> lifetime, and we return *owned* data - not borrowed data.
|
// arena is *definitely* shorter than the <'a> lifetime, and we return *owned* data - not borrowed data.
|
||||||
let scope = unsafe { &*scope };
|
let scope = unsafe { &*scope };
|
||||||
|
|
||||||
let root = f.into_vnode(NodeFactory::new(&scope));
|
let root = f.into_vnode(NodeFactory::new(scope));
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"{:}",
|
"{:}",
|
||||||
@@ -186,6 +187,36 @@ impl<'a> TextRenderer<'a, '_> {
|
|||||||
match attr.namespace {
|
match attr.namespace {
|
||||||
None => match attr.name {
|
None => match attr.name {
|
||||||
"dangerous_inner_html" => inner_html = Some(attr.value),
|
"dangerous_inner_html" => inner_html = Some(attr.value),
|
||||||
|
"allowfullscreen"
|
||||||
|
| "allowpaymentrequest"
|
||||||
|
| "async"
|
||||||
|
| "autofocus"
|
||||||
|
| "autoplay"
|
||||||
|
| "checked"
|
||||||
|
| "controls"
|
||||||
|
| "default"
|
||||||
|
| "defer"
|
||||||
|
| "disabled"
|
||||||
|
| "formnovalidate"
|
||||||
|
| "hidden"
|
||||||
|
| "ismap"
|
||||||
|
| "itemscope"
|
||||||
|
| "loop"
|
||||||
|
| "multiple"
|
||||||
|
| "muted"
|
||||||
|
| "nomodule"
|
||||||
|
| "novalidate"
|
||||||
|
| "open"
|
||||||
|
| "playsinline"
|
||||||
|
| "readonly"
|
||||||
|
| "required"
|
||||||
|
| "reversed"
|
||||||
|
| "selected"
|
||||||
|
| "truespeed" => {
|
||||||
|
if attr.value != "false" {
|
||||||
|
write!(f, " {}=\"{}\"", attr.name, attr.value)?
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => write!(f, " {}=\"{}\"", attr.name, attr.value)?,
|
_ => write!(f, " {}=\"{}\"", attr.name, attr.value)?,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+6
-17
@@ -11,8 +11,8 @@ documentation = "https://dioxuslabs.com"
|
|||||||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { path = "../core", version ="^0.1.7"}
|
dioxus-core = { path = "../core", version = "^0.1.7" }
|
||||||
dioxus-html = { path = "../html", version ="^0.1.4"}
|
dioxus-html = { path = "../html", version = "^0.1.4" }
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] }
|
wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] }
|
||||||
lazy_static = "1.4.0"
|
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"] }
|
log = { version = "0.4.14", features = ["release_max_level_off"] }
|
||||||
fxhash = "0.2.1"
|
fxhash = "0.2.1"
|
||||||
wasm-logger = "0.2.0"
|
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"
|
wasm-bindgen-test = "0.3.21"
|
||||||
once_cell = "1.8"
|
once_cell = "1.8"
|
||||||
async-channel = "1.6.1"
|
async-channel = "1.6.1"
|
||||||
@@ -69,22 +69,11 @@ features = [
|
|||||||
"IdleDeadline",
|
"IdleDeadline",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[features]
|
||||||
# [lib]
|
default = ["panic_hook"]
|
||||||
# crate-type = ["cdylib", "rlib"]
|
panic_hook = ["console_error_panic_hook"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus-core-macro = { path = "../core-macro" }
|
dioxus-core-macro = { path = "../core-macro" }
|
||||||
wasm-bindgen-test = "0.3.28"
|
wasm-bindgen-test = "0.3.28"
|
||||||
dioxus-ssr = { path = "../ssr" }
|
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"]
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use js_sys::Function;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::{Element, Node};
|
||||||
|
|
||||||
|
#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")]
|
||||||
|
extern "C" {
|
||||||
|
pub type Interpreter;
|
||||||
|
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(arg: Element) -> Interpreter;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn set_node(this: &Interpreter, id: usize, node: Node);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn PushRoot(this: &Interpreter, root: u64);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn AppendChildren(this: &Interpreter, many: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn ReplaceWith(this: &Interpreter, root: u64, m: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn InsertAfter(this: &Interpreter, root: u64, n: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn InsertBefore(this: &Interpreter, root: u64, n: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn Remove(this: &Interpreter, root: u64);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn CreateElement(this: &Interpreter, tag: &str, root: u64);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn CreateElementNs(this: &Interpreter, tag: &str, root: u64, ns: &str);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn CreatePlaceholder(this: &Interpreter, root: u64);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn SetText(this: &Interpreter, root: u64, text: &str);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[
|
pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[
|
||||||
// Important tags to dioxus
|
// Important tags to dioxus
|
||||||
"dioxus-id",
|
"dioxus-id",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
@@ -203,6 +203,8 @@ pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[
|
|||||||
"onended",
|
"onended",
|
||||||
"onerror",
|
"onerror",
|
||||||
"onfocus",
|
"onfocus",
|
||||||
|
"onfocusout",
|
||||||
|
"onfocusin",
|
||||||
"onhashchange",
|
"onhashchange",
|
||||||
"oninput",
|
"oninput",
|
||||||
"oninvalid",
|
"oninvalid",
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
/// This struct helps configure the specifics of hydration and render destination for WebSys.
|
/// This struct helps configure the specifics of hydration and render destination for WebSys.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```rust, ignore
|
/// ```rust, ignore
|
||||||
/// fn main() {
|
/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot"))
|
||||||
/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot"))
|
|
||||||
/// }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub struct WebConfig {
|
pub struct WebConfig {
|
||||||
pub(crate) hydrate: bool,
|
pub(crate) hydrate: bool,
|
||||||
@@ -31,7 +30,7 @@ impl WebConfig {
|
|||||||
/// work and suspended nodes.
|
/// work and suspended nodes.
|
||||||
///
|
///
|
||||||
/// Dioxus will load up all the elements with the `dio_el` data attribute into memory when the page is loaded.
|
/// 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.hydrate = f;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -39,7 +38,7 @@ impl WebConfig {
|
|||||||
/// Set the name of the element that Dioxus will use as the root.
|
/// Set the name of the element that Dioxus will use as the root.
|
||||||
///
|
///
|
||||||
/// This is akint to calling React.render() on the element with the specified name.
|
/// This is akint to calling React.render() on the element with the specified name.
|
||||||
pub fn rootname(mut self, name: impl Into<String>) -> Self {
|
pub fn rootname(&mut self, name: impl Into<String>) -> &mut Self {
|
||||||
self.rootname = name.into();
|
self.rootname = name.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -47,7 +46,7 @@ impl WebConfig {
|
|||||||
/// Set the name of the element that Dioxus will use as the root.
|
/// Set the name of the element that Dioxus will use as the root.
|
||||||
///
|
///
|
||||||
/// This is akint to calling React.render() on the element with the specified name.
|
/// This is akint to calling React.render() on the element with the specified name.
|
||||||
pub fn with_string_cache(mut self, cache: Vec<String>) -> Self {
|
pub fn with_string_cache(&mut self, cache: Vec<String>) -> &mut Self {
|
||||||
self.cached_strings = cache;
|
self.cached_strings = cache;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-464
@@ -7,56 +7,51 @@
|
|||||||
//! - tests to ensure dyn_into works for various event types.
|
//! - tests to ensure dyn_into works for various event types.
|
||||||
//! - Partial delegation?>
|
//! - Partial delegation?>
|
||||||
|
|
||||||
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent};
|
use crate::bindings::Interpreter;
|
||||||
use fxhash::FxHashMap;
|
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
|
||||||
use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc};
|
use js_sys::Function;
|
||||||
|
use std::{any::Any, rc::Rc, sync::Arc};
|
||||||
use wasm_bindgen::{closure::Closure, JsCast};
|
use wasm_bindgen::{closure::Closure, JsCast};
|
||||||
use web_sys::{
|
use web_sys::{Document, Element, Event, HtmlElement};
|
||||||
CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
|
|
||||||
HtmlOptionElement, HtmlTextAreaElement, Node,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{nodeslab::NodeSlab, WebConfig};
|
use crate::WebConfig;
|
||||||
|
|
||||||
pub struct WebsysDom {
|
pub struct WebsysDom {
|
||||||
stack: Stack,
|
pub interpreter: Interpreter,
|
||||||
|
|
||||||
/// A map from ElementID (index) to Node
|
|
||||||
pub(crate) nodes: NodeSlab,
|
|
||||||
|
|
||||||
document: Document,
|
|
||||||
|
|
||||||
pub(crate) root: Element,
|
pub(crate) root: Element,
|
||||||
|
|
||||||
sender_callback: Rc<dyn Fn(SchedulerMsg)>,
|
pub handler: Closure<dyn FnMut(&Event)>,
|
||||||
|
|
||||||
// map of listener types to number of those listeners
|
|
||||||
// This is roughly a delegater
|
|
||||||
// TODO: check how infero delegates its events - some are more performant
|
|
||||||
listeners: FxHashMap<&'static str, ListenerEntry>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListenerEntry = (usize, Closure<dyn FnMut(&Event)>);
|
|
||||||
|
|
||||||
impl WebsysDom {
|
impl WebsysDom {
|
||||||
pub fn new(cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
|
pub fn new(cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
|
||||||
let document = load_document();
|
// eventually, we just want to let the interpreter do all the work of decoding events into our event type
|
||||||
|
let callback: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
|
||||||
|
if let Ok(synthetic_event) = decode_trigger(event) {
|
||||||
|
// Try to prevent default if the attribute is set
|
||||||
|
if let Some(target) = event.target() {
|
||||||
|
if let Some(node) = target.dyn_ref::<HtmlElement>() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let nodes = NodeSlab::new(2000);
|
sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event))
|
||||||
let listeners = FxHashMap::default();
|
}
|
||||||
|
});
|
||||||
let mut stack = Stack::with_capacity(10);
|
|
||||||
|
|
||||||
let root = load_document().get_element_by_id(&cfg.rootname).unwrap();
|
let root = load_document().get_element_by_id(&cfg.rootname).unwrap();
|
||||||
let root_node = root.clone().dyn_into::<Node>().unwrap();
|
|
||||||
stack.push(root_node);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
stack,
|
interpreter: Interpreter::new(root.clone()),
|
||||||
nodes,
|
handler: Closure::wrap(callback),
|
||||||
listeners,
|
|
||||||
document,
|
|
||||||
sender_callback,
|
|
||||||
root,
|
root,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,438 +59,49 @@ impl WebsysDom {
|
|||||||
pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
|
pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
|
||||||
for edit in edits.drain(..) {
|
for edit in edits.drain(..) {
|
||||||
match edit {
|
match edit {
|
||||||
DomEdit::PushRoot { root } => self.push(root),
|
DomEdit::PushRoot { root } => self.interpreter.PushRoot(root),
|
||||||
DomEdit::AppendChildren { many } => self.append_children(many),
|
DomEdit::AppendChildren { many } => self.interpreter.AppendChildren(many),
|
||||||
DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
|
DomEdit::ReplaceWith { root, m } => self.interpreter.ReplaceWith(root, m),
|
||||||
DomEdit::Remove { root } => self.remove(root),
|
DomEdit::InsertAfter { root, n } => self.interpreter.InsertAfter(root, n),
|
||||||
DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id),
|
DomEdit::InsertBefore { root, n } => self.interpreter.InsertBefore(root, n),
|
||||||
DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id),
|
DomEdit::Remove { root } => self.interpreter.Remove(root),
|
||||||
DomEdit::CreateElementNs { tag, root: id, ns } => {
|
DomEdit::CreateTextNode { text, root } => {
|
||||||
self.create_element(tag, Some(ns), id)
|
self.interpreter.CreateTextNode(text, root)
|
||||||
}
|
}
|
||||||
DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id),
|
DomEdit::CreateElement { tag, root } => self.interpreter.CreateElement(tag, root),
|
||||||
|
DomEdit::CreateElementNs { tag, root, ns } => {
|
||||||
|
self.interpreter.CreateElementNs(tag, root, ns)
|
||||||
|
}
|
||||||
|
DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root),
|
||||||
DomEdit::NewEventListener {
|
DomEdit::NewEventListener {
|
||||||
event_name,
|
event_name, root, ..
|
||||||
scope,
|
} => {
|
||||||
root: mounted_node_id,
|
let handler: &Function = self.handler.as_ref().unchecked_ref();
|
||||||
} => self.new_event_listener(event_name, scope, mounted_node_id),
|
self.interpreter.NewEventListener(event_name, root, handler);
|
||||||
|
|
||||||
DomEdit::RemoveEventListener { event, root } => {
|
|
||||||
self.remove_event_listener(event, root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DomEdit::SetText { text, root } => self.set_text(text, root),
|
DomEdit::RemoveEventListener { root, event } => {
|
||||||
|
self.interpreter.RemoveEventListener(root, event)
|
||||||
|
}
|
||||||
|
DomEdit::SetText { root, text } => self.interpreter.SetText(root, text),
|
||||||
DomEdit::SetAttribute {
|
DomEdit::SetAttribute {
|
||||||
|
root,
|
||||||
field,
|
field,
|
||||||
value,
|
value,
|
||||||
ns,
|
ns,
|
||||||
root,
|
} => self.interpreter.SetAttribute(root, field, value, ns),
|
||||||
} => self.set_attribute(field, value, ns, root),
|
DomEdit::RemoveAttribute { root, name } => {
|
||||||
DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root),
|
self.interpreter.RemoveAttribute(root, name)
|
||||||
|
|
||||||
DomEdit::InsertAfter { n, root } => self.insert_after(n, root),
|
|
||||||
DomEdit::InsertBefore { n, root } => self.insert_before(n, root),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn push(&mut self, root: u64) {
|
|
||||||
let key = root as usize;
|
|
||||||
let domnode = &self.nodes[key];
|
|
||||||
|
|
||||||
let real_node: Node = match domnode {
|
|
||||||
Some(n) => n.clone(),
|
|
||||||
None => todo!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.stack.push(real_node);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_children(&mut self, many: u32) {
|
|
||||||
let root: Node = self
|
|
||||||
.stack
|
|
||||||
.list
|
|
||||||
.get(self.stack.list.len() - (1 + many as usize))
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
// We need to make sure to add comments between text nodes
|
|
||||||
// We ensure that the text siblings are patched by preventing the browser from merging
|
|
||||||
// neighboring text nodes. Originally inspired by some of React's work from 2016.
|
|
||||||
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
|
|
||||||
// -> https://github.com/facebook/react/pull/5753
|
|
||||||
/*
|
|
||||||
todo: we need to track this for replacing/insert after/etc
|
|
||||||
*/
|
|
||||||
let mut last_node_was_text = false;
|
|
||||||
|
|
||||||
for child in self
|
|
||||||
.stack
|
|
||||||
.list
|
|
||||||
.drain((self.stack.list.len() - many as usize)..)
|
|
||||||
{
|
|
||||||
if child.dyn_ref::<web_sys::Text>().is_some() {
|
|
||||||
if last_node_was_text {
|
|
||||||
let comment_node = self
|
|
||||||
.document
|
|
||||||
.create_comment("dioxus")
|
|
||||||
.dyn_into::<Node>()
|
|
||||||
.unwrap();
|
|
||||||
root.append_child(&comment_node).unwrap();
|
|
||||||
}
|
|
||||||
last_node_was_text = true;
|
|
||||||
} else {
|
|
||||||
last_node_was_text = false;
|
|
||||||
}
|
|
||||||
root.append_child(&child).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_with(&mut self, m: u32, root: u64) {
|
|
||||||
let old = self.nodes[root as usize].as_ref().unwrap();
|
|
||||||
|
|
||||||
let arr: js_sys::Array = self
|
|
||||||
.stack
|
|
||||||
.list
|
|
||||||
.drain((self.stack.list.len() - m as usize)..)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Some(el) = old.dyn_ref::<Element>() {
|
|
||||||
el.replace_with_with_node(&arr).unwrap();
|
|
||||||
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
|
|
||||||
el.replace_with_with_node(&arr).unwrap();
|
|
||||||
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
|
|
||||||
el.replace_with_with_node(&arr).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove(&mut self, root: u64) {
|
|
||||||
let node = self.nodes[root as usize].as_ref().unwrap();
|
|
||||||
if let Some(element) = node.dyn_ref::<Element>() {
|
|
||||||
element.remove();
|
|
||||||
} else {
|
|
||||||
if let Some(parent) = node.parent_node() {
|
|
||||||
parent.remove_child(&node).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_placeholder(&mut self, id: u64) {
|
|
||||||
self.create_element("pre", None, id);
|
|
||||||
self.set_attribute("hidden", "", None, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_text_node(&mut self, text: &str, id: u64) {
|
|
||||||
let textnode = self
|
|
||||||
.document
|
|
||||||
.create_text_node(text)
|
|
||||||
.dyn_into::<Node>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
self.stack.push(textnode.clone());
|
|
||||||
|
|
||||||
self.nodes[(id as usize)] = Some(textnode);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
|
|
||||||
let tag = wasm_bindgen::intern(tag);
|
|
||||||
|
|
||||||
let el = match ns {
|
|
||||||
Some(ns) => self
|
|
||||||
.document
|
|
||||||
.create_element_ns(Some(ns), tag)
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<Node>()
|
|
||||||
.unwrap(),
|
|
||||||
None => self
|
|
||||||
.document
|
|
||||||
.create_element(tag)
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<Node>()
|
|
||||||
.unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
use smallstr::SmallString;
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new();
|
|
||||||
write!(s, "{}", id).unwrap();
|
|
||||||
|
|
||||||
let el2 = el.dyn_ref::<Element>().unwrap();
|
|
||||||
el2.set_attribute("dioxus-id", s.as_str()).unwrap();
|
|
||||||
|
|
||||||
self.stack.push(el.clone());
|
|
||||||
self.nodes[(id as usize)] = Some(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) {
|
|
||||||
let event = wasm_bindgen::intern(event);
|
|
||||||
|
|
||||||
// attach the correct attributes to the element
|
|
||||||
// these will be used by accessing the event's target
|
|
||||||
// This ensures we only ever have one handler attached to the root, but decide
|
|
||||||
// dynamically when we want to call a listener.
|
|
||||||
|
|
||||||
let el = self.stack.top();
|
|
||||||
|
|
||||||
let el = el.dyn_ref::<Element>().unwrap();
|
|
||||||
|
|
||||||
el.set_attribute("dioxus-event", event).unwrap();
|
|
||||||
|
|
||||||
// Register the callback to decode
|
|
||||||
|
|
||||||
if let Some(entry) = self.listeners.get_mut(event) {
|
|
||||||
entry.0 += 1;
|
|
||||||
} else {
|
|
||||||
let trigger = self.sender_callback.clone();
|
|
||||||
|
|
||||||
let c: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
|
|
||||||
// "Result" cannot be received from JS
|
|
||||||
// Instead, we just build and immediately execute a closure that returns result
|
|
||||||
match decode_trigger(event) {
|
|
||||||
Ok(synthetic_event) => {
|
|
||||||
let target = event.target().unwrap();
|
|
||||||
if let Some(node) = target.dyn_ref::<HtmlElement>() {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trigger.as_ref()(SchedulerMsg::Event(synthetic_event))
|
|
||||||
}
|
|
||||||
Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let handler = Closure::wrap(c);
|
|
||||||
|
|
||||||
self.root
|
|
||||||
.add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Increment the listeners
|
|
||||||
self.listeners.insert(event.into(), (1, handler));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_event_listener(&mut self, _event: &str, _root: u64) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_text(&mut self, text: &str, root: u64) {
|
|
||||||
let el = self.nodes[root as usize].as_ref().unwrap();
|
|
||||||
el.set_text_content(Some(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) {
|
|
||||||
let node = self.nodes[root as usize].as_ref().unwrap();
|
|
||||||
if ns == Some("style") {
|
|
||||||
if let Some(el) = node.dyn_ref::<Element>() {
|
|
||||||
let el = el.dyn_ref::<HtmlElement>().unwrap();
|
|
||||||
let style_dc: CssStyleDeclaration = el.style();
|
|
||||||
style_dc.set_property(name, value).unwrap();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let fallback = || {
|
|
||||||
let el = node.dyn_ref::<Element>().unwrap();
|
|
||||||
el.set_attribute(name, value).unwrap()
|
|
||||||
};
|
|
||||||
match name {
|
|
||||||
"dangerous_inner_html" => {
|
|
||||||
if let Some(el) = node.dyn_ref::<Element>() {
|
|
||||||
el.set_inner_html(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"value" => {
|
|
||||||
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
|
|
||||||
/*
|
|
||||||
if the attribute being set is the same as the value of the input, then don't bother setting it.
|
|
||||||
This is used in controlled components to keep the cursor in the right spot.
|
|
||||||
|
|
||||||
this logic should be moved into the virtualdom since we have the notion of "volatile"
|
|
||||||
*/
|
|
||||||
if input.value() != value {
|
|
||||||
input.set_value(value);
|
|
||||||
}
|
|
||||||
} else if let Some(node) = node.dyn_ref::<HtmlTextAreaElement>() {
|
|
||||||
if name == "value" {
|
|
||||||
node.set_value(value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"checked" => {
|
|
||||||
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
|
|
||||||
match value {
|
|
||||||
"true" => input.set_checked(true),
|
|
||||||
"false" => input.set_checked(false),
|
|
||||||
_ => fallback(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"selected" => {
|
|
||||||
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
|
|
||||||
node.set_selected(true);
|
|
||||||
} else {
|
|
||||||
fallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
|
|
||||||
if value == "false" {
|
|
||||||
if let Some(el) = node.dyn_ref::<Element>() {
|
|
||||||
match name {
|
|
||||||
"allowfullscreen"
|
|
||||||
| "allowpaymentrequest"
|
|
||||||
| "async"
|
|
||||||
| "autofocus"
|
|
||||||
| "autoplay"
|
|
||||||
| "checked"
|
|
||||||
| "controls"
|
|
||||||
| "default"
|
|
||||||
| "defer"
|
|
||||||
| "disabled"
|
|
||||||
| "formnovalidate"
|
|
||||||
| "hidden"
|
|
||||||
| "ismap"
|
|
||||||
| "itemscope"
|
|
||||||
| "loop"
|
|
||||||
| "multiple"
|
|
||||||
| "muted"
|
|
||||||
| "nomodule"
|
|
||||||
| "novalidate"
|
|
||||||
| "open"
|
|
||||||
| "playsinline"
|
|
||||||
| "readonly"
|
|
||||||
| "required"
|
|
||||||
| "reversed"
|
|
||||||
| "selected"
|
|
||||||
| "truespeed" => {
|
|
||||||
let _ = el.remove_attribute(name);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let _ = el.set_attribute(name, value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fallback();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_attribute(&mut self, name: &str, root: u64) {
|
|
||||||
let node = self.nodes[root as usize].as_ref().unwrap();
|
|
||||||
if let Some(node) = node.dyn_ref::<web_sys::Element>() {
|
|
||||||
node.remove_attribute(name).unwrap();
|
|
||||||
}
|
|
||||||
if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
|
|
||||||
// Some attributes are "volatile" and don't work through `removeAttribute`.
|
|
||||||
if name == "value" {
|
|
||||||
node.set_value("");
|
|
||||||
}
|
|
||||||
if name == "checked" {
|
|
||||||
node.set_checked(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
|
|
||||||
if name == "selected" {
|
|
||||||
node.set_selected(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_after(&mut self, n: u32, root: u64) {
|
|
||||||
let old = self.nodes[root as usize].as_ref().unwrap();
|
|
||||||
|
|
||||||
let arr: js_sys::Array = self
|
|
||||||
.stack
|
|
||||||
.list
|
|
||||||
.drain((self.stack.list.len() - n as usize)..)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Some(el) = old.dyn_ref::<Element>() {
|
|
||||||
el.after_with_node(&arr).unwrap();
|
|
||||||
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
|
|
||||||
el.after_with_node(&arr).unwrap();
|
|
||||||
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
|
|
||||||
el.after_with_node(&arr).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_before(&mut self, n: u32, root: u64) {
|
|
||||||
let anchor = self.nodes[root as usize].as_ref().unwrap();
|
|
||||||
|
|
||||||
if n == 1 {
|
|
||||||
let before = self.stack.pop();
|
|
||||||
|
|
||||||
anchor
|
|
||||||
.parent_node()
|
|
||||||
.unwrap()
|
|
||||||
.insert_before(&before, Some(&anchor))
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
let arr: js_sys::Array = self
|
|
||||||
.stack
|
|
||||||
.list
|
|
||||||
.drain((self.stack.list.len() - n as usize)..)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Some(el) = anchor.dyn_ref::<Element>() {
|
|
||||||
el.before_with_node(&arr).unwrap();
|
|
||||||
} else if let Some(el) = anchor.dyn_ref::<web_sys::CharacterData>() {
|
|
||||||
el.before_with_node(&arr).unwrap();
|
|
||||||
} else if let Some(el) = anchor.dyn_ref::<web_sys::DocumentType>() {
|
|
||||||
el.before_with_node(&arr).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct Stack {
|
|
||||||
list: Vec<Node>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Stack {
|
|
||||||
#[inline]
|
|
||||||
fn with_capacity(cap: usize) -> Self {
|
|
||||||
Stack {
|
|
||||||
list: Vec::with_capacity(cap),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn push(&mut self, node: Node) {
|
|
||||||
self.list.push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn pop(&mut self) -> Node {
|
|
||||||
self.list.pop().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn top(&self) -> &Node {
|
|
||||||
match self.list.last() {
|
|
||||||
Some(a) => a,
|
|
||||||
None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DioxusWebsysEvent(web_sys::Event);
|
pub struct DioxusWebsysEvent(web_sys::Event);
|
||||||
|
|
||||||
// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread
|
// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread
|
||||||
|
#[allow(clippy::non_send_fields_in_send_ty)]
|
||||||
unsafe impl Send for DioxusWebsysEvent {}
|
unsafe impl Send for DioxusWebsysEvent {}
|
||||||
unsafe impl Sync for DioxusWebsysEvent {}
|
unsafe impl Sync for DioxusWebsysEvent {}
|
||||||
|
|
||||||
@@ -632,6 +238,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send
|
|||||||
shift_key: evt.shift_key(),
|
shift_key: evt.shift_key(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
"scroll" => Arc::new(()),
|
"scroll" => Arc::new(()),
|
||||||
"wheel" => {
|
"wheel" => {
|
||||||
let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap();
|
let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap();
|
||||||
@@ -670,9 +277,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send
|
|||||||
/// This function decodes a websys event and produces an EventTrigger
|
/// This function decodes a websys event and produces an EventTrigger
|
||||||
/// With the websys implementation, we attach a unique key to the nodes
|
/// With the websys implementation, we attach a unique key to the nodes
|
||||||
fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
|
fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
|
||||||
use anyhow::Context;
|
let mut target = event
|
||||||
|
|
||||||
let target = event
|
|
||||||
.target()
|
.target()
|
||||||
.expect("missing target")
|
.expect("missing target")
|
||||||
.dyn_into::<Element>()
|
.dyn_into::<Element>()
|
||||||
@@ -680,18 +285,36 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
|
|||||||
|
|
||||||
let typ = event.type_();
|
let typ = event.type_();
|
||||||
|
|
||||||
let element_id = target
|
loop {
|
||||||
.get_attribute("dioxus-id")
|
match target.get_attribute("data-dioxus-id").map(|f| f.parse()) {
|
||||||
.context("Could not find element id on event target")?
|
Some(Ok(id)) => {
|
||||||
.parse()?;
|
return Ok(UserEvent {
|
||||||
|
name: event_name_from_typ(&typ),
|
||||||
Ok(UserEvent {
|
data: virtual_event_from_websys_event(event.clone()),
|
||||||
name: event_name_from_typ(&typ),
|
element: Some(ElementId(id)),
|
||||||
data: virtual_event_from_websys_event(event.clone()),
|
scope_id: None,
|
||||||
element: Some(ElementId(element_id)),
|
priority: dioxus_core::EventPriority::Medium,
|
||||||
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 {
|
pub(crate) fn load_document() -> Document {
|
||||||
|
|||||||
+37
-6
@@ -61,10 +61,10 @@ pub use dioxus_core as dioxus;
|
|||||||
use dioxus_core::prelude::Component;
|
use dioxus_core::prelude::Component;
|
||||||
use futures_util::FutureExt;
|
use futures_util::FutureExt;
|
||||||
|
|
||||||
|
pub(crate) mod bindings;
|
||||||
mod cache;
|
mod cache;
|
||||||
mod cfg;
|
mod cfg;
|
||||||
mod dom;
|
mod dom;
|
||||||
mod nodeslab;
|
|
||||||
mod rehydrate;
|
mod rehydrate;
|
||||||
mod ric_raf;
|
mod ric_raf;
|
||||||
|
|
||||||
@@ -92,6 +92,29 @@ pub fn launch(root_component: Component) {
|
|||||||
launch_with_props(root_component, (), |c| c);
|
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.
|
/// Launches the VirtualDOM from the specified component function and props.
|
||||||
///
|
///
|
||||||
/// This method will block the thread with `spawn_local`
|
/// This method will block the thread with `spawn_local`
|
||||||
@@ -100,7 +123,11 @@ pub fn launch(root_component: Component) {
|
|||||||
///
|
///
|
||||||
/// ```rust, ignore
|
/// ```rust, ignore
|
||||||
/// fn main() {
|
/// 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)]
|
/// #[derive(ParitalEq, Props)]
|
||||||
@@ -112,15 +139,19 @@ pub fn launch(root_component: Component) {
|
|||||||
/// rsx!(cx, div {"hello {cx.props.name}"})
|
/// rsx!(cx, div {"hello {cx.props.name}"})
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn launch_with_props<T, F>(
|
pub fn launch_with_props<T>(
|
||||||
root_component: Component<T>,
|
root_component: Component<T>,
|
||||||
root_properties: T,
|
root_properties: T,
|
||||||
configuration_builder: F,
|
configuration_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig,
|
||||||
) where
|
) where
|
||||||
T: Send + 'static,
|
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));
|
wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
//! This module provides a mirror of the VirtualDOM Element Slab using a Vector.
|
|
||||||
|
|
||||||
use std::ops::{Index, IndexMut};
|
|
||||||
use web_sys::Node;
|
|
||||||
|
|
||||||
pub(crate) struct NodeSlab {
|
|
||||||
nodes: Vec<Option<Node>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NodeSlab {
|
|
||||||
pub fn new(capacity: usize) -> NodeSlab {
|
|
||||||
let nodes = Vec::with_capacity(capacity);
|
|
||||||
NodeSlab { nodes }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Index<usize> for NodeSlab {
|
|
||||||
type Output = Option<Node>;
|
|
||||||
fn index(&self, index: usize) -> &Self::Output {
|
|
||||||
&self.nodes[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IndexMut<usize> for NodeSlab {
|
|
||||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
|
||||||
if index >= self.nodes.capacity() * 3 {
|
|
||||||
panic!("Trying to mutate an element way too far out of bounds");
|
|
||||||
}
|
|
||||||
|
|
||||||
if index + 1 > self.nodes.len() {
|
|
||||||
self.nodes.resize_with(index + 1, || None);
|
|
||||||
}
|
|
||||||
&mut self.nodes[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,857 @@
|
|||||||
|
//! Implementation of a renderer for Dioxus on the web.
|
||||||
|
//!
|
||||||
|
//! Oustanding todos:
|
||||||
|
//! - Removing event listeners (delegation)
|
||||||
|
//! - Passive event listeners
|
||||||
|
//! - no-op event listener patch for safari
|
||||||
|
//! - tests to ensure dyn_into works for various event types.
|
||||||
|
//! - Partial delegation?>
|
||||||
|
|
||||||
|
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent};
|
||||||
|
use fxhash::FxHashMap;
|
||||||
|
use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc};
|
||||||
|
use wasm_bindgen::{closure::Closure, JsCast};
|
||||||
|
use web_sys::{
|
||||||
|
CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
|
||||||
|
HtmlOptionElement, HtmlTextAreaElement, Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{nodeslab::NodeSlab, WebConfig};
|
||||||
|
|
||||||
|
pub struct WebsysDom {
|
||||||
|
stack: Stack,
|
||||||
|
|
||||||
|
/// A map from ElementID (index) to Node
|
||||||
|
pub(crate) nodes: NodeSlab,
|
||||||
|
|
||||||
|
document: Document,
|
||||||
|
|
||||||
|
pub(crate) root: Element,
|
||||||
|
|
||||||
|
sender_callback: Rc<dyn Fn(SchedulerMsg)>,
|
||||||
|
|
||||||
|
// map of listener types to number of those listeners
|
||||||
|
// This is roughly a delegater
|
||||||
|
// TODO: check how infero delegates its events - some are more performant
|
||||||
|
listeners: FxHashMap<&'static str, ListenerEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListenerEntry = (usize, Closure<dyn FnMut(&Event)>);
|
||||||
|
|
||||||
|
impl WebsysDom {
|
||||||
|
pub fn new(cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
|
||||||
|
let document = load_document();
|
||||||
|
|
||||||
|
let nodes = NodeSlab::new(2000);
|
||||||
|
let listeners = FxHashMap::default();
|
||||||
|
|
||||||
|
let mut stack = Stack::with_capacity(10);
|
||||||
|
|
||||||
|
let root = load_document().get_element_by_id(&cfg.rootname).unwrap();
|
||||||
|
let root_node = root.clone().dyn_into::<Node>().unwrap();
|
||||||
|
stack.push(root_node);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
stack,
|
||||||
|
nodes,
|
||||||
|
listeners,
|
||||||
|
document,
|
||||||
|
sender_callback,
|
||||||
|
root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
|
||||||
|
for edit in edits.drain(..) {
|
||||||
|
match edit {
|
||||||
|
DomEdit::PushRoot { root } => self.push(root),
|
||||||
|
DomEdit::AppendChildren { many } => self.append_children(many),
|
||||||
|
DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
|
||||||
|
DomEdit::Remove { root } => self.remove(root),
|
||||||
|
DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id),
|
||||||
|
DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id),
|
||||||
|
DomEdit::CreateElementNs { tag, root: id, ns } => {
|
||||||
|
self.create_element(tag, Some(ns), id)
|
||||||
|
}
|
||||||
|
DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id),
|
||||||
|
DomEdit::NewEventListener {
|
||||||
|
event_name,
|
||||||
|
scope,
|
||||||
|
root: mounted_node_id,
|
||||||
|
} => self.new_event_listener(event_name, scope, mounted_node_id),
|
||||||
|
|
||||||
|
DomEdit::RemoveEventListener { event, root } => {
|
||||||
|
self.remove_event_listener(event, root)
|
||||||
|
}
|
||||||
|
|
||||||
|
DomEdit::SetText { text, root } => self.set_text(text, root),
|
||||||
|
DomEdit::SetAttribute {
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
ns,
|
||||||
|
root,
|
||||||
|
} => self.set_attribute(field, value, ns, root),
|
||||||
|
DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root),
|
||||||
|
|
||||||
|
DomEdit::InsertAfter { n, root } => self.insert_after(n, root),
|
||||||
|
DomEdit::InsertBefore { n, root } => self.insert_before(n, root),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn push(&mut self, root: u64) {
|
||||||
|
let key = root as usize;
|
||||||
|
let domnode = &self.nodes[key];
|
||||||
|
|
||||||
|
let real_node: Node = match domnode {
|
||||||
|
Some(n) => n.clone(),
|
||||||
|
None => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.stack.push(real_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_children(&mut self, many: u32) {
|
||||||
|
let root: Node = self
|
||||||
|
.stack
|
||||||
|
.list
|
||||||
|
.get(self.stack.list.len() - (1 + many as usize))
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
// We need to make sure to add comments between text nodes
|
||||||
|
// We ensure that the text siblings are patched by preventing the browser from merging
|
||||||
|
// neighboring text nodes. Originally inspired by some of React's work from 2016.
|
||||||
|
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
|
||||||
|
// -> https://github.com/facebook/react/pull/5753
|
||||||
|
/*
|
||||||
|
todo: we need to track this for replacing/insert after/etc
|
||||||
|
*/
|
||||||
|
let mut last_node_was_text = false;
|
||||||
|
|
||||||
|
for child in self
|
||||||
|
.stack
|
||||||
|
.list
|
||||||
|
.drain((self.stack.list.len() - many as usize)..)
|
||||||
|
{
|
||||||
|
if child.dyn_ref::<web_sys::Text>().is_some() {
|
||||||
|
if last_node_was_text {
|
||||||
|
let comment_node = self
|
||||||
|
.document
|
||||||
|
.create_comment("dioxus")
|
||||||
|
.dyn_into::<Node>()
|
||||||
|
.unwrap();
|
||||||
|
root.append_child(&comment_node).unwrap();
|
||||||
|
}
|
||||||
|
last_node_was_text = true;
|
||||||
|
} else {
|
||||||
|
last_node_was_text = false;
|
||||||
|
}
|
||||||
|
root.append_child(&child).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_with(&mut self, m: u32, root: u64) {
|
||||||
|
let old = self.nodes[root as usize].as_ref().unwrap();
|
||||||
|
|
||||||
|
let arr: js_sys::Array = self
|
||||||
|
.stack
|
||||||
|
.list
|
||||||
|
.drain((self.stack.list.len() - m as usize)..)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(el) = old.dyn_ref::<Element>() {
|
||||||
|
el.replace_with_with_node(&arr).unwrap();
|
||||||
|
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
|
||||||
|
el.replace_with_with_node(&arr).unwrap();
|
||||||
|
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
|
||||||
|
el.replace_with_with_node(&arr).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, root: u64) {
|
||||||
|
let node = self.nodes[root as usize].as_ref().unwrap();
|
||||||
|
if let Some(element) = node.dyn_ref::<Element>() {
|
||||||
|
element.remove();
|
||||||
|
} else {
|
||||||
|
if let Some(parent) = node.parent_node() {
|
||||||
|
parent.remove_child(&node).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_placeholder(&mut self, id: u64) {
|
||||||
|
self.create_element("pre", None, id);
|
||||||
|
self.set_attribute("hidden", "", None, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_text_node(&mut self, text: &str, id: u64) {
|
||||||
|
let textnode = self
|
||||||
|
.document
|
||||||
|
.create_text_node(text)
|
||||||
|
.dyn_into::<Node>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.stack.push(textnode.clone());
|
||||||
|
|
||||||
|
self.nodes[(id as usize)] = Some(textnode);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
|
||||||
|
let tag = wasm_bindgen::intern(tag);
|
||||||
|
|
||||||
|
let el = match ns {
|
||||||
|
Some(ns) => self
|
||||||
|
.document
|
||||||
|
.create_element_ns(Some(ns), tag)
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<Node>()
|
||||||
|
.unwrap(),
|
||||||
|
None => self
|
||||||
|
.document
|
||||||
|
.create_element(tag)
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<Node>()
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
use smallstr::SmallString;
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new();
|
||||||
|
write!(s, "{}", id).unwrap();
|
||||||
|
|
||||||
|
let el2 = el.dyn_ref::<Element>().unwrap();
|
||||||
|
el2.set_attribute("dioxus-id", s.as_str()).unwrap();
|
||||||
|
|
||||||
|
self.stack.push(el.clone());
|
||||||
|
self.nodes[(id as usize)] = Some(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) {
|
||||||
|
let event = wasm_bindgen::intern(event);
|
||||||
|
|
||||||
|
// attach the correct attributes to the element
|
||||||
|
// these will be used by accessing the event's target
|
||||||
|
// This ensures we only ever have one handler attached to the root, but decide
|
||||||
|
// dynamically when we want to call a listener.
|
||||||
|
|
||||||
|
let el = self.stack.top();
|
||||||
|
|
||||||
|
let el = el.dyn_ref::<Element>().unwrap();
|
||||||
|
|
||||||
|
el.set_attribute("dioxus-event", event).unwrap();
|
||||||
|
|
||||||
|
// Register the callback to decode
|
||||||
|
|
||||||
|
if let Some(entry) = self.listeners.get_mut(event) {
|
||||||
|
entry.0 += 1;
|
||||||
|
} else {
|
||||||
|
let trigger = self.sender_callback.clone();
|
||||||
|
|
||||||
|
let c: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
|
||||||
|
// "Result" cannot be received from JS
|
||||||
|
// Instead, we just build and immediately execute a closure that returns result
|
||||||
|
match decode_trigger(event) {
|
||||||
|
Ok(synthetic_event) => {
|
||||||
|
let target = event.target().unwrap();
|
||||||
|
if let Some(node) = target.dyn_ref::<HtmlElement>() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger.as_ref()(SchedulerMsg::Event(synthetic_event))
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let handler = Closure::wrap(c);
|
||||||
|
|
||||||
|
self.root
|
||||||
|
.add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Increment the listeners
|
||||||
|
self.listeners.insert(event.into(), (1, handler));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_event_listener(&mut self, _event: &str, _root: u64) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_text(&mut self, text: &str, root: u64) {
|
||||||
|
let el = self.nodes[root as usize].as_ref().unwrap();
|
||||||
|
el.set_text_content(Some(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) {
|
||||||
|
let node = self.nodes[root as usize].as_ref().unwrap();
|
||||||
|
if ns == Some("style") {
|
||||||
|
if let Some(el) = node.dyn_ref::<Element>() {
|
||||||
|
let el = el.dyn_ref::<HtmlElement>().unwrap();
|
||||||
|
let style_dc: CssStyleDeclaration = el.style();
|
||||||
|
style_dc.set_property(name, value).unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let fallback = || {
|
||||||
|
let el = node.dyn_ref::<Element>().unwrap();
|
||||||
|
el.set_attribute(name, value).unwrap()
|
||||||
|
};
|
||||||
|
match name {
|
||||||
|
"dangerous_inner_html" => {
|
||||||
|
if let Some(el) = node.dyn_ref::<Element>() {
|
||||||
|
el.set_inner_html(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"value" => {
|
||||||
|
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
|
||||||
|
/*
|
||||||
|
if the attribute being set is the same as the value of the input, then don't bother setting it.
|
||||||
|
This is used in controlled components to keep the cursor in the right spot.
|
||||||
|
|
||||||
|
this logic should be moved into the virtualdom since we have the notion of "volatile"
|
||||||
|
*/
|
||||||
|
if input.value() != value {
|
||||||
|
input.set_value(value);
|
||||||
|
}
|
||||||
|
} else if let Some(node) = node.dyn_ref::<HtmlTextAreaElement>() {
|
||||||
|
if name == "value" {
|
||||||
|
node.set_value(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"checked" => {
|
||||||
|
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
|
||||||
|
match value {
|
||||||
|
"true" => input.set_checked(true),
|
||||||
|
"false" => input.set_checked(false),
|
||||||
|
_ => fallback(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"selected" => {
|
||||||
|
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
|
||||||
|
node.set_selected(true);
|
||||||
|
} else {
|
||||||
|
fallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
|
||||||
|
if value == "false" {
|
||||||
|
if let Some(el) = node.dyn_ref::<Element>() {
|
||||||
|
match name {
|
||||||
|
"allowfullscreen"
|
||||||
|
| "allowpaymentrequest"
|
||||||
|
| "async"
|
||||||
|
| "autofocus"
|
||||||
|
| "autoplay"
|
||||||
|
| "checked"
|
||||||
|
| "controls"
|
||||||
|
| "default"
|
||||||
|
| "defer"
|
||||||
|
| "disabled"
|
||||||
|
| "formnovalidate"
|
||||||
|
| "hidden"
|
||||||
|
| "ismap"
|
||||||
|
| "itemscope"
|
||||||
|
| "loop"
|
||||||
|
| "multiple"
|
||||||
|
| "muted"
|
||||||
|
| "nomodule"
|
||||||
|
| "novalidate"
|
||||||
|
| "open"
|
||||||
|
| "playsinline"
|
||||||
|
| "readonly"
|
||||||
|
| "required"
|
||||||
|
| "reversed"
|
||||||
|
| "selected"
|
||||||
|
| "truespeed" => {
|
||||||
|
let _ = el.remove_attribute(name);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let _ = el.set_attribute(name, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_attribute(&mut self, name: &str, root: u64) {
|
||||||
|
let node = self.nodes[root as usize].as_ref().unwrap();
|
||||||
|
if let Some(node) = node.dyn_ref::<web_sys::Element>() {
|
||||||
|
node.remove_attribute(name).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
|
||||||
|
// Some attributes are "volatile" and don't work through `removeAttribute`.
|
||||||
|
if name == "value" {
|
||||||
|
node.set_value("");
|
||||||
|
}
|
||||||
|
if name == "checked" {
|
||||||
|
node.set_checked(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
|
||||||
|
if name == "selected" {
|
||||||
|
node.set_selected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_after(&mut self, n: u32, root: u64) {
|
||||||
|
let old = self.nodes[root as usize].as_ref().unwrap();
|
||||||
|
|
||||||
|
let arr: js_sys::Array = self
|
||||||
|
.stack
|
||||||
|
.list
|
||||||
|
.drain((self.stack.list.len() - n as usize)..)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(el) = old.dyn_ref::<Element>() {
|
||||||
|
el.after_with_node(&arr).unwrap();
|
||||||
|
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
|
||||||
|
el.after_with_node(&arr).unwrap();
|
||||||
|
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
|
||||||
|
el.after_with_node(&arr).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_before(&mut self, n: u32, root: u64) {
|
||||||
|
let anchor = self.nodes[root as usize].as_ref().unwrap();
|
||||||
|
|
||||||
|
if n == 1 {
|
||||||
|
let before = self.stack.pop();
|
||||||
|
|
||||||
|
anchor
|
||||||
|
.parent_node()
|
||||||
|
.unwrap()
|
||||||
|
.insert_before(&before, Some(&anchor))
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
let arr: js_sys::Array = self
|
||||||
|
.stack
|
||||||
|
.list
|
||||||
|
.drain((self.stack.list.len() - n as usize)..)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(el) = anchor.dyn_ref::<Element>() {
|
||||||
|
el.before_with_node(&arr).unwrap();
|
||||||
|
} else if let Some(el) = anchor.dyn_ref::<web_sys::CharacterData>() {
|
||||||
|
el.before_with_node(&arr).unwrap();
|
||||||
|
} else if let Some(el) = anchor.dyn_ref::<web_sys::DocumentType>() {
|
||||||
|
el.before_with_node(&arr).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct Stack {
|
||||||
|
list: Vec<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stack {
|
||||||
|
#[inline]
|
||||||
|
fn with_capacity(cap: usize) -> Self {
|
||||||
|
Stack {
|
||||||
|
list: Vec::with_capacity(cap),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn push(&mut self, node: Node) {
|
||||||
|
self.list.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn pop(&mut self) -> Node {
|
||||||
|
self.list.pop().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn top(&self) -> &Node {
|
||||||
|
match self.list.last() {
|
||||||
|
Some(a) => a,
|
||||||
|
None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DioxusWebsysEvent(web_sys::Event);
|
||||||
|
|
||||||
|
// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread
|
||||||
|
unsafe impl Send for DioxusWebsysEvent {}
|
||||||
|
unsafe impl Sync for DioxusWebsysEvent {}
|
||||||
|
|
||||||
|
// todo: some of these events are being casted to the wrong event type.
|
||||||
|
// We need tests that simulate clicks/etc and make sure every event type works.
|
||||||
|
fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send + Sync> {
|
||||||
|
use dioxus_html::on::*;
|
||||||
|
use dioxus_html::KeyCode;
|
||||||
|
|
||||||
|
match event.type_().as_str() {
|
||||||
|
"copy" | "cut" | "paste" => Arc::new(ClipboardData {}),
|
||||||
|
"compositionend" | "compositionstart" | "compositionupdate" => {
|
||||||
|
let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap();
|
||||||
|
Arc::new(CompositionData {
|
||||||
|
data: evt.data().unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"keydown" | "keypress" | "keyup" => {
|
||||||
|
let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap();
|
||||||
|
Arc::new(KeyboardData {
|
||||||
|
alt_key: evt.alt_key(),
|
||||||
|
char_code: evt.char_code(),
|
||||||
|
key: evt.key(),
|
||||||
|
key_code: KeyCode::from_raw_code(evt.key_code() as u8),
|
||||||
|
ctrl_key: evt.ctrl_key(),
|
||||||
|
locale: "not implemented".to_string(),
|
||||||
|
location: evt.location() as usize,
|
||||||
|
meta_key: evt.meta_key(),
|
||||||
|
repeat: evt.repeat(),
|
||||||
|
shift_key: evt.shift_key(),
|
||||||
|
which: evt.which() as usize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"focus" | "blur" => Arc::new(FocusData {}),
|
||||||
|
|
||||||
|
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
|
||||||
|
// don't have a good solution with the serialized event problem
|
||||||
|
"change" | "input" | "invalid" | "reset" | "submit" => {
|
||||||
|
let evt: &web_sys::Event = event.dyn_ref().unwrap();
|
||||||
|
|
||||||
|
let target: web_sys::EventTarget = evt.target().unwrap();
|
||||||
|
let value: String = (&target)
|
||||||
|
.dyn_ref()
|
||||||
|
.map(|input: &web_sys::HtmlInputElement| {
|
||||||
|
// todo: special case more input types
|
||||||
|
match input.type_().as_str() {
|
||||||
|
"checkbox" => {
|
||||||
|
match input.checked() {
|
||||||
|
true => "true".to_string(),
|
||||||
|
false => "false".to_string(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
input.value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
target
|
||||||
|
.dyn_ref()
|
||||||
|
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
|
||||||
|
})
|
||||||
|
// select elements are NOT input events - because - why woudn't they be??
|
||||||
|
.or_else(|| {
|
||||||
|
target
|
||||||
|
.dyn_ref()
|
||||||
|
.map(|input: &web_sys::HtmlSelectElement| input.value())
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
target
|
||||||
|
.dyn_ref::<web_sys::HtmlElement>()
|
||||||
|
.unwrap()
|
||||||
|
.text_content()
|
||||||
|
})
|
||||||
|
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
|
||||||
|
|
||||||
|
Arc::new(FormData { value })
|
||||||
|
}
|
||||||
|
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
|
||||||
|
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
|
||||||
|
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
|
||||||
|
let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap();
|
||||||
|
Arc::new(MouseData {
|
||||||
|
alt_key: evt.alt_key(),
|
||||||
|
button: evt.button(),
|
||||||
|
buttons: evt.buttons(),
|
||||||
|
client_x: evt.client_x(),
|
||||||
|
client_y: evt.client_y(),
|
||||||
|
ctrl_key: evt.ctrl_key(),
|
||||||
|
meta_key: evt.meta_key(),
|
||||||
|
screen_x: evt.screen_x(),
|
||||||
|
screen_y: evt.screen_y(),
|
||||||
|
shift_key: evt.shift_key(),
|
||||||
|
page_x: evt.page_x(),
|
||||||
|
page_y: evt.page_y(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
|
||||||
|
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
|
||||||
|
let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap();
|
||||||
|
Arc::new(PointerData {
|
||||||
|
alt_key: evt.alt_key(),
|
||||||
|
button: evt.button(),
|
||||||
|
buttons: evt.buttons(),
|
||||||
|
client_x: evt.client_x(),
|
||||||
|
client_y: evt.client_y(),
|
||||||
|
ctrl_key: evt.ctrl_key(),
|
||||||
|
meta_key: evt.meta_key(),
|
||||||
|
page_x: evt.page_x(),
|
||||||
|
page_y: evt.page_y(),
|
||||||
|
screen_x: evt.screen_x(),
|
||||||
|
screen_y: evt.screen_y(),
|
||||||
|
shift_key: evt.shift_key(),
|
||||||
|
pointer_id: evt.pointer_id(),
|
||||||
|
width: evt.width(),
|
||||||
|
height: evt.height(),
|
||||||
|
pressure: evt.pressure(),
|
||||||
|
tangential_pressure: evt.tangential_pressure(),
|
||||||
|
tilt_x: evt.tilt_x(),
|
||||||
|
tilt_y: evt.tilt_y(),
|
||||||
|
twist: evt.twist(),
|
||||||
|
pointer_type: evt.pointer_type(),
|
||||||
|
is_primary: evt.is_primary(),
|
||||||
|
// get_modifier_state: evt.get_modifier_state(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"select" => Arc::new(SelectionData {}),
|
||||||
|
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
|
||||||
|
let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap();
|
||||||
|
Arc::new(TouchData {
|
||||||
|
alt_key: evt.alt_key(),
|
||||||
|
ctrl_key: evt.ctrl_key(),
|
||||||
|
meta_key: evt.meta_key(),
|
||||||
|
shift_key: evt.shift_key(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"scroll" => Arc::new(()),
|
||||||
|
"wheel" => {
|
||||||
|
let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap();
|
||||||
|
Arc::new(WheelData {
|
||||||
|
delta_x: evt.delta_x(),
|
||||||
|
delta_y: evt.delta_y(),
|
||||||
|
delta_z: evt.delta_z(),
|
||||||
|
delta_mode: evt.delta_mode(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"animationstart" | "animationend" | "animationiteration" => {
|
||||||
|
let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap();
|
||||||
|
Arc::new(AnimationData {
|
||||||
|
elapsed_time: evt.elapsed_time(),
|
||||||
|
animation_name: evt.animation_name(),
|
||||||
|
pseudo_element: evt.pseudo_element(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"transitionend" => {
|
||||||
|
let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap();
|
||||||
|
Arc::new(TransitionData {
|
||||||
|
elapsed_time: evt.elapsed_time(),
|
||||||
|
property_name: evt.property_name(),
|
||||||
|
pseudo_element: evt.pseudo_element(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
|
||||||
|
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
|
||||||
|
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
|
||||||
|
| "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}),
|
||||||
|
"toggle" => Arc::new(ToggleData {}),
|
||||||
|
_ => Arc::new(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function decodes a websys event and produces an EventTrigger
|
||||||
|
/// With the websys implementation, we attach a unique key to the nodes
|
||||||
|
fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
let target = event
|
||||||
|
.target()
|
||||||
|
.expect("missing target")
|
||||||
|
.dyn_into::<Element>()
|
||||||
|
.expect("not a valid element");
|
||||||
|
|
||||||
|
let typ = event.type_();
|
||||||
|
|
||||||
|
let element_id = target
|
||||||
|
.get_attribute("dioxus-id")
|
||||||
|
.context("Could not find element id on event target")?
|
||||||
|
.parse()?;
|
||||||
|
|
||||||
|
Ok(UserEvent {
|
||||||
|
name: event_name_from_typ(&typ),
|
||||||
|
data: virtual_event_from_websys_event(event.clone()),
|
||||||
|
element: Some(ElementId(element_id)),
|
||||||
|
scope_id: None,
|
||||||
|
priority: dioxus_core::EventPriority::Medium,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_document() -> Document {
|
||||||
|
web_sys::window()
|
||||||
|
.expect("should have access to the Window")
|
||||||
|
.document()
|
||||||
|
.expect("should have access to the Document")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_name_from_typ(typ: &str) -> &'static str {
|
||||||
|
match typ {
|
||||||
|
"copy" => "copy",
|
||||||
|
"cut" => "cut",
|
||||||
|
"paste" => "paste",
|
||||||
|
"compositionend" => "compositionend",
|
||||||
|
"compositionstart" => "compositionstart",
|
||||||
|
"compositionupdate" => "compositionupdate",
|
||||||
|
"keydown" => "keydown",
|
||||||
|
"keypress" => "keypress",
|
||||||
|
"keyup" => "keyup",
|
||||||
|
"focus" => "focus",
|
||||||
|
"blur" => "blur",
|
||||||
|
"change" => "change",
|
||||||
|
"input" => "input",
|
||||||
|
"invalid" => "invalid",
|
||||||
|
"reset" => "reset",
|
||||||
|
"submit" => "submit",
|
||||||
|
"click" => "click",
|
||||||
|
"contextmenu" => "contextmenu",
|
||||||
|
"doubleclick" => "doubleclick",
|
||||||
|
"drag" => "drag",
|
||||||
|
"dragend" => "dragend",
|
||||||
|
"dragenter" => "dragenter",
|
||||||
|
"dragexit" => "dragexit",
|
||||||
|
"dragleave" => "dragleave",
|
||||||
|
"dragover" => "dragover",
|
||||||
|
"dragstart" => "dragstart",
|
||||||
|
"drop" => "drop",
|
||||||
|
"mousedown" => "mousedown",
|
||||||
|
"mouseenter" => "mouseenter",
|
||||||
|
"mouseleave" => "mouseleave",
|
||||||
|
"mousemove" => "mousemove",
|
||||||
|
"mouseout" => "mouseout",
|
||||||
|
"mouseover" => "mouseover",
|
||||||
|
"mouseup" => "mouseup",
|
||||||
|
"pointerdown" => "pointerdown",
|
||||||
|
"pointermove" => "pointermove",
|
||||||
|
"pointerup" => "pointerup",
|
||||||
|
"pointercancel" => "pointercancel",
|
||||||
|
"gotpointercapture" => "gotpointercapture",
|
||||||
|
"lostpointercapture" => "lostpointercapture",
|
||||||
|
"pointerenter" => "pointerenter",
|
||||||
|
"pointerleave" => "pointerleave",
|
||||||
|
"pointerover" => "pointerover",
|
||||||
|
"pointerout" => "pointerout",
|
||||||
|
"select" => "select",
|
||||||
|
"touchcancel" => "touchcancel",
|
||||||
|
"touchend" => "touchend",
|
||||||
|
"touchmove" => "touchmove",
|
||||||
|
"touchstart" => "touchstart",
|
||||||
|
"scroll" => "scroll",
|
||||||
|
"wheel" => "wheel",
|
||||||
|
"animationstart" => "animationstart",
|
||||||
|
"animationend" => "animationend",
|
||||||
|
"animationiteration" => "animationiteration",
|
||||||
|
"transitionend" => "transitionend",
|
||||||
|
"abort" => "abort",
|
||||||
|
"canplay" => "canplay",
|
||||||
|
"canplaythrough" => "canplaythrough",
|
||||||
|
"durationchange" => "durationchange",
|
||||||
|
"emptied" => "emptied",
|
||||||
|
"encrypted" => "encrypted",
|
||||||
|
"ended" => "ended",
|
||||||
|
"error" => "error",
|
||||||
|
"loadeddata" => "loadeddata",
|
||||||
|
"loadedmetadata" => "loadedmetadata",
|
||||||
|
"loadstart" => "loadstart",
|
||||||
|
"pause" => "pause",
|
||||||
|
"play" => "play",
|
||||||
|
"playing" => "playing",
|
||||||
|
"progress" => "progress",
|
||||||
|
"ratechange" => "ratechange",
|
||||||
|
"seeked" => "seeked",
|
||||||
|
"seeking" => "seeking",
|
||||||
|
"stalled" => "stalled",
|
||||||
|
"suspend" => "suspend",
|
||||||
|
"timeupdate" => "timeupdate",
|
||||||
|
"volumechange" => "volumechange",
|
||||||
|
"waiting" => "waiting",
|
||||||
|
"toggle" => "toggle",
|
||||||
|
_ => {
|
||||||
|
panic!("unsupported event type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//! This module provides a mirror of the VirtualDOM Element Slab using a Vector.
|
||||||
|
|
||||||
|
use std::ops::{Index, IndexMut};
|
||||||
|
use web_sys::Node;
|
||||||
|
|
||||||
|
pub(crate) struct NodeSlab {
|
||||||
|
nodes: Vec<Option<Node>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeSlab {
|
||||||
|
pub fn new(capacity: usize) -> NodeSlab {
|
||||||
|
let nodes = Vec::with_capacity(capacity);
|
||||||
|
NodeSlab { nodes }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Index<usize> for NodeSlab {
|
||||||
|
type Output = Option<Node>;
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
&self.nodes[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexMut<usize> for NodeSlab {
|
||||||
|
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||||
|
if index >= self.nodes.capacity() * 3 {
|
||||||
|
panic!("Trying to mutate an element way too far out of bounds");
|
||||||
|
}
|
||||||
|
|
||||||
|
if index + 1 > self.nodes.len() {
|
||||||
|
self.nodes.resize_with(index + 1, || None);
|
||||||
|
}
|
||||||
|
&mut self.nodes[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct Stack {
|
||||||
|
list: Vec<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stack {
|
||||||
|
#[inline]
|
||||||
|
fn with_capacity(cap: usize) -> Self {
|
||||||
|
Stack {
|
||||||
|
list: Vec::with_capacity(cap),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn push(&mut self, node: Node) {
|
||||||
|
self.list.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn pop(&mut self) -> Node {
|
||||||
|
self.list.pop().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn top(&self) -> &Node {
|
||||||
|
match self.list.last() {
|
||||||
|
Some(a) => a,
|
||||||
|
None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,7 +80,8 @@ impl WebsysDom {
|
|||||||
|
|
||||||
*last_node_was_text = true;
|
*last_node_was_text = true;
|
||||||
|
|
||||||
self.nodes[node_id.0] = Some(node);
|
self.interpreter.set_node(node_id.0, node);
|
||||||
|
// self.nodes[node_id.0] = Some(node);
|
||||||
|
|
||||||
*cur_place += 1;
|
*cur_place += 1;
|
||||||
}
|
}
|
||||||
@@ -105,7 +106,8 @@ impl WebsysDom {
|
|||||||
.set_attribute("dioxus-id", s.as_str())
|
.set_attribute("dioxus-id", s.as_str())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
self.nodes[node_id.0] = Some(node.clone());
|
self.interpreter.set_node(node_id.0, node.clone());
|
||||||
|
// self.nodes[node_id.0] = Some(node.clone());
|
||||||
|
|
||||||
*cur_place += 1;
|
*cur_place += 1;
|
||||||
|
|
||||||
@@ -116,7 +118,15 @@ impl WebsysDom {
|
|||||||
// we cant have the last node be text
|
// we cant have the last node be text
|
||||||
let mut last_node_was_text = false;
|
let mut last_node_was_text = false;
|
||||||
for child in vel.children {
|
for child in vel.children {
|
||||||
self.rehydrate_single(nodes, place, dom, &child, &mut last_node_was_text)?;
|
self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for listener in vel.listeners {
|
||||||
|
self.interpreter.NewEventListener(
|
||||||
|
listener.event,
|
||||||
|
listener.mounted_node.get().unwrap().as_u64(),
|
||||||
|
self.handler.as_ref().unchecked_ref(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
place.pop();
|
place.pop();
|
||||||
@@ -135,14 +145,16 @@ impl WebsysDom {
|
|||||||
let cur_place = place.last_mut().unwrap();
|
let cur_place = place.last_mut().unwrap();
|
||||||
let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap();
|
let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap();
|
||||||
|
|
||||||
self.nodes[node_id.0] = Some(node);
|
self.interpreter.set_node(node_id.0, node);
|
||||||
|
|
||||||
|
// self.nodes[node_id.0] = Some(node);
|
||||||
|
|
||||||
*cur_place += 1;
|
*cur_place += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
VNode::Fragment(el) => {
|
VNode::Fragment(el) => {
|
||||||
for el in el.children {
|
for el in el.children {
|
||||||
self.rehydrate_single(nodes, place, dom, &el, last_node_was_text)?;
|
self.rehydrate_single(nodes, place, dom, el, last_node_was_text)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ impl RafLoop {
|
|||||||
let ric_fn = self.ric_closure.as_ref().dyn_ref::<Function>().unwrap();
|
let ric_fn = self.ric_closure.as_ref().dyn_ref::<Function>().unwrap();
|
||||||
let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap();
|
let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap();
|
||||||
let deadline = self.ric_receiver.recv().await.unwrap();
|
let deadline = self.ric_receiver.recv().await.unwrap();
|
||||||
let deadline = TimeoutFuture::new(deadline);
|
TimeoutFuture::new(deadline)
|
||||||
deadline
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn wait_for_raf(&self) {
|
pub async fn wait_for_raf(&self) {
|
||||||
|
|||||||
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user