mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-23 04:33:06 +00:00
Merge branch 'upstream' into simplify-native-core
This commit is contained in:
commit
d53bfb6c56
57 changed files with 1553 additions and 368 deletions
|
@ -19,6 +19,7 @@ members = [
|
||||||
"packages/native-core",
|
"packages/native-core",
|
||||||
"packages/rsx-rosetta",
|
"packages/rsx-rosetta",
|
||||||
"packages/signals",
|
"packages/signals",
|
||||||
|
"packages/hot-reload",
|
||||||
"docs/guide",
|
"docs/guide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ async fn main() {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
println!("Listening on http://{}", addr);
|
println!("Listening on http://{addr}");
|
||||||
|
|
||||||
axum::Server::bind(&addr.to_string().parse().unwrap())
|
axum::Server::bind(&addr.to_string().parse().unwrap())
|
||||||
.serve(app.into_make_service())
|
.serve(app.into_make_service())
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
- [Getting Started](getting_started/index.md)
|
- [Getting Started](getting_started/index.md)
|
||||||
- [Desktop](getting_started/desktop.md)
|
- [Desktop](getting_started/desktop.md)
|
||||||
- [Web](getting_started/web.md)
|
- [Web](getting_started/web.md)
|
||||||
- [Hot Reload](getting_started/hot_reload.md)
|
|
||||||
- [Server-Side Rendering](getting_started/ssr.md)
|
- [Server-Side Rendering](getting_started/ssr.md)
|
||||||
- [Liveview](getting_started/liveview.md)
|
- [Liveview](getting_started/liveview.md)
|
||||||
- [Terminal UI](getting_started/tui.md)
|
- [Terminal UI](getting_started/tui.md)
|
||||||
- [Mobile](getting_started/mobile.md)
|
- [Mobile](getting_started/mobile.md)
|
||||||
|
- [Hot Reloading](getting_started/hot_reload.md)
|
||||||
- [Describing the UI](describing_ui/index.md)
|
- [Describing the UI](describing_ui/index.md)
|
||||||
- [Special Attributes](describing_ui/special_attributes.md)
|
- [Special Attributes](describing_ui/special_attributes.md)
|
||||||
- [Components](describing_ui/components.md)
|
- [Components](describing_ui/components.md)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer.
|
Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer.
|
||||||
|
|
||||||
Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but only really require processing `DomEdits` and sending `UserEvents`.
|
Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but only really require processing `Mutations` and sending `UserEvents`.
|
||||||
|
|
||||||
## The specifics:
|
## The specifics:
|
||||||
|
|
||||||
|
@ -46,114 +46,179 @@ enum Mutation {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The Dioxus diffing mechanism operates as a [stack machine](https://en.wikipedia.org/wiki/Stack_machine) where the "push_root" method pushes a new "real" DOM node onto the stack and "append_child" and "replace_with" both remove nodes from the stack.
|
The Dioxus diffing mechanism operates as a [stack machine](https://en.wikipedia.org/wiki/Stack_machine) where the [LoadTemplate](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.LoadTemplate), [CreatePlaceholder](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.CreatePlaceholder), and [CreateTextNode](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.CreateTextNode) mutations pushes a new "real" DOM node onto the stack and [AppendChildren](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.AppendChildren), [InsertAfter](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.InsertAfter), [InsertBefore](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.InsertBefore), [ReplacePlaceholder](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.ReplacePlaceholder), and [ReplaceWith](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.ReplaceWith) all remove nodes from the stack.
|
||||||
|
|
||||||
|
## Node storage
|
||||||
|
|
||||||
|
Dioxus saves and loads elements with IDs. Inside the VirtualDOM, this is just tracked as as a u64.
|
||||||
|
|
||||||
|
Whenever a `CreateElement` edit is generated during diffing, Dioxus increments its node counter and assigns that new element its current NodeCount. The RealDom is responsible for remembering this ID and pushing the correct node when id is used in a mutation. Dioxus reclaims the IDs of elements when removed. To stay in sync with Dioxus you can use a sparse Vec (Vec<Option<T>>) with possibly unoccupied items. You can use the ids as indexes into the Vec for elements, and grow the Vec when an id does not exist.
|
||||||
|
|
||||||
### An Example
|
### An Example
|
||||||
|
|
||||||
For the sake of understanding, let's consider this example – a very simple UI declaration:
|
For the sake of understanding, let's consider this example – a very simple UI declaration:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
rsx!( h1 {"count {x}"} )
|
rsx!( h1 {"count: {x}"} )
|
||||||
```
|
```
|
||||||
|
|
||||||
To get things started, Dioxus must first navigate to the container of this h1 tag. To "navigate" here, the internal diffing algorithm generates the DomEdit `PushRoot` where the ID of the root is the container.
|
#### Building Templates
|
||||||
|
|
||||||
When the renderer receives this instruction, it pushes the actual Node onto its own stack. The real renderer's stack will look like this:
|
The above rsx will create a template that contains one static h1 tag and a placeholder for a dynamic text node. The template contains the static parts of the UI, and ids for the dynamic parts along with the paths to access them.
|
||||||
|
|
||||||
|
The template will look something like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Template {
|
||||||
|
// Some id that is unique for the entire project
|
||||||
|
name: "main.rs:1:1:0",
|
||||||
|
// The root nodes of the template
|
||||||
|
roots: &[
|
||||||
|
TemplateNode::Element {
|
||||||
|
tag: "h1",
|
||||||
|
namespace: None,
|
||||||
|
attrs: &[],
|
||||||
|
children: &[
|
||||||
|
TemplateNode::DynamicText {
|
||||||
|
id: 0
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// the path to each of the dynamic nodes
|
||||||
|
node_paths: &[
|
||||||
|
// the path to dynamic node with a id of 0
|
||||||
|
&[
|
||||||
|
// on the first root node
|
||||||
|
0,
|
||||||
|
// the first child of the root node
|
||||||
|
0,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
// the path to each of the dynamic attributes
|
||||||
|
attr_paths: &'a [&'a [u8]],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> For more detailed docs about the struture of templates see the [Template api docs](https://docs.rs/dioxus-core/latest/dioxus_core/prelude/struct.Template.html)
|
||||||
|
|
||||||
|
This template will be sent to the renderer in the [list of templates](https://docs.rs/dioxus-core/latest/dioxus_core/struct.Mutations.html#structfield.templates) supplied with the mutations the first time it is used. Any time the renderer encounters a [LoadTemplate](https://docs.rs/dioxus-core/latest/dioxus_core/enum.Mutation.html#variant.LoadTemplate) mutation after this, it should clone the template and store it in the given id.
|
||||||
|
|
||||||
|
For dynamic nodes and dynamic text nodes, a placeholder node should be created and inserted into the UI so that the node can be navigated to later.
|
||||||
|
|
||||||
|
In HTML renderers, this template could look like:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1>""</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Applying Mutations
|
||||||
|
|
||||||
|
After the renderer has created all of the new templates, it can begin to process the mutations.
|
||||||
|
|
||||||
|
When the renderer starts, it should contain the Root node on the stack and store the Root node with an id of 0. The Root node is the top-level node of the UI. In HTML, this is the `<div id="main">` element.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
instructions: []
|
||||||
|
stack: [
|
||||||
|
RootNode,
|
||||||
|
]
|
||||||
|
nodes: [
|
||||||
|
RootNode,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The first mutation is a `LoadTemplate` mutation. This tells the renderer to load a root from the template with the given id. The renderer will then push the root node of the template onto the stack and store it with an id for later. In this case, the root node is an h1 element.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
instructions: [
|
instructions: [
|
||||||
PushRoot(Container)
|
LoadTemplate {
|
||||||
|
// the id of the template
|
||||||
|
name: "main.rs:1:1:0",
|
||||||
|
// the index of the root node in the template
|
||||||
|
index: 0,
|
||||||
|
// the id to store
|
||||||
|
id: ElementId(1),
|
||||||
|
}
|
||||||
]
|
]
|
||||||
stack: [
|
stack: [
|
||||||
ContainerNode,
|
RootNode,
|
||||||
|
<h1>""</h1>,
|
||||||
|
]
|
||||||
|
nodes: [
|
||||||
|
RootNode,
|
||||||
|
<h1>""</h1>,
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, Dioxus will encounter the h1 node. The diff algorithm decides that this node needs to be created, so Dioxus will generate the DomEdit `CreateElement`. When the renderer receives this instruction, it will create an unmounted node and push it into its own stack:
|
Next, Dioxus will create the dynamic text node. The diff algorithm decides that this node needs to be created, so Dioxus will generate the Mutation `HydrateText`. When the renderer receives this instruction, it will navigate to the placeholder text node in the template and replace it with the new text.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
instructions: [
|
instructions: [
|
||||||
PushRoot(Container),
|
LoadTemplate {
|
||||||
CreateElement(h1),
|
name: "main.rs:1:1:0",
|
||||||
|
index: 0,
|
||||||
|
id: ElementId(1),
|
||||||
|
},
|
||||||
|
HydrateText {
|
||||||
|
// the id to store the text node
|
||||||
|
id: ElementId(2),
|
||||||
|
// the text to set
|
||||||
|
text: "count: 0",
|
||||||
|
}
|
||||||
]
|
]
|
||||||
stack: [
|
stack: [
|
||||||
ContainerNode,
|
RootNode,
|
||||||
h1,
|
<h1>"count: 0"</h1>,
|
||||||
|
]
|
||||||
|
nodes: [
|
||||||
|
RootNode,
|
||||||
|
<h1>"count: 0"</h1>,
|
||||||
|
"count: 0",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
Next, Dioxus sees the text node, and generates the `CreateTextNode` DomEdit:
|
|
||||||
```rust
|
Remember, the h1 node is not attached to anything (it is unmounted) so Dioxus needs to generate an Edit that connects the h1 node to the Root. It depends on the situation, but in this case, we use `AppendChildren`. This pops the text node off the stack, leaving the Root element as the next element on the stack.
|
||||||
instructions: [
|
|
||||||
PushRoot(Container),
|
|
||||||
CreateElement(h1),
|
|
||||||
CreateTextNode("hello world")
|
|
||||||
]
|
|
||||||
stack: [
|
|
||||||
ContainerNode,
|
|
||||||
h1,
|
|
||||||
"hello world"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
Remember, the text node is not attached to anything (it is unmounted) so Dioxus needs to generate an Edit that connects the text node to the h1 element. It depends on the situation, but in this case, we use `AppendChildren`. This pops the text node off the stack, leaving the h1 element as the next element in line.
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
instructions: [
|
instructions: [
|
||||||
PushRoot(Container),
|
LoadTemplate {
|
||||||
CreateElement(h1),
|
name: "main.rs:1:1:0",
|
||||||
CreateTextNode("hello world"),
|
index: 0,
|
||||||
AppendChildren(1)
|
id: ElementId(1),
|
||||||
|
},
|
||||||
|
HydrateText {
|
||||||
|
id: ElementId(2),
|
||||||
|
text: "count: 0",
|
||||||
|
},
|
||||||
|
AppendChildren {
|
||||||
|
// the id of the parent node
|
||||||
|
id: ElementId(0),
|
||||||
|
// the number of nodes to pop off the stack and append
|
||||||
|
m: 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
stack: [
|
stack: [
|
||||||
ContainerNode,
|
RootNode,
|
||||||
h1
|
|
||||||
]
|
]
|
||||||
```
|
nodes: [
|
||||||
We call `AppendChildren` again, popping off the h1 node and attaching it to the parent:
|
RootNode,
|
||||||
```rust
|
<h1>"count: 0"</h1>,
|
||||||
instructions: [
|
"count: 0",
|
||||||
PushRoot(Container),
|
|
||||||
CreateElement(h1),
|
|
||||||
CreateTextNode("hello world"),
|
|
||||||
AppendChildren(1),
|
|
||||||
AppendChildren(1)
|
|
||||||
]
|
]
|
||||||
stack: [
|
|
||||||
ContainerNode,
|
|
||||||
]
|
|
||||||
```
|
|
||||||
Finally, the container is popped since we don't need it anymore.
|
|
||||||
```rust
|
|
||||||
instructions: [
|
|
||||||
PushRoot(Container),
|
|
||||||
CreateElement(h1),
|
|
||||||
CreateTextNode("hello world"),
|
|
||||||
AppendChildren(1),
|
|
||||||
AppendChildren(1),
|
|
||||||
PopRoot
|
|
||||||
]
|
|
||||||
stack: []
|
|
||||||
```
|
```
|
||||||
Over time, our stack looked like this:
|
Over time, our stack looked like this:
|
||||||
```rust
|
```rust
|
||||||
[]
|
[Root]
|
||||||
[Container]
|
[Root, <h1>""</h1>]
|
||||||
[Container, h1]
|
[Root, <h1>"count: 0"</h1>]
|
||||||
[Container, h1, "hello world"]
|
[Root]
|
||||||
[Container, h1]
|
|
||||||
[Container]
|
|
||||||
[]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice how our stack is empty once UI has been mounted. Conveniently, this approach completely separates the Virtual DOM and the Real DOM. Additionally, these edits are serializable, meaning we can even manage UIs across a network connection. This little stack machine and serialized edits make Dioxus independent of platform specifics.
|
Conveniently, this approach completely separates the Virtual DOM and the Real DOM. Additionally, these edits are serializable, meaning we can even manage UIs across a network connection. This little stack machine and serialized edits make Dioxus independent of platform specifics.
|
||||||
|
|
||||||
Dioxus is also really fast. Because Dioxus splits the diff and patch phase, it's able to make all the edits to the RealDOM in a very short amount of time (less than a single frame) making rendering very snappy. It also allows Dioxus to cancel large diffing operations if higher priority work comes in while it's diffing.
|
Dioxus is also really fast. Because Dioxus splits the diff and patch phase, it's able to make all the edits to the RealDOM in a very short amount of time (less than a single frame) making rendering very snappy. It also allows Dioxus to cancel large diffing operations if higher priority work comes in while it's diffing.
|
||||||
|
|
||||||
It's important to note that there _is_ one layer of connectedness between Dioxus and the renderer. Dioxus saves and loads elements (the PushRoot edit) with an ID. Inside the VirtualDOM, this is just tracked as a u64.
|
This little demo serves to show exactly how a Renderer would need to process an edit stream to build UIs.
|
||||||
|
|
||||||
Whenever a `CreateElement` edit is generated during diffing, Dioxus increments its node counter and assigns that new element its current NodeCount. The RealDom is responsible for remembering this ID and pushing the correct node when PushRoot(ID) is generated. Dioxus reclaims the IDs of elements when removed. To stay in sync with Dioxus you can use a sparse Vec (Vec<Option<T>>) with possibly unoccupied items. You can use the ids as indexes into the Vec for elements, and grow the Vec when an id does not exist.
|
|
||||||
|
|
||||||
This little demo serves to show exactly how a Renderer would need to process an edit stream to build UIs. A set of serialized DomEditss for various demos is available for you to test your custom renderer against.
|
|
||||||
|
|
||||||
## Event loop
|
## Event loop
|
||||||
|
|
||||||
|
@ -223,35 +288,17 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent {
|
||||||
|
|
||||||
## Custom raw elements
|
## Custom raw elements
|
||||||
|
|
||||||
If you need to go as far as relying on custom elements for your renderer – you totally can. This still enables you to use Dioxus' reactive nature, component system, shared state, and other features, but will ultimately generate different nodes. All attributes and listeners for the HTML and SVG namespace are shuttled through helper structs that essentially compile away (pose no runtime overhead). You can drop in your own elements any time you want, with little hassle. However, you must be absolutely sure your renderer can handle the new type, or it will crash and burn.
|
If you need to go as far as relying on custom elements/attributes for your renderer – you totally can. This still enables you to use Dioxus' reactive nature, component system, shared state, and other features, but will ultimately generate different nodes. All attributes and listeners for the HTML and SVG namespace are shuttled through helper structs that essentially compile away. You can drop in your elements any time you want, with little hassle. However, you must be sure your renderer can handle the new namespace.
|
||||||
|
|
||||||
These custom elements are defined as unit structs with trait implementations.
|
For more examples and information on how to create custom namespaces, see the [`dioxus_html` crate](https://github.com/DioxusLabs/dioxus/blob/master/packages/html/README.md#how-to-extend-it).
|
||||||
|
|
||||||
For example, the `div` element is (approximately!) defined as such:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
struct div;
|
|
||||||
impl div {
|
|
||||||
/// Some glorious documentation about the class property.
|
|
||||||
const TAG_NAME: &'static str = "div";
|
|
||||||
const NAME_SPACE: Option<&'static str> = None;
|
|
||||||
// define the class attribute
|
|
||||||
pub fn class<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
|
|
||||||
cx.attr("class", val, None, false)
|
|
||||||
}
|
|
||||||
// more attributes
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You've probably noticed that many elements in the `rsx!` macros support on-hover documentation. The approach we take to custom elements means that the unit struct is created immediately where the element is used in the macro. When the macro is expanded, the doc comments still apply to the unit struct, giving tons of in-editor feedback, even inside a proc macro.
|
|
||||||
|
|
||||||
# Native Core
|
# Native Core
|
||||||
|
|
||||||
If you are creating a renderer in rust, native-core provides some utilities to implement a renderer. It provides an abstraction over DomEdits and handles the layout for you.
|
If you are creating a renderer in rust, the [native-core](https://github.com/DioxusLabs/dioxus/tree/master/packages/native-core) crate provides some utilities to implement a renderer. It provides an abstraction over Mutations and Templates and contains helpers that can handle the layout, and text editing for you.
|
||||||
|
|
||||||
## RealDom
|
## The RealDom
|
||||||
|
|
||||||
The `RealDom` is a higher-level abstraction over updating the Dom. It updates with `DomEdits` and provides a way to incrementally update the state of nodes based on what attributes change.
|
The `RealDom` is a higher-level abstraction over updating the Dom. It updates with `Mutations` and provides a way to incrementally update the state of nodes based on attributes or other states that change.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
|
@ -269,11 +316,11 @@ cx.render(rsx!{
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
In this tree, the color depends on the parent's color. The size depends on the children's size, the current text, and the text size. The border depends on only the current node.
|
In this tree, the color depends on the parent's color. The layout depends on the children's layout, the current text, and the text size. The border depends on only the current node.
|
||||||
|
|
||||||
In the following diagram arrows represent dataflow:
|
In the following diagram arrows represent dataflow:
|
||||||
|
|
||||||
[![](https://mermaid.ink/img/pako:eNqdVNFqgzAU_RXJXizUUZPJmIM-jO0LukdhpCbO0JhIGteW0n9fNK1Oa0brfUnu9VxyzzkXjyCVhIIYZFzu0hwr7X2-JcIzsa3W3wqXuZdKoele22oddfa1Y0Tnfn31muvMfqeCDNq3GmvaNROmaKqZFO1DPTRhP8MOd1fTWYNDvzlmQbBMJZcq9JtjNgY1mLVUhBqQPQeojl3wGCw5PsjqnIe-zXqEL8GZ2Kz0gVMPmoeU3ND4IcuiaLGY2zRouuKncv_qGKv3VodpJe0JVU6QCQ5kgqMyWQVr8hbk4hm1PBcmsuwmnrCVH94rP7xN_ucp8sOB_EPSfz9drYVrkpc_AmH8_yTjJueUc-ntpOJkgt2os9tKjcYlt-DLUiD3UsB2KZCLcwjv3Aq33-g2v0M0xXA0MBy5DUdXi-gcJZriuLmAOSioKjAj5ld8rMsJ0DktaAJicyVYbRKQiJPBVSUx438QpqUCcYb5ls4BrrRcHUTaFizqnWGzR8W5evoFI-bJdw)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqdVNFqgzAU_RXJXizUUZPJmIM-jO0LukdhpCbO0JhIGteW0n9fNK1Oa0brfUnu9VxyzzkXjyCVhIIYZFzu0hwr7X2-JcIzsa3W3wqXuZdKoele22oddfa1Y0Tnfn31muvMfqeCDNq3GmvaNROmaKqZFO1DPTRhP8MOd1fTWYNDvzlmQbBMJZcq9JtjNgY1mLVUhBqQPQeojl3wGCw5PsjqnIe-zXqEL8GZ2Kz0gVMPmoeU3ND4IcuiaLGY2zRouuKncv_qGKv3VodpJe0JVU6QCQ5kgqMyWQVr8hbk4hm1PBcmsuwmnrCVH94rP7xN_ucp8sOB_EPSfz9drYVrkpc_AmH8_yTjJueUc-ntpOJkgt2os9tKjcYlt-DLUiD3UsB2KZCLcwjv3Aq33-g2v0M0xXA0MBy5DUdXi-gcJZriuLmAOSioKjAj5ld8rMsJ0DktaAJicyVYbRKQiJPBVSUx438QpqUCcYb5ls4BrrRcHUTaFizqnWGzR8W5evoFI-bJdw)
|
[![](https://mermaid.ink/img/pako:eNqllV1vgjAUhv8K6W4wkQVa2QdLdrHsdlfukmSptEhjoaSWqTH-9xVwONAKst70g5739JzzlO5BJAgFAYi52EQJlsr6fAszS7d1sVhKnCdWJDJFt6peLVs5-9owohK7HFrVcFJ_pxnpmK8VVvRkTJikkWIiaxy1dhP23bUwW1WW5WbPrrqJ4ziR4EJ6dtVN2ls5y1ZztePUcrWZFCvqVEcPPDffvlyS1XoLIQnVgnVvVPR6FU9Zc-6dV453ojjOPbuetRJ57gIeXQR3cez7rjtteZyZQ2j5MqmjqwE0ZW0VKx9RKtgpFewp1aw3sXXFy6TWgiYlv8mfq1scD8ofbBCAfQg8_AMBOAyBxzEIwA4CxgQ99QbQkjnD2KT7_CfxGF8_9WXQEsq5sDZCcjICOXRCri4h6r3NA38Q6Jdi1EOx5w3DGDYYI6MUvJFjM3VoGHUeGoMd6mBnDmh2E3fo7O4Yhf0x4OkBmIKUyhQzol_GfbkcApXQlIYg0EOC5SoEYXbQ-3ChxHyXRSBQsqBTUOREx_7OsAY3BUGM-VqvUsKUkB_1U6vf05gtweEHTk4_HQ?type=png)](https://mermaid.live/edit#pako:eNqllV1vgjAUhv8K6W4wkQVa2QdLdrHsdlfukmSptEhjoaSWqTH-9xVwONAKst70g5739JzzlO5BJAgFAYi52EQJlsr6fAszS7d1sVhKnCdWJDJFt6peLVs5-9owohK7HFrVcFJ_pxnpmK8VVvRkTJikkWIiaxy1dhP23bUwW1WW5WbPrrqJ4ziR4EJ6dtVN2ls5y1ZztePUcrWZFCvqVEcPPDffvlyS1XoLIQnVgnVvVPR6FU9Zc-6dV453ojjOPbuetRJ57gIeXQR3cez7rjtteZyZQ2j5MqmjqwE0ZW0VKx9RKtgpFewp1aw3sXXFy6TWgiYlv8mfq1scD8ofbBCAfQg8_AMBOAyBxzEIwA4CxgQ99QbQkjnD2KT7_CfxGF8_9WXQEsq5sDZCcjICOXRCri4h6r3NA38Q6Jdi1EOx5w3DGDYYI6MUvJFjM3VoGHUeGoMd6mBnDmh2E3fo7O4Yhf0x4OkBmIKUyhQzol_GfbkcApXQlIYg0EOC5SoEYXbQ-3ChxHyXRSBQsqBTUOREx_7OsAY3BUGM-VqvUsKUkB_1U6vf05gtweEHTk4_HQ)
|
||||||
|
|
||||||
[//]: # "%% mermaid flow chart"
|
[//]: # "%% mermaid flow chart"
|
||||||
[//]: # "flowchart TB"
|
[//]: # "flowchart TB"
|
||||||
|
@ -284,33 +331,42 @@ In the following diagram arrows represent dataflow:
|
||||||
[//]: # " direction TB"
|
[//]: # " direction TB"
|
||||||
[//]: # " subgraph div state"
|
[//]: # " subgraph div state"
|
||||||
[//]: # " direction TB"
|
[//]: # " direction TB"
|
||||||
[//]: # " state1(state)-->color1(color)"
|
[//]: # " state1(state)---color1(color)"
|
||||||
[//]: # " state1-->border1(border)"
|
[//]: # " linkStyle 0 stroke-width:10px;"
|
||||||
|
[//]: # " state1---border1(border)"
|
||||||
|
[//]: # " linkStyle 1 stroke-width:10px;"
|
||||||
[//]: # " text_width-.->layout_width1(layout width)"
|
[//]: # " text_width-.->layout_width1(layout width)"
|
||||||
[//]: # " linkStyle 2 stroke:#ff5500,stroke-width:4px;"
|
[//]: # " linkStyle 2 stroke:#ff5500,stroke-width:4px;"
|
||||||
[//]: # " state1-->layout_width1"
|
[//]: # " state1---layout_width1"
|
||||||
|
[//]: # " linkStyle 3 stroke-width:10px;"
|
||||||
[//]: # " end"
|
[//]: # " end"
|
||||||
[//]: # " subgraph p state"
|
[//]: # " subgraph p state"
|
||||||
[//]: # " direction TB"
|
[//]: # " direction TB"
|
||||||
[//]: # " state2(state)-->color2(color)"
|
[//]: # " state2(state)---color2(color)"
|
||||||
|
[//]: # " linkStyle 4 stroke-width:10px;"
|
||||||
[//]: # " color1-.->color2"
|
[//]: # " color1-.->color2"
|
||||||
[//]: # " linkStyle 5 stroke:#0000ff,stroke-width:4px;"
|
[//]: # " linkStyle 5 stroke:#0000ff,stroke-width:4px;"
|
||||||
[//]: # " state2-->border2(border)"
|
[//]: # " state2---border2(border)"
|
||||||
|
[//]: # " linkStyle 6 stroke-width:10px;"
|
||||||
[//]: # " text_width-.->layout_width2(layout width)"
|
[//]: # " text_width-.->layout_width2(layout width)"
|
||||||
[//]: # " linkStyle 7 stroke:#ff5500,stroke-width:4px;"
|
[//]: # " linkStyle 7 stroke:#ff5500,stroke-width:4px;"
|
||||||
[//]: # " state2-->layout_width2"
|
[//]: # " state2---layout_width2"
|
||||||
|
[//]: # " linkStyle 8 stroke-width:10px;"
|
||||||
[//]: # " layout_width2-.->layout_width1"
|
[//]: # " layout_width2-.->layout_width1"
|
||||||
[//]: # " linkStyle 9 stroke:#00aa00,stroke-width:4px;"
|
[//]: # " linkStyle 9 stroke:#00aa00,stroke-width:4px;"
|
||||||
[//]: # " end"
|
[//]: # " end"
|
||||||
[//]: # " subgraph hello world state"
|
[//]: # " subgraph hello world state"
|
||||||
[//]: # " direction TB"
|
[//]: # " direction TB"
|
||||||
[//]: # " state3(state)-->border3(border)"
|
[//]: # " state3(state)---border3(border)"
|
||||||
[//]: # " state3-->color3(color)"
|
[//]: # " linkStyle 10 stroke-width:10px;"
|
||||||
|
[//]: # " state3---color3(color)"
|
||||||
|
[//]: # " linkStyle 11 stroke-width:10px;"
|
||||||
[//]: # " color2-.->color3"
|
[//]: # " color2-.->color3"
|
||||||
[//]: # " linkStyle 12 stroke:#0000ff,stroke-width:4px;"
|
[//]: # " linkStyle 12 stroke:#0000ff,stroke-width:4px;"
|
||||||
[//]: # " text_width-.->layout_width3(layout width)"
|
[//]: # " text_width-.->layout_width3(layout width)"
|
||||||
[//]: # " linkStyle 13 stroke:#ff5500,stroke-width:4px;"
|
[//]: # " linkStyle 13 stroke:#ff5500,stroke-width:4px;"
|
||||||
[//]: # " state3-->layout_width3"
|
[//]: # " state3---layout_width3"
|
||||||
|
[//]: # " linkStyle 14 stroke-width:10px;"
|
||||||
[//]: # " layout_width3-.->layout_width2"
|
[//]: # " layout_width3-.->layout_width2"
|
||||||
[//]: # " linkStyle 15 stroke:#00aa00,stroke-width:4px;"
|
[//]: # " linkStyle 15 stroke:#00aa00,stroke-width:4px;"
|
||||||
[//]: # " end"
|
[//]: # " end"
|
||||||
|
@ -319,25 +375,29 @@ In the following diagram arrows represent dataflow:
|
||||||
To help in building a Dom, native-core provides four traits: State, ChildDepState, ParentDepState, NodeDepState, and a RealDom struct. The ChildDepState, ParentDepState, and NodeDepState provide a way to describe how some information in a node relates to that of its relatives. By providing how to build a single node from its relations, native-core will derive a way to update the state of all nodes for you with ```#[derive(State)]```. Once you have a state you can provide it as a generic to RealDom. RealDom provides all of the methods to interact and update your new dom.
|
To help in building a Dom, native-core provides four traits: State, ChildDepState, ParentDepState, NodeDepState, and a RealDom struct. The ChildDepState, ParentDepState, and NodeDepState provide a way to describe how some information in a node relates to that of its relatives. By providing how to build a single node from its relations, native-core will derive a way to update the state of all nodes for you with ```#[derive(State)]```. Once you have a state you can provide it as a generic to RealDom. RealDom provides all of the methods to interact and update your new dom.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
||||||
use dioxus_native_core::node_ref::*;
|
use dioxus_native_core::node_ref::*;
|
||||||
use dioxus_native_core::state::{ChildDepState, NodeDepState, ParentDepState, State};
|
use dioxus_native_core::state::{ChildDepState, NodeDepState, ParentDepState, State};
|
||||||
use dioxus_native_core_macro::{sorted_str_slice, State};
|
use dioxus_native_core_macro::{sorted_str_slice, State};
|
||||||
|
|
||||||
#[derive(Default, Copy, Clone)]
|
#[derive(Default, Copy, Clone)]
|
||||||
struct Size(f32, f32);
|
struct Size(f64, f64);
|
||||||
// Size only depends on the current node and its children, so it implements ChildDepState
|
// Size only depends on the current node and its children, so it implements ChildDepState
|
||||||
impl ChildDepState for Size {
|
impl ChildDepState for Size {
|
||||||
// Size accepts a font size context
|
// Size accepts a font size context
|
||||||
type Ctx = f32;
|
type Ctx = f64;
|
||||||
// Size depends on the Size part of each child
|
// Size depends on the Size part of each child
|
||||||
type DepState = Self;
|
type DepState = (Self,);
|
||||||
// Size only cares about the width, height, and text parts of the current node
|
// Size only cares about the width, height, and text parts of the current node
|
||||||
const NODE_MASK: NodeMask =
|
const NODE_MASK: NodeMask =
|
||||||
NodeMask::new_with_attrs(AttributeMask::Static(&sorted_str_slice!(["width", "height"]))).with_text();
|
NodeMask::new_with_attrs(AttributeMask::Static(&sorted_str_slice!([
|
||||||
|
"width", "height"
|
||||||
|
])))
|
||||||
|
.with_text();
|
||||||
fn reduce<'a>(
|
fn reduce<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
node: NodeView,
|
node: NodeView,
|
||||||
children: impl Iterator<Item = &'a Self::DepState>,
|
children: impl Iterator<Item = (&'a Self,)>,
|
||||||
ctx: &Self::Ctx,
|
ctx: &Self::Ctx,
|
||||||
) -> bool
|
) -> bool
|
||||||
where
|
where
|
||||||
|
@ -347,28 +407,28 @@ impl ChildDepState for Size {
|
||||||
let mut height;
|
let mut height;
|
||||||
if let Some(text) = node.text() {
|
if let Some(text) = node.text() {
|
||||||
// if the node has text, use the text to size our object
|
// if the node has text, use the text to size our object
|
||||||
width = text.len() as f32 * ctx;
|
width = text.len() as f64 * ctx;
|
||||||
height = *ctx;
|
height = *ctx;
|
||||||
} else {
|
} else {
|
||||||
// otherwise, the size is the maximum size of the children
|
// otherwise, the size is the maximum size of the children
|
||||||
width = children
|
width = children
|
||||||
.by_ref()
|
.by_ref()
|
||||||
.map(|item| item.0)
|
.map(|(item,)| item.0)
|
||||||
.reduce(|accum, item| if accum >= item { accum } else { item })
|
.reduce(|accum, item| if accum >= item { accum } else { item })
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
height = children
|
height = children
|
||||||
.map(|item| item.1)
|
.map(|(item,)| item.1)
|
||||||
.reduce(|accum, item| if accum >= item { accum } else { item })
|
.reduce(|accum, item| if accum >= item { accum } else { item })
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
}
|
}
|
||||||
// if the node contains a width or height attribute it overrides the other size
|
// if the node contains a width or height attribute it overrides the other size
|
||||||
for a in node.attributes(){
|
for a in node.attributes().into_iter().flatten() {
|
||||||
match a.name{
|
match &*a.attribute.name {
|
||||||
"width" => width = a.value.as_float32().unwrap(),
|
"width" => width = a.value.as_float().unwrap(),
|
||||||
"height" => height = a.value.as_float32().unwrap(),
|
"height" => height = a.value.as_float().unwrap(),
|
||||||
// because Size only depends on the width and height, no other attributes will be passed to the member
|
// because Size only depends on the width and height, no other attributes will be passed to the member
|
||||||
_ => panic!()
|
_ => panic!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
|
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
|
||||||
|
@ -388,17 +448,16 @@ struct TextColor {
|
||||||
impl ParentDepState for TextColor {
|
impl ParentDepState for TextColor {
|
||||||
type Ctx = ();
|
type Ctx = ();
|
||||||
// TextColor depends on the TextColor part of the parent
|
// TextColor depends on the TextColor part of the parent
|
||||||
type DepState = Self;
|
type DepState = (Self,);
|
||||||
// TextColor only cares about the color attribute of the current node
|
// TextColor only cares about the color attribute of the current node
|
||||||
const NODE_MASK: NodeMask = NodeMask::new_with_attrs(AttributeMask::Static(&["color"]));
|
const NODE_MASK: NodeMask = NodeMask::new_with_attrs(AttributeMask::Static(&["color"]));
|
||||||
fn reduce(
|
fn reduce(&mut self, node: NodeView, parent: Option<(&Self,)>, _ctx: &Self::Ctx) -> bool {
|
||||||
&mut self,
|
|
||||||
node: NodeView,
|
|
||||||
parent: Option<&Self::DepState>,
|
|
||||||
_ctx: &Self::Ctx,
|
|
||||||
) -> bool {
|
|
||||||
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
|
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
|
||||||
let new = match node.attributes().next().map(|attr| attr.name) {
|
let new = match node
|
||||||
|
.attributes()
|
||||||
|
.and_then(|attrs| attrs.next())
|
||||||
|
.map(|attr| attr.attribute.name.as_str())
|
||||||
|
{
|
||||||
// if there is a color tag, translate it
|
// if there is a color tag, translate it
|
||||||
Some("red") => TextColor { r: 255, g: 0, b: 0 },
|
Some("red") => TextColor { r: 255, g: 0, b: 0 },
|
||||||
Some("green") => TextColor { r: 0, g: 255, b: 0 },
|
Some("green") => TextColor { r: 0, g: 255, b: 0 },
|
||||||
|
@ -406,7 +465,7 @@ impl ParentDepState for TextColor {
|
||||||
Some(_) => panic!("unknown color"),
|
Some(_) => panic!("unknown color"),
|
||||||
// otherwise check if the node has a parent and inherit that color
|
// otherwise check if the node has a parent and inherit that color
|
||||||
None => match parent {
|
None => match parent {
|
||||||
Some(parent) => *parent,
|
Some((parent,)) => *parent,
|
||||||
None => Self::default(),
|
None => Self::default(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -420,15 +479,19 @@ impl ParentDepState for TextColor {
|
||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
struct Border(bool);
|
struct Border(bool);
|
||||||
// TextColor only depends on the current node, so it implements NodeDepState
|
// TextColor only depends on the current node, so it implements NodeDepState
|
||||||
impl NodeDepState<()> for Border {
|
impl NodeDepState for Border {
|
||||||
type Ctx = ();
|
type Ctx = ();
|
||||||
|
type DepState = ();
|
||||||
|
|
||||||
// Border does not depended on any other member in the current node
|
// Border does not depended on any other member in the current node
|
||||||
const NODE_MASK: NodeMask =
|
const NODE_MASK: NodeMask = NodeMask::new_with_attrs(AttributeMask::Static(&["border"]));
|
||||||
NodeMask::new_with_attrs(AttributeMask::Static(&["border"]));
|
|
||||||
fn reduce(&mut self, node: NodeView, _sibling: (), _ctx: &Self::Ctx) -> bool {
|
fn reduce(&mut self, node: NodeView, _sibling: (), _ctx: &Self::Ctx) -> bool {
|
||||||
// check if the node contians a border attribute
|
// check if the node contians a border attribute
|
||||||
let new = Self(node.attributes().next().map(|a| a.name == "border").is_some());
|
let new = Self(
|
||||||
|
node.attributes()
|
||||||
|
.and_then(|attrs| attrs.next().map(|a| a.attribute.name == "border"))
|
||||||
|
.is_some(),
|
||||||
|
);
|
||||||
// check if the member has changed
|
// check if the member has changed
|
||||||
let changed = new != *self;
|
let changed = new != *self;
|
||||||
*self = new;
|
*self = new;
|
||||||
|
@ -451,7 +514,7 @@ struct ToyState {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Now that we have our state, we can put it to use in our dom. Re can update the dom with update_state to update the structure of the dom (adding, removing, and changing properties of nodes) and then apply_mutations to update the ToyState for each of the nodes that changed.
|
Now that we have our state, we can put it to use in our dom. We can update the dom with update_state to update the structure of the dom (adding, removing, and changing properties of nodes) and then apply_mutations to update the ToyState for each of the nodes that changed.
|
||||||
```rust
|
```rust
|
||||||
fn main(){
|
fn main(){
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
|
@ -470,7 +533,7 @@ fn main(){
|
||||||
let to_update = rdom.apply_mutations(vec![mutations]);
|
let to_update = rdom.apply_mutations(vec![mutations]);
|
||||||
let mut ctx = AnyMap::new();
|
let mut ctx = AnyMap::new();
|
||||||
// set the font size to 3.3
|
// set the font size to 3.3
|
||||||
ctx.insert(3.3f32);
|
ctx.insert(3.3f64);
|
||||||
// update the ToyState for nodes in the real_dom tree
|
// update the ToyState for nodes in the real_dom tree
|
||||||
let _to_rerender = rdom.update_state(&dom, to_update, ctx).unwrap();
|
let _to_rerender = rdom.update_state(&dom, to_update, ctx).unwrap();
|
||||||
|
|
||||||
|
@ -484,7 +547,7 @@ fn main(){
|
||||||
let mutations = vdom.work_with_deadline(|| false);
|
let mutations = vdom.work_with_deadline(|| false);
|
||||||
let to_update = rdom.apply_mutations(mutations);
|
let to_update = rdom.apply_mutations(mutations);
|
||||||
let mut ctx = AnyMap::new();
|
let mut ctx = AnyMap::new();
|
||||||
ctx.insert(3.3);
|
ctx.insert(3.3f64);
|
||||||
let _to_rerender = rdom.update_state(vdom, to_update, ctx).unwrap();
|
let _to_rerender = rdom.update_state(vdom, to_update, ctx).unwrap();
|
||||||
|
|
||||||
// render...
|
// render...
|
||||||
|
@ -494,7 +557,34 @@ fn main(){
|
||||||
```
|
```
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
For most platforms, the layout of the Elements will stay the same. The layout_attributes module provides a way to apply HTML attributes to a stretch layout style.
|
|
||||||
|
For most platforms, the layout of the Elements will stay the same. The [layout_attributes](https://docs.rs/dioxus-native-core/latest/dioxus_native_core/layout_attributes/index.html) module provides a way to apply HTML attributes a [Taffy](https://docs.rs/taffy/latest/taffy/index.html) layout style.
|
||||||
|
|
||||||
|
## Text Editing
|
||||||
|
|
||||||
|
To make it easier to implement text editing in rust renderers, `native-core` also contains a renderer-agnostic cursor system. The cursor can handle text editing, selection, and movement with common keyboard shortcuts integrated.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut cursor = Cursor::default();
|
||||||
|
let mut text = String::new();
|
||||||
|
|
||||||
|
let keyboard_data = dioxus_html::KeyboardData::new(
|
||||||
|
dioxus_html::input_data::keyboard_types::Key::ArrowRight,
|
||||||
|
dioxus_html::input_data::keyboard_types::Code::ArrowRight,
|
||||||
|
dioxus_html::input_data::keyboard_types::Location::Standard,
|
||||||
|
false,
|
||||||
|
Modifiers::empty(),
|
||||||
|
);
|
||||||
|
// handle keyboard input with a max text length of 10
|
||||||
|
cursor.handle_input(&keyboard_data, &mut text, 10);
|
||||||
|
|
||||||
|
// mannually select text between characters 0-5 on the first line (this could be from dragging with a mouse)
|
||||||
|
cursor.start = Pos::new(0, 0);
|
||||||
|
cursor.end = Some(Pos::new(5, 0));
|
||||||
|
|
||||||
|
// delete the selected text and move the cursor to the start of the selection
|
||||||
|
cursor.delete_selection(&mut text);
|
||||||
|
```
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
That should be it! You should have nearly all the knowledge required on how to implement your own renderer. We're super interested in seeing Dioxus apps brought to custom desktop renderers, mobile renderers, video game UI, and even augmented reality! If you're interested in contributing to any of these projects, don't be afraid to reach out or join the [community](https://discord.gg/XgGxMSkvUM).
|
That should be it! You should have nearly all the knowledge required on how to implement your renderer. We're super interested in seeing Dioxus apps brought to custom desktop renderers, mobile renderers, video game UI, and even augmented reality! If you're interested in contributing to any of these projects, don't be afraid to reach out or join the [community](https://discord.gg/XgGxMSkvUM).
|
||||||
|
|
|
@ -2,21 +2,48 @@
|
||||||
|
|
||||||
1. Hot reloading allows much faster iteration times inside of rsx calls by interpreting them and streaming the edits.
|
1. Hot reloading allows much faster iteration times inside of rsx calls by interpreting them and streaming the edits.
|
||||||
2. It is useful when changing the styling/layout of a program, but will not help with changing the logic of a program.
|
2. It is useful when changing the styling/layout of a program, but will not help with changing the logic of a program.
|
||||||
3. Currently the cli only implements hot reloading for the web renderer.
|
3. Currently the cli only implements hot reloading for the web renderer. For TUI, desktop, and LiveView you can use the hot reload macro instead.
|
||||||
|
|
||||||
# Setup
|
# Web
|
||||||
|
For the web renderer, you can use the dioxus cli to serve your application with hot reloading enabled.
|
||||||
|
|
||||||
|
## Setup
|
||||||
Install [dioxus-cli](https://github.com/DioxusLabs/cli).
|
Install [dioxus-cli](https://github.com/DioxusLabs/cli).
|
||||||
Hot reloading is automatically enabled when using the web renderer on debug builds.
|
Hot reloading is automatically enabled when using the web renderer on debug builds.
|
||||||
|
|
||||||
# Usage
|
## Usage
|
||||||
1. run:
|
1. Run:
|
||||||
```
|
```bash
|
||||||
dioxus serve --hot-reload
|
dioxus serve --hot-reload
|
||||||
```
|
```
|
||||||
2. change some code within a rsx macro
|
2. Change some code within a rsx or render macro
|
||||||
3. open your localhost in a browser
|
3. Open your localhost in a browser
|
||||||
4. save and watch the style change without recompiling
|
4. Save and watch the style change without recompiling
|
||||||
|
|
||||||
|
# Desktop/Liveview/TUI
|
||||||
|
For desktop, LiveView, and tui, you can place the hot reload macro at the top of your main function to enable hot reloading.
|
||||||
|
Hot reloading is automatically enabled on debug builds.
|
||||||
|
|
||||||
|
For more information about hot reloading on native platforms and configuration options see the [dioxus-hot-reload](https://crates.io/crates/dioxus-hot-reload) crate.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Add the following to your main function:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
hot_reload_init!();
|
||||||
|
// launch your application
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
1. Run:
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
2. Change some code within a rsx or render macro
|
||||||
|
3. Save and watch the style change without recompiling
|
||||||
|
|
||||||
# Limitations
|
# Limitations
|
||||||
1. The interpreter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will trigger a full recompile to capture the expression.
|
1. The interpreter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will require a full recompile to capture the expression.
|
||||||
2. Components and Iterators can contain arbitrary rust code and will trigger a full recompile when changed.
|
2. Components, Iterators, and some attributes can contain arbitrary rust code and will trigger a full recompile when changed.
|
||||||
|
|
|
@ -82,7 +82,7 @@ fn app(cx: Scope) -> Element {
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let temp = calc_val(val.as_str());
|
let temp = calc_val(val.as_str());
|
||||||
if temp > 0.0 {
|
if temp > 0.0 {
|
||||||
val.set(format!("-{}", temp));
|
val.set(format!("-{temp}"));
|
||||||
} else {
|
} else {
|
||||||
val.set(format!("{}", temp.abs()));
|
val.set(format!("{}", temp.abs()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ fn app(cx: Scope) -> Element {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
println!("{:#?}, ", res);
|
println!("{res:#?}, ");
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
|
|
@ -18,7 +18,7 @@ fn app(cx: Scope) -> Element {
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
count += 1;
|
count += 1;
|
||||||
println!("current: {}", count);
|
println!("current: {count}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ struct DogApi {
|
||||||
#[inline_props]
|
#[inline_props]
|
||||||
async fn breed_pic(cx: Scope, breed: String) -> Element {
|
async fn breed_pic(cx: Scope, breed: String) -> Element {
|
||||||
let fut = use_future!(cx, |breed| async move {
|
let fut = use_future!(cx, |breed| async move {
|
||||||
reqwest::get(format!("https://dog.ceo/api/breed/{}/images/random", breed))
|
reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.json::<DogApi>()
|
.json::<DogApi>()
|
||||||
|
|
|
@ -89,7 +89,7 @@ impl Files {
|
||||||
let paths = match std::fs::read_dir(cur_path) {
|
let paths = match std::fs::read_dir(cur_path) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let err = format!("An error occured: {:?}", err);
|
let err = format!("An error occured: {err:?}");
|
||||||
self.err = Some(err);
|
self.err = Some(err);
|
||||||
self.path_stack.pop();
|
self.path_stack.pop();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -3,7 +3,7 @@ use dioxus_desktop::Config;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cfg = Config::new().with_file_drop_handler(|_w, e| {
|
let cfg = Config::new().with_file_drop_handler(|_w, e| {
|
||||||
println!("{:?}", e);
|
println!("{e:?}");
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ fn app(cx: Scope) -> Element {
|
||||||
// so the value of our input event will be either huey, dewey, louie, or true/false (because of the checkboxe)
|
// so the value of our input event will be either huey, dewey, louie, or true/false (because of the checkboxe)
|
||||||
// be mindful in grouping inputs together, as they will all be handled by the same event handler
|
// be mindful in grouping inputs together, as they will all be handled by the same event handler
|
||||||
oninput: move |evt| {
|
oninput: move |evt| {
|
||||||
println!("{:?}", evt);
|
println!("{evt:?}");
|
||||||
},
|
},
|
||||||
div {
|
div {
|
||||||
input {
|
input {
|
||||||
|
@ -104,7 +104,7 @@ fn app(cx: Scope) -> Element {
|
||||||
name: "pdf",
|
name: "pdf",
|
||||||
r#type: "checkbox",
|
r#type: "checkbox",
|
||||||
oninput: move |evt| {
|
oninput: move |evt| {
|
||||||
println!("{:?}", evt);
|
println!("{evt:?}");
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
|
@ -121,7 +121,7 @@ fn app(cx: Scope) -> Element {
|
||||||
r#type: "{field}",
|
r#type: "{field}",
|
||||||
value: "{value}",
|
value: "{value}",
|
||||||
oninput: move |evt: FormEvent| {
|
oninput: move |evt: FormEvent| {
|
||||||
println!("{:?}", evt);
|
println!("{evt:?}");
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
|
|
|
@ -83,7 +83,7 @@ fn app(cx: Scope) -> Element {
|
||||||
div {
|
div {
|
||||||
class: {
|
class: {
|
||||||
const WORD: &str = "expressions";
|
const WORD: &str = "expressions";
|
||||||
format_args!("Arguments can be passed in through curly braces for complex {}", WORD)
|
format_args!("Arguments can be passed in through curly braces for complex {WORD}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,7 +214,7 @@ fn app(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_dollars(dollars: u32, cents: u32) -> String {
|
fn format_dollars(dollars: u32, cents: u32) -> String {
|
||||||
format!("${}.{:02}", dollars, cents)
|
format!("${dollars}.{cents:02}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn helper<'a>(cx: &'a ScopeState, text: &str) -> Element<'a> {
|
fn helper<'a>(cx: &'a ScopeState, text: &str) -> Element<'a> {
|
||||||
|
|
|
@ -27,7 +27,7 @@ fn main() {
|
||||||
let mut file = String::new();
|
let mut file = String::new();
|
||||||
let mut renderer = dioxus_ssr::Renderer::default();
|
let mut renderer = dioxus_ssr::Renderer::default();
|
||||||
renderer.render_to(&mut file, &vdom).unwrap();
|
renderer.render_to(&mut file, &vdom).unwrap();
|
||||||
println!("{}", file);
|
println!("{file}");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
|
|
|
@ -9,7 +9,7 @@ fn main() {
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let model = use_state(cx, || String::from("asd"));
|
let model = use_state(cx, || String::from("asd"));
|
||||||
|
|
||||||
println!("{}", model);
|
println!("{model}");
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
textarea {
|
textarea {
|
||||||
|
|
|
@ -140,7 +140,7 @@ impl Writer<'_> {
|
||||||
let mut written = generics.to_token_stream().to_string();
|
let mut written = generics.to_token_stream().to_string();
|
||||||
written.retain(|c| !c.is_whitespace());
|
written.retain(|c| !c.is_whitespace());
|
||||||
|
|
||||||
write!(self.out, "{}", written)?;
|
write!(self.out, "{written}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(self.out, " {{")?;
|
write!(self.out, " {{")?;
|
||||||
|
@ -165,7 +165,7 @@ impl Writer<'_> {
|
||||||
match &field.content {
|
match &field.content {
|
||||||
ContentField::ManExpr(exp) => {
|
ContentField::ManExpr(exp) => {
|
||||||
let out = prettyplease::unparse_expr(exp);
|
let out = prettyplease::unparse_expr(exp);
|
||||||
write!(self.out, "{}: {}", name, out)?;
|
write!(self.out, "{name}: {out}")?;
|
||||||
}
|
}
|
||||||
ContentField::Formatted(s) => {
|
ContentField::Formatted(s) => {
|
||||||
write!(
|
write!(
|
||||||
|
@ -179,11 +179,11 @@ impl Writer<'_> {
|
||||||
let out = prettyplease::unparse_expr(exp);
|
let out = prettyplease::unparse_expr(exp);
|
||||||
let mut lines = out.split('\n').peekable();
|
let mut lines = out.split('\n').peekable();
|
||||||
let first = lines.next().unwrap();
|
let first = lines.next().unwrap();
|
||||||
write!(self.out, "{}: {}", name, first)?;
|
write!(self.out, "{name}: {first}")?;
|
||||||
for line in lines {
|
for line in lines {
|
||||||
self.out.new_line()?;
|
self.out.new_line()?;
|
||||||
self.out.indented_tab()?;
|
self.out.indented_tab()?;
|
||||||
write!(self.out, "{}", line)?;
|
write!(self.out, "{line}")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -224,7 +224,7 @@ impl Writer<'_> {
|
||||||
}
|
}
|
||||||
ElementAttr::AttrExpression { name, value } => {
|
ElementAttr::AttrExpression { name, value } => {
|
||||||
let out = prettyplease::unparse_expr(value);
|
let out = prettyplease::unparse_expr(value);
|
||||||
write!(self.out, "{}: {}", name, out)?;
|
write!(self.out, "{name}: {out}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ElementAttr::CustomAttrText { name, value } => {
|
ElementAttr::CustomAttrText { name, value } => {
|
||||||
|
@ -250,13 +250,13 @@ impl Writer<'_> {
|
||||||
// a one-liner for whatever reason
|
// a one-liner for whatever reason
|
||||||
// Does not need a new line
|
// Does not need a new line
|
||||||
if lines.peek().is_none() {
|
if lines.peek().is_none() {
|
||||||
write!(self.out, "{}: {}", name, first)?;
|
write!(self.out, "{name}: {first}")?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(self.out, "{}: {}", name, first)?;
|
writeln!(self.out, "{name}: {first}")?;
|
||||||
|
|
||||||
while let Some(line) = lines.next() {
|
while let Some(line) = lines.next() {
|
||||||
self.out.indented_tab()?;
|
self.out.indented_tab()?;
|
||||||
write!(self.out, "{}", line)?;
|
write!(self.out, "{line}")?;
|
||||||
if lines.peek().is_none() {
|
if lines.peek().is_none() {
|
||||||
write!(self.out, "")?;
|
write!(self.out, "")?;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -20,7 +20,7 @@ impl Writer<'_> {
|
||||||
let start = byte_offset(self.raw_src, start);
|
let start = byte_offset(self.raw_src, start);
|
||||||
let end = byte_offset(self.raw_src, end);
|
let end = byte_offset(self.raw_src, end);
|
||||||
let row = self.raw_src[start..end].trim();
|
let row = self.raw_src[start..end].trim();
|
||||||
write!(self.out, "{}", row)?;
|
write!(self.out, "{row}")?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,11 +56,11 @@ impl Writer<'_> {
|
||||||
write!(self.out, " ")?;
|
write!(self.out, " ")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(self.out, "{}", line)?;
|
write!(self.out, "{line}")?;
|
||||||
} else {
|
} else {
|
||||||
let offset = offset as usize;
|
let offset = offset as usize;
|
||||||
let right = &line[offset..];
|
let right = &line[offset..];
|
||||||
write!(self.out, "{}", right)?;
|
write!(self.out, "{right}")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||||
&& matches!(body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_));
|
&& matches!(body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_));
|
||||||
|
|
||||||
if formatted.len() <= 80 && !formatted.contains('\n') && !body_is_solo_expr {
|
if formatted.len() <= 80 && !formatted.contains('\n') && !body_is_solo_expr {
|
||||||
formatted = format!(" {} ", formatted);
|
formatted = format!(" {formatted} ");
|
||||||
}
|
}
|
||||||
|
|
||||||
end_span = span.end();
|
end_span = span.end();
|
||||||
|
|
|
@ -95,7 +95,7 @@ impl ToTokens for InlinePropsBody {
|
||||||
quote! { #vis #f }
|
quote! { #vis #f }
|
||||||
});
|
});
|
||||||
|
|
||||||
let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site());
|
let struct_name = Ident::new(&format!("{ident}Props"), Span::call_site());
|
||||||
|
|
||||||
let field_names = inputs.iter().filter_map(|f| match f {
|
let field_names = inputs.iter().filter_map(|f| match f {
|
||||||
FnArg::Receiver(_) => todo!(),
|
FnArg::Receiver(_) => todo!(),
|
||||||
|
|
|
@ -323,7 +323,7 @@ mod field_info {
|
||||||
let tokenized_code = TokenStream::from_str(&code.value())?;
|
let tokenized_code = TokenStream::from_str(&code.value())?;
|
||||||
self.default = Some(
|
self.default = Some(
|
||||||
syn::parse(tokenized_code.into())
|
syn::parse(tokenized_code.into())
|
||||||
.map_err(|e| Error::new_spanned(code, format!("{}", e)))?,
|
.map_err(|e| Error::new_spanned(code, format!("{e}")))?,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::new_spanned(assign.right, "Expected string"));
|
return Err(Error::new_spanned(assign.right, "Expected string"));
|
||||||
|
@ -332,7 +332,7 @@ mod field_info {
|
||||||
}
|
}
|
||||||
_ => Err(Error::new_spanned(
|
_ => Err(Error::new_spanned(
|
||||||
&assign,
|
&assign,
|
||||||
format!("Unknown parameter {:?}", name),
|
format!("Unknown parameter {name:?}"),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -503,11 +503,11 @@ mod struct_info {
|
||||||
builder_attr,
|
builder_attr,
|
||||||
builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()),
|
builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()),
|
||||||
conversion_helper_trait_name: syn::Ident::new(
|
conversion_helper_trait_name: syn::Ident::new(
|
||||||
&format!("{}_Optional", builder_name),
|
&format!("{builder_name}_Optional"),
|
||||||
proc_macro2::Span::call_site(),
|
proc_macro2::Span::call_site(),
|
||||||
),
|
),
|
||||||
core: syn::Ident::new(
|
core: syn::Ident::new(
|
||||||
&format!("{}_core", builder_name),
|
&format!("{builder_name}_core"),
|
||||||
proc_macro2::Span::call_site(),
|
proc_macro2::Span::call_site(),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
@ -594,7 +594,6 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
None => {
|
None => {
|
||||||
let doc = format!(
|
let doc = format!(
|
||||||
"Builder for [`{name}`] instances.\n\nSee [`{name}::builder()`] for more info.",
|
"Builder for [`{name}`] instances.\n\nSee [`{name}::builder()`] for more info.",
|
||||||
name = name
|
|
||||||
);
|
);
|
||||||
quote!(#[doc = #doc])
|
quote!(#[doc = #doc])
|
||||||
}
|
}
|
||||||
|
@ -709,9 +708,9 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
});
|
});
|
||||||
let reconstructing = self.included_fields().map(|f| f.name);
|
let reconstructing = self.included_fields().map(|f| f.name);
|
||||||
|
|
||||||
let &FieldInfo {
|
let FieldInfo {
|
||||||
name: ref field_name,
|
name: field_name,
|
||||||
ty: ref field_type,
|
ty: field_type,
|
||||||
..
|
..
|
||||||
} = field;
|
} = field;
|
||||||
let mut ty_generics: Vec<syn::GenericArgument> = self
|
let mut ty_generics: Vec<syn::GenericArgument> = self
|
||||||
|
@ -810,7 +809,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
),
|
),
|
||||||
proc_macro2::Span::call_site(),
|
proc_macro2::Span::call_site(),
|
||||||
);
|
);
|
||||||
let repeated_fields_error_message = format!("Repeated field {}", field_name);
|
let repeated_fields_error_message = format!("Repeated field {field_name}");
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
@ -929,7 +928,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
),
|
),
|
||||||
proc_macro2::Span::call_site(),
|
proc_macro2::Span::call_site(),
|
||||||
);
|
);
|
||||||
let early_build_error_message = format!("Missing required field {}", field_name);
|
let early_build_error_message = format!("Missing required field {field_name}");
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
@ -1037,7 +1036,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
// I’d prefer “a” or “an” to “its”, but determining which is grammatically
|
// I’d prefer “a” or “an” to “its”, but determining which is grammatically
|
||||||
// correct is roughly impossible.
|
// correct is roughly impossible.
|
||||||
let doc =
|
let doc =
|
||||||
format!("Finalise the builder and create its [`{}`] instance", name);
|
format!("Finalise the builder and create its [`{name}`] instance");
|
||||||
quote!(#[doc = #doc])
|
quote!(#[doc = #doc])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1132,7 +1131,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
}
|
}
|
||||||
_ => Err(Error::new_spanned(
|
_ => Err(Error::new_spanned(
|
||||||
&assign,
|
&assign,
|
||||||
format!("Unknown parameter {:?}", name),
|
format!("Unknown parameter {name:?}"),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1146,7 +1145,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
}
|
}
|
||||||
_ => Err(Error::new_spanned(
|
_ => Err(Error::new_spanned(
|
||||||
&path,
|
&path,
|
||||||
format!("Unknown parameter {:?}", name),
|
format!("Unknown parameter {name:?}"),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1161,7 +1160,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
let call_func = quote!(#call_func);
|
let call_func = quote!(#call_func);
|
||||||
Error::new_spanned(
|
Error::new_spanned(
|
||||||
&call.func,
|
&call.func,
|
||||||
format!("Illegal builder setting group {}", call_func),
|
format!("Illegal builder setting group {call_func}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
match subsetting_name.as_str() {
|
match subsetting_name.as_str() {
|
||||||
|
@ -1173,7 +1172,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
}
|
}
|
||||||
_ => Err(Error::new_spanned(
|
_ => Err(Error::new_spanned(
|
||||||
&call.func,
|
&call.func,
|
||||||
format!("Illegal builder setting group name {}", subsetting_name),
|
format!("Illegal builder setting group name {subsetting_name}"),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,11 @@ fn formatting_compiles() {
|
||||||
// escape sequences work
|
// escape sequences work
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_args_f!("{x:?} {{}}}}").to_string(),
|
format_args_f!("{x:?} {{}}}}").to_string(),
|
||||||
format!("{:?} {{}}}}", x)
|
format!("{x:?} {{}}}}")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_args_f!("{{{{}} {x:?}").to_string(),
|
format_args_f!("{{{{}} {x:?}").to_string(),
|
||||||
format!("{{{{}} {:?}", x)
|
format!("{{{{}} {x:?}")
|
||||||
);
|
);
|
||||||
|
|
||||||
// paths in formating works
|
// paths in formating works
|
||||||
|
@ -27,6 +27,6 @@ fn formatting_compiles() {
|
||||||
// allows duplicate format args
|
// allows duplicate format args
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_args_f!("{x:?} {x:?}").to_string(),
|
format_args_f!("{x:?} {x:?}").to_string(),
|
||||||
format!("{:?} {:?}", x, x)
|
format!("{x:?} {x:?}")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dioxus-core"
|
name = "dioxus-core"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
authors = ["Jonathan Kelley"]
|
authors = ["Jonathan Kelley"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||||
|
@ -23,7 +23,9 @@ rustc-hash = "1.1.0"
|
||||||
# Used in diffing
|
# Used in diffing
|
||||||
longest-increasing-subsequence = "0.1.0"
|
longest-increasing-subsequence = "0.1.0"
|
||||||
|
|
||||||
futures-util = { version = "0.3", default-features = false, features = ["alloc"]}
|
futures-util = { version = "0.3", default-features = false, features = [
|
||||||
|
"alloc",
|
||||||
|
] }
|
||||||
|
|
||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
|
|
||||||
|
@ -37,6 +39,8 @@ log = "0.4.17"
|
||||||
# Serialize the Edits for use in Webview/Liveview instances
|
# Serialize the Edits for use in Webview/Liveview instances
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
|
||||||
|
bumpslab = { version = "0.2.0" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
dioxus = { path = "../dioxus" }
|
dioxus = { path = "../dioxus" }
|
||||||
|
|
|
@ -92,21 +92,21 @@ impl VirtualDom {
|
||||||
// Note: This will not remove any ids from the arena
|
// Note: This will not remove any ids from the arena
|
||||||
pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
|
pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
|
||||||
self.dirty_scopes.remove(&DirtyScope {
|
self.dirty_scopes.remove(&DirtyScope {
|
||||||
height: self.scopes[id.0].height,
|
height: self.scopes[id].height,
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.ensure_drop_safety(id);
|
self.ensure_drop_safety(id);
|
||||||
|
|
||||||
if recursive {
|
if recursive {
|
||||||
if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
|
if let Some(root) = self.scopes[id].try_root_node() {
|
||||||
if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } {
|
if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } {
|
||||||
self.drop_scope_inner(node)
|
self.drop_scope_inner(node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let scope = &mut self.scopes[id.0];
|
let scope = &mut self.scopes[id];
|
||||||
|
|
||||||
// Drop all the hooks once the children are dropped
|
// Drop all the hooks once the children are dropped
|
||||||
// this means we'll drop hooks bottom-up
|
// this means we'll drop hooks bottom-up
|
||||||
|
@ -119,7 +119,7 @@ impl VirtualDom {
|
||||||
scope.tasks.remove(task_id);
|
scope.tasks.remove(task_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.scopes.remove(id.0);
|
self.scopes.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drop_scope_inner(&mut self, node: &VNode) {
|
fn drop_scope_inner(&mut self, node: &VNode) {
|
||||||
|
@ -140,7 +140,7 @@ impl VirtualDom {
|
||||||
|
|
||||||
/// Descend through the tree, removing any borrowed props and listeners
|
/// Descend through the tree, removing any borrowed props and listeners
|
||||||
pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
|
pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
|
||||||
let scope = &self.scopes[scope_id.0];
|
let scope = &self.scopes[scope_id];
|
||||||
|
|
||||||
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
|
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
|
||||||
// run the hooks (which hold an &mut Reference)
|
// run the hooks (which hold an &mut Reference)
|
||||||
|
|
|
@ -535,7 +535,7 @@ impl<'b> VirtualDom {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense
|
// If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense
|
||||||
let boundary = match self.scopes[scope.0].has_context::<Rc<SuspenseContext>>() {
|
let boundary = match self.scopes[scope].has_context::<Rc<SuspenseContext>>() {
|
||||||
Some(boundary) => boundary,
|
Some(boundary) => boundary,
|
||||||
_ => return created,
|
_ => return created,
|
||||||
};
|
};
|
||||||
|
@ -544,7 +544,7 @@ impl<'b> VirtualDom {
|
||||||
let new_id = self.next_element(new, parent.template.get().node_paths[idx]);
|
let new_id = self.next_element(new, parent.template.get().node_paths[idx]);
|
||||||
|
|
||||||
// Now connect everything to the boundary
|
// Now connect everything to the boundary
|
||||||
self.scopes[scope.0].placeholder.set(Some(new_id));
|
self.scopes[scope].placeholder.set(Some(new_id));
|
||||||
|
|
||||||
// This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary
|
// This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary
|
||||||
// Note that we break off dynamic mutations only - since static mutations aren't rendered immediately
|
// Note that we break off dynamic mutations only - since static mutations aren't rendered immediately
|
||||||
|
@ -583,7 +583,7 @@ impl<'b> VirtualDom {
|
||||||
let new_id = self.next_element(template, template.template.get().node_paths[idx]);
|
let new_id = self.next_element(template, template.template.get().node_paths[idx]);
|
||||||
|
|
||||||
// Set the placeholder of the scope
|
// Set the placeholder of the scope
|
||||||
self.scopes[scope.0].placeholder.set(Some(new_id));
|
self.scopes[scope].placeholder.set(Some(new_id));
|
||||||
|
|
||||||
// Since the placeholder is already in the DOM, we don't create any new nodes
|
// Since the placeholder is already in the DOM, we don't create any new nodes
|
||||||
self.mutations.push(AssignId {
|
self.mutations.push(AssignId {
|
||||||
|
|
|
@ -15,7 +15,7 @@ use DynamicNode::*;
|
||||||
|
|
||||||
impl<'b> VirtualDom {
|
impl<'b> VirtualDom {
|
||||||
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
|
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
|
||||||
let scope_state = &mut self.scopes[scope.0];
|
let scope_state = &mut self.scopes[scope];
|
||||||
|
|
||||||
self.scope_stack.push(scope);
|
self.scope_stack.push(scope);
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -202,7 +202,7 @@ impl<'b> VirtualDom {
|
||||||
right.scope.set(Some(scope_id));
|
right.scope.set(Some(scope_id));
|
||||||
|
|
||||||
// copy out the box for both
|
// copy out the box for both
|
||||||
let old = self.scopes[scope_id.0].props.as_ref();
|
let old = self.scopes[scope_id].props.as_ref();
|
||||||
let new: Box<dyn AnyProps> = right.props.take().unwrap();
|
let new: Box<dyn AnyProps> = right.props.take().unwrap();
|
||||||
let new: Box<dyn AnyProps> = unsafe { std::mem::transmute(new) };
|
let new: Box<dyn AnyProps> = unsafe { std::mem::transmute(new) };
|
||||||
|
|
||||||
|
@ -214,14 +214,14 @@ impl<'b> VirtualDom {
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, move over the props from the old to the new, dropping old props in the process
|
// First, move over the props from the old to the new, dropping old props in the process
|
||||||
self.scopes[scope_id.0].props = Some(new);
|
self.scopes[scope_id].props = Some(new);
|
||||||
|
|
||||||
// Now run the component and diff it
|
// Now run the component and diff it
|
||||||
self.run_scope(scope_id);
|
self.run_scope(scope_id);
|
||||||
self.diff_scope(scope_id);
|
self.diff_scope(scope_id);
|
||||||
|
|
||||||
self.dirty_scopes.remove(&DirtyScope {
|
self.dirty_scopes.remove(&DirtyScope {
|
||||||
height: self.scopes[scope_id.0].height,
|
height: self.scopes[scope_id].height,
|
||||||
id: scope_id,
|
id: scope_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -721,7 +721,7 @@ impl<'b> VirtualDom {
|
||||||
|
|
||||||
Component(comp) => {
|
Component(comp) => {
|
||||||
let scope = comp.scope.get().unwrap();
|
let scope = comp.scope.get().unwrap();
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||||
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
|
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
|
||||||
RenderReturn::Aborted(_node) => todo!(),
|
RenderReturn::Aborted(_node) => todo!(),
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
|
@ -923,14 +923,14 @@ impl<'b> VirtualDom {
|
||||||
.expect("VComponents to always have a scope");
|
.expect("VComponents to always have a scope");
|
||||||
|
|
||||||
// Remove the component from the dom
|
// Remove the component from the dom
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||||
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
|
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
|
||||||
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
|
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Restore the props back to the vcomponent in case it gets rendered again
|
// Restore the props back to the vcomponent in case it gets rendered again
|
||||||
let props = self.scopes[scope.0].props.take();
|
let props = self.scopes[scope].props.take();
|
||||||
*comp.props.borrow_mut() = unsafe { std::mem::transmute(props) };
|
*comp.props.borrow_mut() = unsafe { std::mem::transmute(props) };
|
||||||
|
|
||||||
// Now drop all the resouces
|
// Now drop all the resouces
|
||||||
|
@ -945,7 +945,7 @@ impl<'b> VirtualDom {
|
||||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||||
Some(Component(comp)) => {
|
Some(Component(comp)) => {
|
||||||
let scope = comp.scope.get().unwrap();
|
let scope = comp.scope.get().unwrap();
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||||
RenderReturn::Ready(t) => self.find_first_element(t),
|
RenderReturn::Ready(t) => self.find_first_element(t),
|
||||||
_ => todo!("cannot handle nonstandard nodes"),
|
_ => todo!("cannot handle nonstandard nodes"),
|
||||||
}
|
}
|
||||||
|
@ -961,7 +961,7 @@ impl<'b> VirtualDom {
|
||||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||||
Some(Component(comp)) => {
|
Some(Component(comp)) => {
|
||||||
let scope = comp.scope.get().unwrap();
|
let scope = comp.scope.get().unwrap();
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||||
RenderReturn::Ready(t) => self.find_last_element(t),
|
RenderReturn::Ready(t) => self.find_last_element(t),
|
||||||
_ => todo!("cannot handle nonstandard nodes"),
|
_ => todo!("cannot handle nonstandard nodes"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -757,15 +757,21 @@ impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> IntoDynNode<'_> for &'a str {
|
impl<'a, 'b> IntoDynNode<'b> for &'a str {
|
||||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> {
|
||||||
cx.text_node(format_args!("{}", self))
|
DynamicNode::Text(VText {
|
||||||
|
value: bumpalo::collections::String::from_str_in(self, cx.bump()).into_bump_str(),
|
||||||
|
id: Default::default(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDynNode<'_> for String {
|
impl IntoDynNode<'_> for String {
|
||||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
||||||
cx.text_node(format_args!("{}", self))
|
DynamicNode::Text(VText {
|
||||||
|
value: cx.bump().alloc(self),
|
||||||
|
id: Default::default(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ impl VirtualDom {
|
||||||
// If the task completes...
|
// If the task completes...
|
||||||
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
||||||
// Remove it from the scope so we dont try to double drop it when the scope dropes
|
// Remove it from the scope so we dont try to double drop it when the scope dropes
|
||||||
let scope = &self.scopes[task.scope.0];
|
let scope = &self.scopes[task.scope];
|
||||||
scope.spawned_tasks.borrow_mut().remove(&id);
|
scope.spawned_tasks.borrow_mut().remove(&id);
|
||||||
|
|
||||||
// Remove it from the scheduler
|
// Remove it from the scheduler
|
||||||
|
@ -40,7 +40,7 @@ impl VirtualDom {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
|
pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
|
||||||
self.scopes[id.0]
|
self.scopes[id]
|
||||||
.consume_context::<Rc<SuspenseContext>>()
|
.consume_context::<Rc<SuspenseContext>>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ impl VirtualDom {
|
||||||
if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
|
if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
|
||||||
let fiber = self.acquire_suspense_boundary(leaf.scope_id);
|
let fiber = self.acquire_suspense_boundary(leaf.scope_id);
|
||||||
|
|
||||||
let scope = &self.scopes[scope_id.0];
|
let scope = &self.scopes[scope_id];
|
||||||
let arena = scope.current_frame();
|
let arena = scope.current_frame();
|
||||||
|
|
||||||
let ret = arena.bump().alloc(match new_nodes {
|
let ret = arena.bump().alloc(match new_nodes {
|
||||||
|
|
|
@ -24,9 +24,9 @@ impl VirtualDom {
|
||||||
let parent = self.acquire_current_scope_raw();
|
let parent = self.acquire_current_scope_raw();
|
||||||
let entry = self.scopes.vacant_entry();
|
let entry = self.scopes.vacant_entry();
|
||||||
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
||||||
let id = ScopeId(entry.key());
|
let id = entry.key();
|
||||||
|
|
||||||
entry.insert(Box::new(ScopeState {
|
entry.insert(ScopeState {
|
||||||
parent,
|
parent,
|
||||||
id,
|
id,
|
||||||
height,
|
height,
|
||||||
|
@ -44,13 +44,13 @@ impl VirtualDom {
|
||||||
shared_contexts: Default::default(),
|
shared_contexts: Default::default(),
|
||||||
borrowed_props: Default::default(),
|
borrowed_props: Default::default(),
|
||||||
attributes_to_drop: Default::default(),
|
attributes_to_drop: Default::default(),
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> {
|
fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> {
|
||||||
let id = self.scope_stack.last().copied()?;
|
let id = self.scope_stack.last().copied()?;
|
||||||
let scope = self.scopes.get(id.0)?;
|
let scope = self.scopes.get(id)?;
|
||||||
Some(scope.as_ref())
|
Some(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
||||||
|
@ -60,9 +60,9 @@ impl VirtualDom {
|
||||||
self.ensure_drop_safety(scope_id);
|
self.ensure_drop_safety(scope_id);
|
||||||
|
|
||||||
let mut new_nodes = unsafe {
|
let mut new_nodes = unsafe {
|
||||||
self.scopes[scope_id.0].previous_frame().bump_mut().reset();
|
self.scopes[scope_id].previous_frame().bump_mut().reset();
|
||||||
|
|
||||||
let scope = &self.scopes[scope_id.0];
|
let scope = &self.scopes[scope_id];
|
||||||
|
|
||||||
scope.hook_idx.set(0);
|
scope.hook_idx.set(0);
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ impl VirtualDom {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let scope = &self.scopes[scope_id.0];
|
let scope = &self.scopes[scope_id];
|
||||||
|
|
||||||
// We write on top of the previous frame and then make it the current by pushing the generation forward
|
// We write on top of the previous frame and then make it the current by pushing the generation forward
|
||||||
let frame = scope.previous_frame();
|
let frame = scope.previous_frame();
|
||||||
|
|
|
@ -10,12 +10,15 @@ use crate::{
|
||||||
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||||
};
|
};
|
||||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||||
|
use bumpslab::{BumpSlab, Slot};
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
|
use slab::{Slab, VacantEntry};
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
fmt::{Arguments, Debug},
|
fmt::{Arguments, Debug},
|
||||||
future::Future,
|
future::Future,
|
||||||
|
ops::{Index, IndexMut},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
@ -63,6 +66,95 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> {
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
||||||
pub struct ScopeId(pub usize);
|
pub struct ScopeId(pub usize);
|
||||||
|
|
||||||
|
/// A thin wrapper around a BumpSlab that uses ids to index into the slab.
|
||||||
|
pub(crate) struct ScopeSlab {
|
||||||
|
slab: BumpSlab<ScopeState>,
|
||||||
|
// a slab of slots of stable pointers to the ScopeState in the bump slab
|
||||||
|
entries: Slab<Slot<'static, ScopeState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ScopeSlab {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Bump slab doesn't drop its contents, so we need to do it manually
|
||||||
|
for slot in self.entries.drain() {
|
||||||
|
self.slab.remove(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ScopeSlab {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
slab: BumpSlab::new(),
|
||||||
|
entries: Slab::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScopeSlab {
|
||||||
|
pub(crate) fn get(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||||
|
self.entries.get(id.0).map(|slot| unsafe { &*slot.ptr() })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_mut(&mut self, id: ScopeId) -> Option<&mut ScopeState> {
|
||||||
|
self.entries
|
||||||
|
.get(id.0)
|
||||||
|
.map(|slot| unsafe { &mut *slot.ptr_mut() })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn vacant_entry(&mut self) -> ScopeSlabEntry {
|
||||||
|
let entry = self.entries.vacant_entry();
|
||||||
|
ScopeSlabEntry {
|
||||||
|
slab: &mut self.slab,
|
||||||
|
entry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove(&mut self, id: ScopeId) {
|
||||||
|
self.slab.remove(self.entries.remove(id.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn contains(&self, id: ScopeId) -> bool {
|
||||||
|
self.entries.contains(id.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn iter(&self) -> impl Iterator<Item = &ScopeState> {
|
||||||
|
self.entries.iter().map(|(_, slot)| unsafe { &*slot.ptr() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ScopeSlabEntry<'a> {
|
||||||
|
slab: &'a mut BumpSlab<ScopeState>,
|
||||||
|
entry: VacantEntry<'a, Slot<'static, ScopeState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ScopeSlabEntry<'a> {
|
||||||
|
pub(crate) fn key(&self) -> ScopeId {
|
||||||
|
ScopeId(self.entry.key())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insert(self, scope: ScopeState) -> &'a ScopeState {
|
||||||
|
let slot = self.slab.push(scope);
|
||||||
|
// this is safe because the slot is only ever accessed with the lifetime of the borrow of the slab
|
||||||
|
let slot = unsafe { std::mem::transmute(slot) };
|
||||||
|
let entry = self.entry.insert(slot);
|
||||||
|
unsafe { &*entry.ptr() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<ScopeId> for ScopeSlab {
|
||||||
|
type Output = ScopeState;
|
||||||
|
fn index(&self, id: ScopeId) -> &Self::Output {
|
||||||
|
self.get(id).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexMut<ScopeId> for ScopeSlab {
|
||||||
|
fn index_mut(&mut self, id: ScopeId) -> &mut Self::Output {
|
||||||
|
self.get_mut(id).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A component's state separate from its props.
|
/// A component's state separate from its props.
|
||||||
///
|
///
|
||||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
any_props::VProps,
|
any_props::VProps,
|
||||||
arena::{ElementId, ElementRef},
|
arena::{ElementId, ElementRef},
|
||||||
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
|
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg, ScopeSlab},
|
||||||
mutations::Mutation,
|
mutations::Mutation,
|
||||||
nodes::RenderReturn,
|
nodes::RenderReturn,
|
||||||
nodes::{Template, TemplateId},
|
nodes::{Template, TemplateId},
|
||||||
|
@ -177,7 +177,7 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future
|
||||||
pub struct VirtualDom {
|
pub struct VirtualDom {
|
||||||
// Maps a template path to a map of byteindexes to templates
|
// Maps a template path to a map of byteindexes to templates
|
||||||
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
|
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
|
||||||
pub(crate) scopes: Slab<Box<ScopeState>>,
|
pub(crate) scopes: ScopeSlab,
|
||||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||||
pub(crate) scheduler: Rc<Scheduler>,
|
pub(crate) scheduler: Rc<Scheduler>,
|
||||||
|
|
||||||
|
@ -258,7 +258,7 @@ impl VirtualDom {
|
||||||
rx,
|
rx,
|
||||||
scheduler: Scheduler::new(tx),
|
scheduler: Scheduler::new(tx),
|
||||||
templates: Default::default(),
|
templates: Default::default(),
|
||||||
scopes: Slab::default(),
|
scopes: Default::default(),
|
||||||
elements: Default::default(),
|
elements: Default::default(),
|
||||||
scope_stack: Vec::new(),
|
scope_stack: Vec::new(),
|
||||||
dirty_scopes: BTreeSet::new(),
|
dirty_scopes: BTreeSet::new(),
|
||||||
|
@ -291,14 +291,14 @@ impl VirtualDom {
|
||||||
///
|
///
|
||||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||||
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||||
self.scopes.get(id.0).map(|f| f.as_ref())
|
self.scopes.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the single scope at the top of the VirtualDom tree that will always be around
|
/// Get the single scope at the top of the VirtualDom tree that will always be around
|
||||||
///
|
///
|
||||||
/// This scope has a ScopeId of 0 and is the root of the tree
|
/// This scope has a ScopeId of 0 and is the root of the tree
|
||||||
pub fn base_scope(&self) -> &ScopeState {
|
pub fn base_scope(&self) -> &ScopeState {
|
||||||
self.scopes.get(0).unwrap()
|
self.scopes.get(ScopeId(0)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the virtualdom with a global context inserted into the base scope
|
/// Build the virtualdom with a global context inserted into the base scope
|
||||||
|
@ -313,7 +313,7 @@ impl VirtualDom {
|
||||||
///
|
///
|
||||||
/// Whenever the VirtualDom "works", it will re-render this scope
|
/// Whenever the VirtualDom "works", it will re-render this scope
|
||||||
pub fn mark_dirty(&mut self, id: ScopeId) {
|
pub fn mark_dirty(&mut self, id: ScopeId) {
|
||||||
if let Some(scope) = self.scopes.get(id.0) {
|
if let Some(scope) = self.scopes.get(id) {
|
||||||
let height = scope.height;
|
let height = scope.height;
|
||||||
self.dirty_scopes.insert(DirtyScope { height, id });
|
self.dirty_scopes.insert(DirtyScope { height, id });
|
||||||
}
|
}
|
||||||
|
@ -324,7 +324,7 @@ impl VirtualDom {
|
||||||
/// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is
|
/// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is
|
||||||
/// currently suspended.
|
/// currently suspended.
|
||||||
pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
|
pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
|
||||||
!self.scopes[id.0]
|
!self.scopes[id]
|
||||||
.consume_context::<Rc<SuspenseContext>>()
|
.consume_context::<Rc<SuspenseContext>>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.waiting_on
|
.waiting_on
|
||||||
|
@ -499,7 +499,7 @@ impl VirtualDom {
|
||||||
pub fn replace_template(&mut self, template: Template<'static>) {
|
pub fn replace_template(&mut self, template: Template<'static>) {
|
||||||
self.register_template_first_byte_index(template);
|
self.register_template_first_byte_index(template);
|
||||||
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
||||||
for (_, scope) in &self.scopes {
|
for scope in self.scopes.iter() {
|
||||||
if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
|
if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
|
||||||
if sync.template.get().name.rsplit_once(':').unwrap().0
|
if sync.template.get().name.rsplit_once(':').unwrap().0
|
||||||
== template.name.rsplit_once(':').unwrap().0
|
== template.name.rsplit_once(':').unwrap().0
|
||||||
|
@ -583,7 +583,7 @@ impl VirtualDom {
|
||||||
loop {
|
loop {
|
||||||
// first, unload any complete suspense trees
|
// first, unload any complete suspense trees
|
||||||
for finished_fiber in self.finished_fibers.drain(..) {
|
for finished_fiber in self.finished_fibers.drain(..) {
|
||||||
let scope = &self.scopes[finished_fiber.0];
|
let scope = &self.scopes[finished_fiber];
|
||||||
let context = scope.has_context::<Rc<SuspenseContext>>().unwrap();
|
let context = scope.has_context::<Rc<SuspenseContext>>().unwrap();
|
||||||
|
|
||||||
self.mutations
|
self.mutations
|
||||||
|
@ -607,7 +607,7 @@ impl VirtualDom {
|
||||||
self.dirty_scopes.remove(&dirty);
|
self.dirty_scopes.remove(&dirty);
|
||||||
|
|
||||||
// If the scope doesn't exist for whatever reason, then we should skip it
|
// If the scope doesn't exist for whatever reason, then we should skip it
|
||||||
if !self.scopes.contains(dirty.id.0) {
|
if !self.scopes.contains(dirty.id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -626,7 +626,7 @@ impl VirtualDom {
|
||||||
// If suspended leaves are present, then we should find the boundary for this scope and attach things
|
// If suspended leaves are present, then we should find the boundary for this scope and attach things
|
||||||
// No placeholder necessary since this is a diff
|
// No placeholder necessary since this is a diff
|
||||||
if !self.collected_leaves.is_empty() {
|
if !self.collected_leaves.is_empty() {
|
||||||
let mut boundary = self.scopes[dirty.id.0]
|
let mut boundary = self.scopes[dirty.id]
|
||||||
.consume_context::<Rc<SuspenseContext>>()
|
.consume_context::<Rc<SuspenseContext>>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react"]
|
||||||
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
|
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
|
||||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
|
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
|
||||||
|
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
||||||
|
|
||||||
serde = "1.0.136"
|
serde = "1.0.136"
|
||||||
serde_json = "1.0.79"
|
serde_json = "1.0.79"
|
||||||
|
@ -34,7 +35,6 @@ infer = "0.11.0"
|
||||||
dunce = "1.0.2"
|
dunce = "1.0.2"
|
||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
|
|
||||||
interprocess = { version = "1.1.1", optional = true }
|
|
||||||
futures-util = "0.3.25"
|
futures-util = "0.3.25"
|
||||||
|
|
||||||
[target.'cfg(target_os = "ios")'.dependencies]
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
|
@ -50,7 +50,7 @@ tokio_runtime = ["tokio"]
|
||||||
fullscreen = ["wry/fullscreen"]
|
fullscreen = ["wry/fullscreen"]
|
||||||
transparent = ["wry/transparent"]
|
transparent = ["wry/transparent"]
|
||||||
tray = ["wry/tray"]
|
tray = ["wry/tray"]
|
||||||
hot-reload = ["interprocess"]
|
hot-reload = ["dioxus-hot-reload"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus-core-macro = { path = "../core-macro" }
|
dioxus-core-macro = { path = "../core-macro" }
|
||||||
|
|
|
@ -9,6 +9,7 @@ use crate::Config;
|
||||||
use crate::WebviewHandler;
|
use crate::WebviewHandler;
|
||||||
use dioxus_core::ScopeState;
|
use dioxus_core::ScopeState;
|
||||||
use dioxus_core::VirtualDom;
|
use dioxus_core::VirtualDom;
|
||||||
|
use dioxus_hot_reload::HotReloadMsg;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use wry::application::event::Event;
|
use wry::application::event::Event;
|
||||||
|
@ -206,13 +207,12 @@ impl DesktopContext {
|
||||||
"method":"eval_result",
|
"method":"eval_result",
|
||||||
"params": (
|
"params": (
|
||||||
function(){{
|
function(){{
|
||||||
{}
|
{code}
|
||||||
}}
|
}}
|
||||||
)()
|
)()
|
||||||
}})
|
}})
|
||||||
);
|
);
|
||||||
"#,
|
"#
|
||||||
code
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = self.webview.evaluate_script(&script) {
|
if let Err(e) = self.webview.evaluate_script(&script) {
|
||||||
|
@ -285,6 +285,8 @@ pub enum EventData {
|
||||||
|
|
||||||
Ipc(IpcMessage),
|
Ipc(IpcMessage),
|
||||||
|
|
||||||
|
HotReloadEvent(HotReloadMsg),
|
||||||
|
|
||||||
NewWindow,
|
NewWindow,
|
||||||
|
|
||||||
CloseWindow,
|
CloseWindow,
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use dioxus_core::Template;
|
|
||||||
|
|
||||||
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
|
||||||
use std::io::{BufRead, BufReader};
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::{sync::Arc, sync::Mutex};
|
|
||||||
|
|
||||||
fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalSocketStream> {
|
|
||||||
connection
|
|
||||||
.map_err(|error| eprintln!("Incoming connection failed: {}", error))
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn init(proxy: futures_channel::mpsc::UnboundedSender<Template<'static>>) {
|
|
||||||
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
|
|
||||||
Arc::new(Mutex::new(None));
|
|
||||||
|
|
||||||
let latest_in_connection_handle = latest_in_connection.clone();
|
|
||||||
|
|
||||||
// connect to processes for incoming data
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let temp_file = std::env::temp_dir().join("@dioxusin");
|
|
||||||
|
|
||||||
if let Ok(listener) = LocalSocketListener::bind(temp_file) {
|
|
||||||
for conn in listener.incoming().filter_map(handle_error) {
|
|
||||||
*latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
loop {
|
|
||||||
if let Some(conn) = &mut *latest_in_connection.lock().unwrap() {
|
|
||||||
let mut buf = String::new();
|
|
||||||
match conn.read_line(&mut buf) {
|
|
||||||
Ok(_) => {
|
|
||||||
let msg: Template<'static> =
|
|
||||||
serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
|
|
||||||
proxy.unbounded_send(msg).unwrap();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
if err.kind() != std::io::ErrorKind::WouldBlock {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// give the error handler time to take the mutex
|
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -12,9 +12,6 @@ mod protocol;
|
||||||
mod waker;
|
mod waker;
|
||||||
mod webview;
|
mod webview;
|
||||||
|
|
||||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
|
||||||
mod hot_reload;
|
|
||||||
|
|
||||||
pub use cfg::Config;
|
pub use cfg::Config;
|
||||||
pub use desktop_context::{
|
pub use desktop_context::{
|
||||||
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
|
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
|
||||||
|
@ -111,6 +108,18 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||||
|
|
||||||
let proxy = event_loop.create_proxy();
|
let proxy = event_loop.create_proxy();
|
||||||
|
|
||||||
|
// Intialize hot reloading if it is enabled
|
||||||
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
|
{
|
||||||
|
let proxy = proxy.clone();
|
||||||
|
dioxus_hot_reload::connect(move |template| {
|
||||||
|
let _ = proxy.send_event(UserWindowEvent(
|
||||||
|
EventData::HotReloadEvent(template),
|
||||||
|
unsafe { WindowId::dummy() },
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// We start the tokio runtime *on this thread*
|
// We start the tokio runtime *on this thread*
|
||||||
// Any future we poll later will use this runtime to spawn tasks and for IO
|
// Any future we poll later will use this runtime to spawn tasks and for IO
|
||||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
@ -176,6 +185,19 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::UserEvent(event) => match event.0 {
|
Event::UserEvent(event) => match event.0 {
|
||||||
|
EventData::HotReloadEvent(msg) => match msg {
|
||||||
|
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
|
||||||
|
for webview in webviews.values_mut() {
|
||||||
|
webview.dom.replace_template(template);
|
||||||
|
|
||||||
|
poll_vdom(webview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dioxus_hot_reload::HotReloadMsg::Shutdown => {
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
EventData::CloseWindow => {
|
EventData::CloseWindow => {
|
||||||
webviews.remove(&event.1);
|
webviews.remove(&event.1);
|
||||||
|
|
||||||
|
@ -304,5 +326,5 @@ fn send_edits(edits: Mutations, webview: &WebView) {
|
||||||
let serialized = serde_json::to_string(&edits).unwrap();
|
let serialized = serde_json::to_string(&edits).unwrap();
|
||||||
|
|
||||||
// todo: use SSE and binary data to send the edits with lower overhead
|
// todo: use SSE and binary data to send the edits with lower overhead
|
||||||
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized));
|
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,14 @@ fn module_loader(root_name: &str) -> String {
|
||||||
<script>
|
<script>
|
||||||
{INTERPRETER_JS}
|
{INTERPRETER_JS}
|
||||||
|
|
||||||
let rootname = "{}";
|
let rootname = "{root_name}";
|
||||||
let root = window.document.getElementById(rootname);
|
let root = window.document.getElementById(rootname);
|
||||||
if (root != null) {{
|
if (root != null) {{
|
||||||
window.interpreter = new Interpreter(root);
|
window.interpreter = new Interpreter(root);
|
||||||
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
||||||
}}
|
}}
|
||||||
</script>
|
</script>
|
||||||
"#,
|
"#
|
||||||
root_name
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,15 @@ dioxus-core-macro = { path = "../core-macro", version = "^0.3.0", optional = tru
|
||||||
dioxus-hooks = { path = "../hooks", version = "^0.3.0", optional = true }
|
dioxus-hooks = { path = "../hooks", version = "^0.3.0", optional = true }
|
||||||
dioxus-rsx = { path = "../rsx", version = "0.0.2", optional = true }
|
dioxus-rsx = { path = "../rsx", version = "0.0.2", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
dioxus-hot-reload = { path = "../hot-reload", version = "0.1.0", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["macro", "hooks", "html"]
|
default = ["macro", "hooks", "html", "hot-reload"]
|
||||||
macro = ["dioxus-core-macro", "dioxus-rsx"]
|
macro = ["dioxus-core-macro", "dioxus-rsx"]
|
||||||
html = ["dioxus-html"]
|
html = ["dioxus-html"]
|
||||||
hooks = ["dioxus-hooks"]
|
hooks = ["dioxus-hooks"]
|
||||||
|
hot-reload = ["dioxus-hot-reload"]
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -31,4 +31,7 @@ pub mod prelude {
|
||||||
|
|
||||||
#[cfg(feature = "html")]
|
#[cfg(feature = "html")]
|
||||||
pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes};
|
pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes};
|
||||||
|
|
||||||
|
#[cfg(all(not(target_arch = "wasm32"), feature = "hot-reload"))]
|
||||||
|
pub use dioxus_hot_reload::{self, hot_reload_init};
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ mod tests {
|
||||||
fn app(cx: Scope, name: String) -> Element {
|
fn app(cx: Scope, name: String) -> Element {
|
||||||
let task = use_coroutine(cx, |mut rx: UnboundedReceiver<i32>| async move {
|
let task = use_coroutine(cx, |mut rx: UnboundedReceiver<i32>| async move {
|
||||||
while let Some(msg) = rx.next().await {
|
while let Some(msg) = rx.next().await {
|
||||||
println!("got message: {}", msg);
|
println!("got message: {msg}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ mod tests {
|
||||||
|
|
||||||
async fn view_task(mut rx: UnboundedReceiver<i32>) {
|
async fn view_task(mut rx: UnboundedReceiver<i32>) {
|
||||||
while let Some(msg) = rx.next().await {
|
while let Some(msg) = rx.next().await {
|
||||||
println!("got message: {}", msg);
|
println!("got message: {msg}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
26
packages/hot-reload/Cargo.toml
Normal file
26
packages/hot-reload/Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
[package]
|
||||||
|
name = "dioxus-hot-reload"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||||
|
homepage = "https://dioxuslabs.com"
|
||||||
|
description = "Hot reloading utilites for Dioxus"
|
||||||
|
documentation = "https://dioxuslabs.com"
|
||||||
|
keywords = ["dom", "ui", "gui", "react", "hot-reloading", "watch"]
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dioxus-rsx = { path = "../rsx" }
|
||||||
|
dioxus-core = { path = "../core", features = ["serialize"] }
|
||||||
|
dioxus-html = { path = "../html", features = ["hot-reload-context"] }
|
||||||
|
|
||||||
|
interprocess = { version = "1.2.1" }
|
||||||
|
notify = "5.0.0"
|
||||||
|
chrono = "0.4.23"
|
||||||
|
serde_json = "1.0.91"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
execute = "0.2.11"
|
||||||
|
once_cell = "1.17.0"
|
||||||
|
ignore = "0.4.19"
|
170
packages/hot-reload/README.md
Normal file
170
packages/hot-reload/README.md
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
# `dioxus-hot-reload`: Hot Reloading Utilites for Dioxus
|
||||||
|
|
||||||
|
|
||||||
|
[![Crates.io][crates-badge]][crates-url]
|
||||||
|
[![MIT licensed][mit-badge]][mit-url]
|
||||||
|
[![Build Status][actions-badge]][actions-url]
|
||||||
|
[![Discord chat][discord-badge]][discord-url]
|
||||||
|
|
||||||
|
[crates-badge]: https://img.shields.io/crates/v/dioxus-hot-reload.svg
|
||||||
|
[crates-url]: https://crates.io/crates/dioxus-hot-reload
|
||||||
|
|
||||||
|
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||||
|
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||||
|
|
||||||
|
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||||
|
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||||
|
|
||||||
|
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||||
|
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||||
|
|
||||||
|
[Website](https://dioxuslabs.com) |
|
||||||
|
[Guides](https://dioxuslabs.com/guide/) |
|
||||||
|
[API Docs](https://docs.rs/dioxus-hot-reload/latest/dioxus_hot_reload) |
|
||||||
|
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||||
|
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Dioxus supports hot reloading for static parts of rsx macros. This enables changing the styling of your application without recompiling the rust code. This is useful for rapid iteration on the styling of your application.
|
||||||
|
|
||||||
|
|
||||||
|
Hot reloading could update the following change without recompiling:
|
||||||
|
```rust
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
"Count: {count}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
=>
|
||||||
|
```rust
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
color: "red",
|
||||||
|
font_size: "2em",
|
||||||
|
"Count: {count}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
But it could not update the following change:
|
||||||
|
```rust
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
"Count: {count}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
=>
|
||||||
|
```rust
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
"Count: {count*2}",
|
||||||
|
onclick: |_| println!("clicked"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
> This crate implements hot reloading for native compilation targets not WASM. For hot relaoding with the web renderer, see the [dioxus-cli](https://github.com/DioxusLabs/cli) project.
|
||||||
|
|
||||||
|
Add this to the top of your main function on any renderer that supports hot reloading to start the hot reloading server:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main(){
|
||||||
|
hot_reload_init!();
|
||||||
|
// launch your application
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
By default the dev server watches on the root of the crate the macro is called in and ignores changes in the `/target` directory and any directories set in the `.gitignore` file in the root directory. To watch on custom paths pass call the `with_paths` function on the config builder:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main(){
|
||||||
|
hot_reload_init!(Config::new().with_paths(&["src", "examples", "assets"]));
|
||||||
|
// launch your application
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
By default the hot reloading server will output some logs in the console, to disable these logs call the `with_logging` function on the config builder:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main(){
|
||||||
|
hot_reload_init!(Config::new().with_logging(false));
|
||||||
|
// launch your application
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To rebuild the application when the logic changes, you can use the `with_rebuild_command` function on the config builder. This command will be called when hot reloading fails to quickly update the rsx:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main(){
|
||||||
|
hot_reload_init!(Config::new().with_rebuild_command("cargo run"));
|
||||||
|
// launch your application
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using a namespace other than html, you can implement the [HotReloadingContext](https://docs.rs/dioxus-rsx/latest/dioxus_rsx/trait.HotReloadingContext.html) trait to provide a mapping between the rust names of your elements/attributes and the resulting strings.
|
||||||
|
|
||||||
|
You can then provide the Context to the builder to make hot reloading work with your custom namespace:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main(){
|
||||||
|
// Note: Use default instead of new if you are using a custom namespace
|
||||||
|
hot_reload_init!(Config::<MyContext>::default());
|
||||||
|
// launch your application
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementing Hot Reloading for a Custom Renderer
|
||||||
|
|
||||||
|
To add hot reloading support to your custom renderer you can use the connect function. This will connect to the dev server you just need to provide a way to transfer `Template`s to the `VirtualDom`. Once you implement this your users can use the hot_reload_init function just like any other render.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn launch(app: Component) {
|
||||||
|
let mut vdom = VirtualDom::new(app);
|
||||||
|
// ...
|
||||||
|
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
dioxus_hot_reload::connect(move |msg| {
|
||||||
|
let _ = tx.send(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
Some(msg) = rx.recv() => {
|
||||||
|
match msg{
|
||||||
|
HotReloadMsg::Shutdown => {
|
||||||
|
// ... shutdown the application
|
||||||
|
}
|
||||||
|
HotReloadMsg::UpdateTemplate(template) => {
|
||||||
|
// update the template in the virtual dom
|
||||||
|
vdom.replace_template(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = vdom.wait_for_work() => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mutations = vdom.render_immediate();
|
||||||
|
// apply the mutations to the dom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||||
|
- Join the discord and ask questions!
|
||||||
|
|
||||||
|
## License
|
||||||
|
This project is licensed under the [MIT license].
|
||||||
|
|
||||||
|
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||||
|
for inclusion in Dioxus by you shall be licensed as MIT without any additional
|
||||||
|
terms or conditions.
|
359
packages/hot-reload/src/lib.rs
Normal file
359
packages/hot-reload/src/lib.rs
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
use std::{
|
||||||
|
io::{BufRead, BufReader, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
str::FromStr,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use dioxus_core::Template;
|
||||||
|
use dioxus_rsx::{
|
||||||
|
hot_reload::{FileMap, UpdateResult},
|
||||||
|
HotReloadingContext,
|
||||||
|
};
|
||||||
|
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||||
|
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
|
|
||||||
|
pub use dioxus_html::HtmlCtx;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A message the hot reloading server sends to the client
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||||
|
pub enum HotReloadMsg {
|
||||||
|
/// A template has been updated
|
||||||
|
#[serde(borrow = "'static")]
|
||||||
|
UpdateTemplate(Template<'static>),
|
||||||
|
/// The program needs to be recompiled, and the client should shut down
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Config<Ctx: HotReloadingContext = HtmlCtx> {
|
||||||
|
root_path: &'static str,
|
||||||
|
listening_paths: &'static [&'static str],
|
||||||
|
excluded_paths: &'static [&'static str],
|
||||||
|
log: bool,
|
||||||
|
rebuild_with: Option<Box<dyn FnMut() -> bool + Send + 'static>>,
|
||||||
|
phantom: std::marker::PhantomData<Ctx>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx: HotReloadingContext> Default for Config<Ctx> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
root_path: "",
|
||||||
|
listening_paths: &[""],
|
||||||
|
excluded_paths: &["./target"],
|
||||||
|
log: true,
|
||||||
|
rebuild_with: None,
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config<HtmlCtx> {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
root_path: "",
|
||||||
|
listening_paths: &[""],
|
||||||
|
excluded_paths: &["./target"],
|
||||||
|
log: true,
|
||||||
|
rebuild_with: None,
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx: HotReloadingContext> Config<Ctx> {
|
||||||
|
/// Set the root path of the project (where the Cargo.toml file is). This is automatically set by the [`hot_reload_init`] macro.
|
||||||
|
pub fn root(self, path: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
root_path: path,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set whether to enable logs
|
||||||
|
pub fn with_logging(self, log: bool) -> Self {
|
||||||
|
Self { log, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the command to run to rebuild the project
|
||||||
|
///
|
||||||
|
/// For example to restart the application after a change is made, you could use `cargo run`
|
||||||
|
pub fn with_rebuild_command(self, rebuild_command: &'static str) -> Self {
|
||||||
|
self.with_rebuild_callback(move || {
|
||||||
|
execute::shell(rebuild_command)
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to spawn the rebuild command");
|
||||||
|
true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a callback to run to when the project needs to be rebuilt and returns if the server should shut down
|
||||||
|
///
|
||||||
|
/// For example a CLI application could rebuild the application when a change is made
|
||||||
|
pub fn with_rebuild_callback(
|
||||||
|
self,
|
||||||
|
rebuild_callback: impl FnMut() -> bool + Send + 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
rebuild_with: Some(Box::new(rebuild_callback)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the paths to listen for changes in to trigger hot reloading. If this is a directory it will listen for changes in all files in that directory recursively.
|
||||||
|
pub fn with_paths(self, paths: &'static [&'static str]) -> Self {
|
||||||
|
Self {
|
||||||
|
listening_paths: paths,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets paths to ignore changes on. This will override any paths set in the [`Config::with_paths`] method in the case of conflicts.
|
||||||
|
pub fn excluded_paths(self, paths: &'static [&'static str]) -> Self {
|
||||||
|
Self {
|
||||||
|
excluded_paths: paths,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the hot reloading listener
|
||||||
|
pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
||||||
|
let Config {
|
||||||
|
root_path,
|
||||||
|
listening_paths,
|
||||||
|
log,
|
||||||
|
mut rebuild_with,
|
||||||
|
excluded_paths,
|
||||||
|
phantom: _,
|
||||||
|
} = cfg;
|
||||||
|
|
||||||
|
if let Ok(crate_dir) = PathBuf::from_str(root_path) {
|
||||||
|
let temp_file = std::env::temp_dir().join("@dioxusin");
|
||||||
|
let channels = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let file_map = Arc::new(Mutex::new(FileMap::<Ctx>::new(crate_dir.clone())));
|
||||||
|
if let Ok(local_socket_stream) = LocalSocketListener::bind(temp_file.as_path()) {
|
||||||
|
let aborted = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
// listen for connections
|
||||||
|
std::thread::spawn({
|
||||||
|
let file_map = file_map.clone();
|
||||||
|
let channels = channels.clone();
|
||||||
|
let aborted = aborted.clone();
|
||||||
|
let _ = local_socket_stream.set_nonblocking(true);
|
||||||
|
move || {
|
||||||
|
loop {
|
||||||
|
if let Ok(mut connection) = local_socket_stream.accept() {
|
||||||
|
// send any templates than have changed before the socket connected
|
||||||
|
let templates: Vec<_> = {
|
||||||
|
file_map
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.map
|
||||||
|
.values()
|
||||||
|
.filter_map(|(_, template_slot)| *template_slot)
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
for template in templates {
|
||||||
|
if !send_msg(
|
||||||
|
HotReloadMsg::UpdateTemplate(template),
|
||||||
|
&mut connection,
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channels.lock().unwrap().push(connection);
|
||||||
|
if log {
|
||||||
|
println!("Connected to hot reloading 🚀");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
if *aborted.lock().unwrap() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// watch for changes
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
// try to find the gitingore file
|
||||||
|
let gitignore_file_path = crate_dir.join(".gitignore");
|
||||||
|
let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
|
||||||
|
|
||||||
|
let mut last_update_time = chrono::Local::now().timestamp();
|
||||||
|
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
|
let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
|
||||||
|
|
||||||
|
for path in listening_paths {
|
||||||
|
let full_path = crate_dir.join(path);
|
||||||
|
if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
|
||||||
|
if log {
|
||||||
|
println!(
|
||||||
|
"hot reloading failed to start watching {full_path:?}:\n{err:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let excluded_paths = excluded_paths
|
||||||
|
.iter()
|
||||||
|
.map(|path| crate_dir.join(PathBuf::from(path)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut rebuild = {
|
||||||
|
let aborted = aborted.clone();
|
||||||
|
let channels = channels.clone();
|
||||||
|
move || {
|
||||||
|
if let Some(rebuild_callback) = &mut rebuild_with {
|
||||||
|
if log {
|
||||||
|
println!("Rebuilding the application...");
|
||||||
|
}
|
||||||
|
let shutdown = rebuild_callback();
|
||||||
|
|
||||||
|
if shutdown {
|
||||||
|
*aborted.lock().unwrap() = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for channel in &mut *channels.lock().unwrap() {
|
||||||
|
send_msg(HotReloadMsg::Shutdown, channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shutdown;
|
||||||
|
} else if log {
|
||||||
|
println!(
|
||||||
|
"Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for evt in rx {
|
||||||
|
if chrono::Local::now().timestamp() > last_update_time {
|
||||||
|
if let Ok(evt) = evt {
|
||||||
|
let real_paths = evt
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.filter(|path| {
|
||||||
|
// skip non rust files
|
||||||
|
matches!(
|
||||||
|
path.extension().and_then(|p| p.to_str()),
|
||||||
|
Some("rs" | "toml" | "css" | "html" | "js")
|
||||||
|
)&&
|
||||||
|
// skip excluded paths
|
||||||
|
!excluded_paths.iter().any(|p| path.starts_with(p)) &&
|
||||||
|
// respect .gitignore
|
||||||
|
!gitignore
|
||||||
|
.matched_path_or_any_parents(path, false)
|
||||||
|
.is_ignore()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Give time for the change to take effect before reading the file
|
||||||
|
if !real_paths.is_empty() {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut channels = channels.lock().unwrap();
|
||||||
|
for path in real_paths {
|
||||||
|
// if this file type cannot be hot reloaded, rebuild the application
|
||||||
|
if path.extension().and_then(|p| p.to_str()) != Some("rs")
|
||||||
|
&& rebuild()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// find changes to the rsx in the file
|
||||||
|
match file_map
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.update_rsx(path, crate_dir.as_path())
|
||||||
|
{
|
||||||
|
UpdateResult::UpdatedRsx(msgs) => {
|
||||||
|
for msg in msgs {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < channels.len() {
|
||||||
|
let channel = &mut channels[i];
|
||||||
|
if send_msg(
|
||||||
|
HotReloadMsg::UpdateTemplate(msg),
|
||||||
|
channel,
|
||||||
|
) {
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
channels.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateResult::NeedsRebuild => {
|
||||||
|
drop(channels);
|
||||||
|
if rebuild() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_update_time = chrono::Local::now().timestamp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool {
|
||||||
|
if let Ok(msg) = serde_json::to_string(&msg) {
|
||||||
|
if channel.write_all(msg.as_bytes()).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if channel.write_all(&[b'\n']).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected
|
||||||
|
pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let temp_file = std::env::temp_dir().join("@dioxusin");
|
||||||
|
if let Ok(socket) = LocalSocketStream::connect(temp_file.as_path()) {
|
||||||
|
let mut buf_reader = BufReader::new(socket);
|
||||||
|
loop {
|
||||||
|
let mut buf = String::new();
|
||||||
|
match buf_reader.read_line(&mut buf) {
|
||||||
|
Ok(_) => {
|
||||||
|
let template: HotReloadMsg =
|
||||||
|
serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
|
||||||
|
f(template);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
if err.kind() != std::io::ErrorKind::WouldBlock {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the hot reloading server with the current directory as the root
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! hot_reload_init {
|
||||||
|
() => {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
dioxus_hot_reload::init(dioxus_hot_reload::Config::new().root(env!("CARGO_MANIFEST_DIR")));
|
||||||
|
};
|
||||||
|
|
||||||
|
($cfg: expr) => {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
dioxus_hot_reload::init($cfg.root(env!("CARGO_MANIFEST_DIR")));
|
||||||
|
};
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ fn render_template_node(node: &TemplateNode, out: &mut String) -> std::fmt::Resu
|
||||||
write!(out, "<{tag}")?;
|
write!(out, "<{tag}")?;
|
||||||
for attr in *attrs {
|
for attr in *attrs {
|
||||||
if let TemplateAttribute::Static { name, value, .. } = attr {
|
if let TemplateAttribute::Static { name, value, .. } = attr {
|
||||||
write!(out, "{}=\"{}\"", name, value)?;
|
write!(out, "{name}=\"{value}\"")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for child in *children {
|
for child in *children {
|
||||||
|
|
|
@ -445,6 +445,41 @@ class Interpreter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_mouse_data(event) {
|
||||||
|
const {
|
||||||
|
altKey,
|
||||||
|
button,
|
||||||
|
buttons,
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
ctrlKey,
|
||||||
|
metaKey,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
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,
|
||||||
|
offset_x: offsetX,
|
||||||
|
offset_y: offsetY,
|
||||||
|
page_x: pageX,
|
||||||
|
page_y: pageY,
|
||||||
|
screen_x: screenX,
|
||||||
|
screen_y: screenY,
|
||||||
|
shift_key: shiftKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function serialize_event(event) {
|
function serialize_event(event) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "copy":
|
case "copy":
|
||||||
|
@ -523,10 +558,6 @@ function serialize_event(event) {
|
||||||
values: {},
|
values: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "click":
|
|
||||||
case "contextmenu":
|
|
||||||
case "doubleclick":
|
|
||||||
case "dblclick":
|
|
||||||
case "drag":
|
case "drag":
|
||||||
case "dragend":
|
case "dragend":
|
||||||
case "dragenter":
|
case "dragenter":
|
||||||
|
@ -534,7 +565,13 @@ function serialize_event(event) {
|
||||||
case "dragleave":
|
case "dragleave":
|
||||||
case "dragover":
|
case "dragover":
|
||||||
case "dragstart":
|
case "dragstart":
|
||||||
case "drop":
|
case "drop": {
|
||||||
|
return { mouse: get_mouse_data(event) };
|
||||||
|
}
|
||||||
|
case "click":
|
||||||
|
case "contextmenu":
|
||||||
|
case "doubleclick":
|
||||||
|
case "dblclick":
|
||||||
case "mousedown":
|
case "mousedown":
|
||||||
case "mouseenter":
|
case "mouseenter":
|
||||||
case "mouseleave":
|
case "mouseleave":
|
||||||
|
@ -542,38 +579,7 @@ function serialize_event(event) {
|
||||||
case "mouseout":
|
case "mouseout":
|
||||||
case "mouseover":
|
case "mouseover":
|
||||||
case "mouseup": {
|
case "mouseup": {
|
||||||
const {
|
return get_mouse_data(event);
|
||||||
altKey,
|
|
||||||
button,
|
|
||||||
buttons,
|
|
||||||
clientX,
|
|
||||||
clientY,
|
|
||||||
ctrlKey,
|
|
||||||
metaKey,
|
|
||||||
offsetX,
|
|
||||||
offsetY,
|
|
||||||
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,
|
|
||||||
offset_x: offsetX,
|
|
||||||
offset_y: offsetY,
|
|
||||||
page_x: pageX,
|
|
||||||
page_y: pageY,
|
|
||||||
screen_x: screenX,
|
|
||||||
screen_y: screenY,
|
|
||||||
shift_key: shiftKey,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
case "pointerdown":
|
case "pointerdown":
|
||||||
case "pointermove":
|
case "pointermove":
|
||||||
|
|
|
@ -26,6 +26,7 @@ serde_json = "1.0.91"
|
||||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
|
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
|
||||||
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" }
|
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" }
|
||||||
dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" }
|
dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" }
|
||||||
|
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
||||||
|
|
||||||
# warp
|
# warp
|
||||||
warp = { version = "0.3.3", optional = true }
|
warp = { version = "0.3.3", optional = true }
|
||||||
|
@ -51,8 +52,9 @@ salvo = { version = "0.37.7", features = ["affix", "ws"] }
|
||||||
tower = "0.4.13"
|
tower = "0.4.13"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["hot-reload"]
|
||||||
# actix = ["actix-files", "actix-web", "actix-ws"]
|
# actix = ["actix-files", "actix-web", "actix-ws"]
|
||||||
|
hot-reload = ["dioxus-hot-reload"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
|
|
|
@ -46,7 +46,7 @@ async fn main() {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
println!("Listening on http://{}", addr);
|
println!("Listening on http://{addr}");
|
||||||
|
|
||||||
axum::Server::bind(&addr.to_string().parse().unwrap())
|
axum::Server::bind(&addr.to_string().parse().unwrap())
|
||||||
.serve(app.into_make_service())
|
.serve(app.into_make_service())
|
||||||
|
|
|
@ -103,6 +103,15 @@ pub async fn run<T>(
|
||||||
where
|
where
|
||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
{
|
{
|
||||||
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
|
let mut hot_reload_rx = {
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
dioxus_hot_reload::connect(move |template| {
|
||||||
|
let _ = tx.send(template);
|
||||||
|
});
|
||||||
|
rx
|
||||||
|
};
|
||||||
|
|
||||||
let mut vdom = VirtualDom::new_with_props(app, props);
|
let mut vdom = VirtualDom::new_with_props(app, props);
|
||||||
|
|
||||||
// todo: use an efficient binary packed format for this
|
// todo: use an efficient binary packed format for this
|
||||||
|
@ -122,6 +131,11 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
|
let hot_reload_wait = hot_reload_rx.recv();
|
||||||
|
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
|
||||||
|
let hot_reload_wait = std::future::pending();
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
// poll any futures or suspense
|
// poll any futures or suspense
|
||||||
_ = vdom.wait_for_work() => {}
|
_ = vdom.wait_for_work() => {}
|
||||||
|
@ -142,6 +156,19 @@ where
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msg = hot_reload_wait => {
|
||||||
|
if let Some(msg) = msg {
|
||||||
|
match msg{
|
||||||
|
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(new_template) => {
|
||||||
|
vdom.replace_template(new_template);
|
||||||
|
}
|
||||||
|
dioxus_hot_reload::HotReloadMsg::Shutdown => {
|
||||||
|
std::process::exit(0);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let edits = vdom
|
let edits = vdom
|
||||||
|
|
|
@ -316,7 +316,7 @@ fn creation() {
|
||||||
|
|
||||||
tree.add_child(parent_id, child_id);
|
tree.add_child(parent_id, child_id);
|
||||||
|
|
||||||
println!("Tree: {:#?}", tree);
|
println!("Tree: {tree:#?}");
|
||||||
assert_eq!(tree.size(), 2);
|
assert_eq!(tree.size(), 2);
|
||||||
assert_eq!(tree.height(parent_id), Some(0));
|
assert_eq!(tree.height(parent_id), Some(0));
|
||||||
assert_eq!(tree.height(child_id), Some(1));
|
assert_eq!(tree.height(child_id), Some(1));
|
||||||
|
@ -346,7 +346,7 @@ fn insertion() {
|
||||||
let after = after.id();
|
let after = after.id();
|
||||||
tree.insert_after(child, after);
|
tree.insert_after(child, after);
|
||||||
|
|
||||||
println!("Tree: {:#?}", tree);
|
println!("Tree: {tree:#?}");
|
||||||
assert_eq!(tree.size(), 4);
|
assert_eq!(tree.size(), 4);
|
||||||
assert_eq!(tree.height(parent), Some(0));
|
assert_eq!(tree.height(parent), Some(0));
|
||||||
assert_eq!(tree.height(child), Some(1));
|
assert_eq!(tree.height(child), Some(1));
|
||||||
|
@ -381,7 +381,7 @@ fn deletion() {
|
||||||
let after = after.id();
|
let after = after.id();
|
||||||
tree.insert_after(child, after);
|
tree.insert_after(child, after);
|
||||||
|
|
||||||
println!("Tree: {:#?}", tree);
|
println!("Tree: {tree:#?}");
|
||||||
assert_eq!(tree.size(), 4);
|
assert_eq!(tree.size(), 4);
|
||||||
assert_eq!(tree.height(parent), Some(0));
|
assert_eq!(tree.height(parent), Some(0));
|
||||||
assert_eq!(tree.height(child), Some(1));
|
assert_eq!(tree.height(child), Some(1));
|
||||||
|
@ -399,7 +399,7 @@ fn deletion() {
|
||||||
|
|
||||||
tree.remove(child);
|
tree.remove(child);
|
||||||
|
|
||||||
println!("Tree: {:#?}", tree);
|
println!("Tree: {tree:#?}");
|
||||||
assert_eq!(tree.size(), 3);
|
assert_eq!(tree.size(), 3);
|
||||||
assert_eq!(tree.height(parent), Some(0));
|
assert_eq!(tree.height(parent), Some(0));
|
||||||
assert_eq!(tree.height(before), Some(1));
|
assert_eq!(tree.height(before), Some(1));
|
||||||
|
@ -415,7 +415,7 @@ fn deletion() {
|
||||||
|
|
||||||
tree.remove(before);
|
tree.remove(before);
|
||||||
|
|
||||||
println!("Tree: {:#?}", tree);
|
println!("Tree: {tree:#?}");
|
||||||
assert_eq!(tree.size(), 2);
|
assert_eq!(tree.size(), 2);
|
||||||
assert_eq!(tree.height(parent), Some(0));
|
assert_eq!(tree.height(parent), Some(0));
|
||||||
assert_eq!(tree.height(after), Some(1));
|
assert_eq!(tree.height(after), Some(1));
|
||||||
|
@ -428,7 +428,7 @@ fn deletion() {
|
||||||
|
|
||||||
tree.remove(after);
|
tree.remove(after);
|
||||||
|
|
||||||
println!("Tree: {:#?}", tree);
|
println!("Tree: {tree:#?}");
|
||||||
assert_eq!(tree.size(), 1);
|
assert_eq!(tree.size(), 1);
|
||||||
assert_eq!(tree.height(parent), Some(0));
|
assert_eq!(tree.height(parent), Some(0));
|
||||||
assert_eq!(tree.children_ids(parent).unwrap(), &[]);
|
assert_eq!(tree.children_ids(parent).unwrap(), &[]);
|
||||||
|
|
|
@ -361,7 +361,7 @@ fn persist_removes() {
|
||||||
let mut rdom: RealDom = RealDom::new(Box::new([]));
|
let mut rdom: RealDom = RealDom::new(Box::new([]));
|
||||||
|
|
||||||
let build = vdom.rebuild();
|
let build = vdom.rebuild();
|
||||||
println!("{:#?}", build);
|
println!("{build:#?}");
|
||||||
let _to_update = rdom.apply_mutations(build);
|
let _to_update = rdom.apply_mutations(build);
|
||||||
|
|
||||||
// this will end on the node that is removed
|
// this will end on the node that is removed
|
||||||
|
@ -393,7 +393,7 @@ fn persist_removes() {
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId(0));
|
vdom.mark_dirty(ScopeId(0));
|
||||||
let update = vdom.render_immediate();
|
let update = vdom.render_immediate();
|
||||||
println!("{:#?}", update);
|
println!("{update:#?}");
|
||||||
iter1.prune(&update, &rdom);
|
iter1.prune(&update, &rdom);
|
||||||
iter2.prune(&update, &rdom);
|
iter2.prune(&update, &rdom);
|
||||||
let _to_update = rdom.apply_mutations(update);
|
let _to_update = rdom.apply_mutations(update);
|
||||||
|
|
365
packages/native-core/tests/fuzzing.rs
Normal file
365
packages/native-core/tests/fuzzing.rs
Normal file
|
@ -0,0 +1,365 @@
|
||||||
|
use dioxus::prelude::Props;
|
||||||
|
use dioxus_core::*;
|
||||||
|
use dioxus_native_core::{
|
||||||
|
node_ref::{AttributeMask, NodeView},
|
||||||
|
real_dom::RealDom,
|
||||||
|
state::{ParentDepState, State},
|
||||||
|
NodeMask, SendAnyMap,
|
||||||
|
};
|
||||||
|
use dioxus_native_core_macro::{sorted_str_slice, State};
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
fn random_ns() -> Option<&'static str> {
|
||||||
|
let namespace = rand::random::<u8>() % 2;
|
||||||
|
match namespace {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(Box::leak(
|
||||||
|
format!("ns{}", rand::random::<usize>()).into_boxed_str(),
|
||||||
|
)),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_random_attribute(attr_idx: &mut usize) -> TemplateAttribute<'static> {
|
||||||
|
match rand::random::<u8>() % 2 {
|
||||||
|
0 => TemplateAttribute::Static {
|
||||||
|
name: Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
|
||||||
|
value: Box::leak(format!("value{}", rand::random::<usize>()).into_boxed_str()),
|
||||||
|
namespace: random_ns(),
|
||||||
|
},
|
||||||
|
1 => TemplateAttribute::Dynamic {
|
||||||
|
id: {
|
||||||
|
let old_idx = *attr_idx;
|
||||||
|
*attr_idx += 1;
|
||||||
|
old_idx
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_random_template_node(
|
||||||
|
dynamic_node_types: &mut Vec<DynamicNodeType>,
|
||||||
|
template_idx: &mut usize,
|
||||||
|
attr_idx: &mut usize,
|
||||||
|
depth: usize,
|
||||||
|
) -> TemplateNode<'static> {
|
||||||
|
match rand::random::<u8>() % 4 {
|
||||||
|
0 => {
|
||||||
|
let attrs = {
|
||||||
|
let attrs: Vec<_> = (0..(rand::random::<usize>() % 10))
|
||||||
|
.map(|_| create_random_attribute(attr_idx))
|
||||||
|
.collect();
|
||||||
|
Box::leak(attrs.into_boxed_slice())
|
||||||
|
};
|
||||||
|
TemplateNode::Element {
|
||||||
|
tag: Box::leak(format!("tag{}", rand::random::<usize>()).into_boxed_str()),
|
||||||
|
namespace: random_ns(),
|
||||||
|
attrs,
|
||||||
|
children: {
|
||||||
|
if depth > 4 {
|
||||||
|
&[]
|
||||||
|
} else {
|
||||||
|
let children: Vec<_> = (0..(rand::random::<usize>() % 3))
|
||||||
|
.map(|_| {
|
||||||
|
create_random_template_node(
|
||||||
|
dynamic_node_types,
|
||||||
|
template_idx,
|
||||||
|
attr_idx,
|
||||||
|
depth + 1,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Box::leak(children.into_boxed_slice())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 => TemplateNode::Text {
|
||||||
|
text: Box::leak(format!("{}", rand::random::<usize>()).into_boxed_str()),
|
||||||
|
},
|
||||||
|
2 => TemplateNode::DynamicText {
|
||||||
|
id: {
|
||||||
|
let old_idx = *template_idx;
|
||||||
|
*template_idx += 1;
|
||||||
|
dynamic_node_types.push(DynamicNodeType::Text);
|
||||||
|
old_idx
|
||||||
|
},
|
||||||
|
},
|
||||||
|
3 => TemplateNode::Dynamic {
|
||||||
|
id: {
|
||||||
|
let old_idx = *template_idx;
|
||||||
|
*template_idx += 1;
|
||||||
|
dynamic_node_types.push(DynamicNodeType::Other);
|
||||||
|
old_idx
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_paths(
|
||||||
|
node: &TemplateNode<'static>,
|
||||||
|
current_path: &[u8],
|
||||||
|
node_paths: &mut Vec<Vec<u8>>,
|
||||||
|
attr_paths: &mut Vec<Vec<u8>>,
|
||||||
|
) {
|
||||||
|
match node {
|
||||||
|
TemplateNode::Element {
|
||||||
|
children, attrs, ..
|
||||||
|
} => {
|
||||||
|
for attr in *attrs {
|
||||||
|
match attr {
|
||||||
|
TemplateAttribute::Static { .. } => {}
|
||||||
|
TemplateAttribute::Dynamic { .. } => {
|
||||||
|
attr_paths.push(current_path.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i, child) in children.iter().enumerate() {
|
||||||
|
let mut current_path = current_path.to_vec();
|
||||||
|
current_path.push(i as u8);
|
||||||
|
generate_paths(child, ¤t_path, node_paths, attr_paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TemplateNode::Text { .. } => {}
|
||||||
|
TemplateNode::DynamicText { .. } => {
|
||||||
|
node_paths.push(current_path.to_vec());
|
||||||
|
}
|
||||||
|
TemplateNode::Dynamic { .. } => {
|
||||||
|
node_paths.push(current_path.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DynamicNodeType {
|
||||||
|
Text,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_random_template(name: &'static str) -> (Template<'static>, Vec<DynamicNodeType>) {
|
||||||
|
let mut dynamic_node_type = Vec::new();
|
||||||
|
let mut template_idx = 0;
|
||||||
|
let mut attr_idx = 0;
|
||||||
|
let roots = (0..(1 + rand::random::<usize>() % 5))
|
||||||
|
.map(|_| {
|
||||||
|
create_random_template_node(&mut dynamic_node_type, &mut template_idx, &mut attr_idx, 0)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert!(!roots.is_empty());
|
||||||
|
let roots = Box::leak(roots.into_boxed_slice());
|
||||||
|
let mut node_paths = Vec::new();
|
||||||
|
let mut attr_paths = Vec::new();
|
||||||
|
for (i, root) in roots.iter().enumerate() {
|
||||||
|
generate_paths(root, &[i as u8], &mut node_paths, &mut attr_paths);
|
||||||
|
}
|
||||||
|
let node_paths = Box::leak(
|
||||||
|
node_paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| &*Box::leak(v.into_boxed_slice()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_boxed_slice(),
|
||||||
|
);
|
||||||
|
let attr_paths = Box::leak(
|
||||||
|
attr_paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| &*Box::leak(v.into_boxed_slice()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_boxed_slice(),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
Template {
|
||||||
|
name,
|
||||||
|
roots,
|
||||||
|
node_paths,
|
||||||
|
attr_paths,
|
||||||
|
},
|
||||||
|
dynamic_node_type,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
|
||||||
|
let range = if depth > 3 { 1 } else { 3 };
|
||||||
|
match rand::random::<u8>() % range {
|
||||||
|
0 => DynamicNode::Placeholder(Default::default()),
|
||||||
|
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
|
||||||
|
key: None,
|
||||||
|
parent: Default::default(),
|
||||||
|
template: Cell::new(Template {
|
||||||
|
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
|
||||||
|
roots: &[TemplateNode::Dynamic { id: 0 }],
|
||||||
|
node_paths: &[&[0]],
|
||||||
|
attr_paths: &[],
|
||||||
|
}),
|
||||||
|
root_ids: Default::default(),
|
||||||
|
dynamic_nodes: cx.bump().alloc([cx.component(
|
||||||
|
create_random_element,
|
||||||
|
DepthProps { depth, root: false },
|
||||||
|
"create_random_element",
|
||||||
|
)]),
|
||||||
|
dynamic_attrs: &[],
|
||||||
|
})),
|
||||||
|
2 => cx.component(
|
||||||
|
create_random_element,
|
||||||
|
DepthProps { depth, root: false },
|
||||||
|
"create_random_element",
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
|
||||||
|
let value = match rand::random::<u8>() % 6 {
|
||||||
|
0 => AttributeValue::Text(Box::leak(
|
||||||
|
format!("{}", rand::random::<usize>()).into_boxed_str(),
|
||||||
|
)),
|
||||||
|
1 => AttributeValue::Float(rand::random()),
|
||||||
|
2 => AttributeValue::Int(rand::random()),
|
||||||
|
3 => AttributeValue::Bool(rand::random()),
|
||||||
|
4 => cx.any_value(rand::random::<usize>()),
|
||||||
|
5 => AttributeValue::None,
|
||||||
|
// Listener(RefCell<Option<ListenerCb<'a>>>),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
Attribute {
|
||||||
|
name: Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
|
||||||
|
value,
|
||||||
|
namespace: random_ns(),
|
||||||
|
mounted_element: Default::default(),
|
||||||
|
volatile: rand::random(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static mut TEMPLATE_COUNT: usize = 0;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Props)]
|
||||||
|
struct DepthProps {
|
||||||
|
depth: usize,
|
||||||
|
root: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||||
|
cx.needs_update();
|
||||||
|
let range = if cx.props.root { 2 } else { 3 };
|
||||||
|
let node = match rand::random::<usize>() % range {
|
||||||
|
0 | 1 => {
|
||||||
|
let (template, dynamic_node_types) = create_random_template(Box::leak(
|
||||||
|
format!(
|
||||||
|
"{}{}",
|
||||||
|
concat!(file!(), ":", line!(), ":", column!(), ":"),
|
||||||
|
{
|
||||||
|
unsafe {
|
||||||
|
let old = TEMPLATE_COUNT;
|
||||||
|
TEMPLATE_COUNT += 1;
|
||||||
|
old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.into_boxed_str(),
|
||||||
|
));
|
||||||
|
println!("{template:#?}");
|
||||||
|
let node = VNode {
|
||||||
|
key: None,
|
||||||
|
parent: None,
|
||||||
|
template: Cell::new(template),
|
||||||
|
root_ids: Default::default(),
|
||||||
|
dynamic_nodes: {
|
||||||
|
let dynamic_nodes: Vec<_> = dynamic_node_types
|
||||||
|
.iter()
|
||||||
|
.map(|ty| match ty {
|
||||||
|
DynamicNodeType::Text => DynamicNode::Text(VText {
|
||||||
|
value: Box::leak(
|
||||||
|
format!("{}", rand::random::<usize>()).into_boxed_str(),
|
||||||
|
),
|
||||||
|
id: Default::default(),
|
||||||
|
}),
|
||||||
|
DynamicNodeType::Other => {
|
||||||
|
create_random_dynamic_node(cx, cx.props.depth + 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
cx.bump().alloc(dynamic_nodes)
|
||||||
|
},
|
||||||
|
dynamic_attrs: cx.bump().alloc(
|
||||||
|
(0..template.attr_paths.len())
|
||||||
|
.map(|_| create_random_dynamic_attr(cx))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
Some(node)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
println!("{node:#?}");
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct BlablaState {}
|
||||||
|
|
||||||
|
/// Font style are inherited by default if not specified otherwise by some of the supported attributes.
|
||||||
|
impl ParentDepState for BlablaState {
|
||||||
|
type Ctx = ();
|
||||||
|
type DepState = (Self,);
|
||||||
|
|
||||||
|
const NODE_MASK: NodeMask =
|
||||||
|
NodeMask::new_with_attrs(AttributeMask::Static(&sorted_str_slice!(["blabla",])));
|
||||||
|
|
||||||
|
fn reduce(&mut self, _node: NodeView, _parent: Option<(&Self,)>, _ctx: &Self::Ctx) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, State, Default, Debug)]
|
||||||
|
pub struct NodeState {
|
||||||
|
#[parent_dep_state(blabla)]
|
||||||
|
blabla: BlablaState,
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for panics when creating random nodes and templates
|
||||||
|
#[test]
|
||||||
|
fn create() {
|
||||||
|
for _ in 0..100 {
|
||||||
|
let mut vdom = VirtualDom::new_with_props(
|
||||||
|
create_random_element,
|
||||||
|
DepthProps {
|
||||||
|
depth: 0,
|
||||||
|
root: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let mutations = vdom.rebuild();
|
||||||
|
let mut rdom: RealDom<NodeState> = RealDom::new();
|
||||||
|
let (to_update, _diff) = rdom.apply_mutations(mutations);
|
||||||
|
|
||||||
|
let ctx = SendAnyMap::new();
|
||||||
|
rdom.update_state(to_update, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for panics when diffing random nodes
|
||||||
|
// This test will change the template every render which is not very realistic, but it helps stress the system
|
||||||
|
#[test]
|
||||||
|
fn diff() {
|
||||||
|
for _ in 0..10 {
|
||||||
|
let mut vdom = VirtualDom::new_with_props(
|
||||||
|
create_random_element,
|
||||||
|
DepthProps {
|
||||||
|
depth: 0,
|
||||||
|
root: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let mutations = vdom.rebuild();
|
||||||
|
let mut rdom: RealDom<NodeState> = RealDom::new();
|
||||||
|
let (to_update, _diff) = rdom.apply_mutations(mutations);
|
||||||
|
|
||||||
|
let ctx = SendAnyMap::new();
|
||||||
|
rdom.update_state(to_update, ctx);
|
||||||
|
for _ in 0..10 {
|
||||||
|
let mutations = vdom.render_immediate();
|
||||||
|
let (to_update, _diff) = rdom.apply_mutations(mutations);
|
||||||
|
|
||||||
|
let ctx = SendAnyMap::new();
|
||||||
|
rdom.update_state(to_update, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,5 +20,5 @@ fn main() {
|
||||||
|
|
||||||
let out = dioxus_autofmt::write_block_out(body).unwrap();
|
let out = dioxus_autofmt::write_block_out(body).unwrap();
|
||||||
|
|
||||||
println!("{}", out);
|
println!("{out}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,7 +193,7 @@ pub struct FormattedSegment {
|
||||||
impl ToTokens for FormattedSegment {
|
impl ToTokens for FormattedSegment {
|
||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
let (fmt, seg) = (&self.format_args, &self.segment);
|
let (fmt, seg) = (&self.format_args, &self.segment);
|
||||||
let fmt = format!("{{0:{}}}", fmt);
|
let fmt = format!("{{0:{fmt}}}");
|
||||||
tokens.append_all(quote! {
|
tokens.append_all(quote! {
|
||||||
format_args!(#fmt, #seg)
|
format_args!(#fmt, #seg)
|
||||||
});
|
});
|
||||||
|
|
|
@ -60,11 +60,11 @@ impl StringCache {
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
cur_path.push(root_idx);
|
cur_path.push(root_idx);
|
||||||
write!(chain, "<{}", tag)?;
|
write!(chain, "<{tag}")?;
|
||||||
for attr in *attrs {
|
for attr in *attrs {
|
||||||
match attr {
|
match attr {
|
||||||
TemplateAttribute::Static { name, value, .. } => {
|
TemplateAttribute::Static { name, value, .. } => {
|
||||||
write!(chain, " {}=\"{}\"", name, value)?;
|
write!(chain, " {name}=\"{value}\"")?;
|
||||||
}
|
}
|
||||||
TemplateAttribute::Dynamic { id: index } => {
|
TemplateAttribute::Dynamic { id: index } => {
|
||||||
chain.segments.push(Segment::Attr(*index))
|
chain.segments.push(Segment::Attr(*index))
|
||||||
|
@ -78,11 +78,11 @@ impl StringCache {
|
||||||
for child in *children {
|
for child in *children {
|
||||||
Self::recurse(child, cur_path, root_idx, chain)?;
|
Self::recurse(child, cur_path, root_idx, chain)?;
|
||||||
}
|
}
|
||||||
write!(chain, "</{}>", tag)?;
|
write!(chain, "</{tag}>")?;
|
||||||
}
|
}
|
||||||
cur_path.pop();
|
cur_path.pop();
|
||||||
}
|
}
|
||||||
TemplateNode::Text { text } => write!(chain, "{}", text)?,
|
TemplateNode::Text { text } => write!(chain, "{text}")?,
|
||||||
TemplateNode::Dynamic { id: idx } | TemplateNode::DynamicText { id: idx } => {
|
TemplateNode::Dynamic { id: idx } | TemplateNode::DynamicText { id: idx } => {
|
||||||
chain.segments.push(Segment::Node(*idx))
|
chain.segments.push(Segment::Node(*idx))
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Segment::PreRendered(contents) => write!(buf, "{}", contents)?,
|
Segment::PreRendered(contents) => write!(buf, "{contents}")?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,10 @@ license = "MIT/Apache-2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus = { path = "../dioxus", version = "^0.3.0" }
|
dioxus = { path = "../dioxus", version = "^0.3.0" }
|
||||||
dioxus-core = { path = "../core", version = "^0.3.0" }
|
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||||
dioxus-html = { path = "../html", version = "^0.3.0" }
|
dioxus-html = { path = "../html", version = "^0.3.0" }
|
||||||
dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
|
dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
|
||||||
|
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
||||||
|
|
||||||
tui = "0.17.0"
|
tui = "0.17.0"
|
||||||
crossterm = "0.23.0"
|
crossterm = "0.23.0"
|
||||||
|
@ -37,3 +38,7 @@ criterion = "0.3.5"
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "update"
|
name = "update"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["hot-reload"]
|
||||||
|
hot-reload = ["dioxus-hot-reload"]
|
||||||
|
|
|
@ -27,6 +27,7 @@ use std::{io, time::Duration};
|
||||||
use style_attributes::StyleModifier;
|
use style_attributes::StyleModifier;
|
||||||
use taffy::Taffy;
|
use taffy::Taffy;
|
||||||
pub use taffy::{geometry::Point, prelude::*};
|
pub use taffy::{geometry::Point, prelude::*};
|
||||||
|
use tokio::{select, sync::mpsc::unbounded_channel};
|
||||||
use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
|
use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
@ -153,6 +154,15 @@ fn render_vdom(
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()?
|
.build()?
|
||||||
.block_on(async {
|
.block_on(async {
|
||||||
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
|
let mut hot_reload_rx = {
|
||||||
|
let (hot_reload_tx, hot_reload_rx) =
|
||||||
|
unbounded_channel::<dioxus_hot_reload::HotReloadMsg>();
|
||||||
|
dioxus_hot_reload::connect(move |msg| {
|
||||||
|
let _ = hot_reload_tx.send(msg);
|
||||||
|
});
|
||||||
|
hot_reload_rx
|
||||||
|
};
|
||||||
let mut terminal = (!cfg.headless).then(|| {
|
let mut terminal = (!cfg.headless).then(|| {
|
||||||
enable_raw_mode().unwrap();
|
enable_raw_mode().unwrap();
|
||||||
let mut stdout = std::io::stdout();
|
let mut stdout = std::io::stdout();
|
||||||
|
@ -238,16 +248,21 @@ fn render_vdom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use futures::future::{select, Either};
|
let mut hot_reload_msg = None;
|
||||||
{
|
{
|
||||||
let wait = vdom.wait_for_work();
|
let wait = vdom.wait_for_work();
|
||||||
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
|
let hot_reload_wait = hot_reload_rx.recv();
|
||||||
|
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
|
||||||
|
let hot_reload_wait = std::future::pending();
|
||||||
|
|
||||||
pin_mut!(wait);
|
pin_mut!(wait);
|
||||||
|
|
||||||
match select(wait, event_reciever.next()).await {
|
select! {
|
||||||
Either::Left((_a, _b)) => {
|
_ = wait => {
|
||||||
//
|
|
||||||
}
|
},
|
||||||
Either::Right((evt, _o)) => {
|
evt = event_reciever.next() => {
|
||||||
match evt.as_ref().unwrap() {
|
match evt.as_ref().unwrap() {
|
||||||
InputEvent::UserInput(event) => match event {
|
InputEvent::UserInput(event) => match event {
|
||||||
TermEvent::Key(key) => {
|
TermEvent::Key(key) => {
|
||||||
|
@ -267,6 +282,21 @@ fn render_vdom(
|
||||||
if let InputEvent::UserInput(evt) = evt.unwrap() {
|
if let InputEvent::UserInput(evt) = evt.unwrap() {
|
||||||
register_event(evt);
|
register_event(evt);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Some(msg) = hot_reload_wait => {
|
||||||
|
hot_reload_msg = Some(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have a new template, replace the old one
|
||||||
|
if let Some(msg) = hot_reload_msg {
|
||||||
|
match msg {
|
||||||
|
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
|
||||||
|
vdom.replace_template(template);
|
||||||
|
}
|
||||||
|
dioxus_hot_reload::HotReloadMsg::Shutdown => {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ fn rehydrates() {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.body()
|
.body()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_inner_html(&format!("<div id='main'>{}</div>", out));
|
.set_inner_html(&format!("<div id='main'>{out}</div>"));
|
||||||
|
|
||||||
dioxus_web::launch_cfg(app, Config::new().hydrate(true));
|
dioxus_web::launch_cfg(app, Config::new().hydrate(true));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue