mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-26 22:20:19 +00:00
Merge pull request #46 from DioxusLabs/jk/release
examples: upgrade to new version of dioxus core.
This commit is contained in:
commit
dcfeca17df
69 changed files with 1311 additions and 466 deletions
|
@ -2,10 +2,10 @@
|
|||
|
||||
![dioxuslogo](./images/dioxuslogo_full.png)
|
||||
|
||||
**Dioxus** is a framework and ecosystem for building fast, scalable, and robust user interfaces with the Rust programming language. This guide will help you get up-and-running with Dioxus running on the Web, Desktop, Mobile, and more.
|
||||
**Dioxus** is a framework and ecosystem for building fast, scalable, and robust user interfaces with the Rust programming language. This guide will help you get started with Dioxus running on the Web, Desktop, Mobile, and more.
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
fn App(cx: Scope) -> Element {
|
||||
let mut count = use_state(&cx, || 0);
|
||||
|
||||
cx.render(rsx!(
|
||||
|
@ -18,7 +18,7 @@ fn App(cx: Context, props: &()) -> Element {
|
|||
|
||||
In general, Dioxus and React share many functional similarities. If this guide is lacking in any general concept or an error message is confusing, React's documentation might be more helpful. We are dedicated to providing a *familiar* toolkit for UI in Rust, so we've chosen to follow in the footsteps of popular UI frameworks (React, Redux, etc). If you know React, then you already know Dioxus. If you don't know either, this guide will still help you!
|
||||
|
||||
> This is introduction book! For advanced topics, check out the [Reference Guide]() instead.
|
||||
> This is introduction book! For advanced topics, check out the [Reference](https://dioxuslabs.com/reference) instead.
|
||||
|
||||
## Multiplatform
|
||||
|
||||
|
@ -76,7 +76,7 @@ Examples:
|
|||
- [Bluetooth scanner]()
|
||||
- [Device Viewer]()
|
||||
|
||||
[![FileExplorerExample](https://github.com/DioxusLabs/file-explorer-example/raw/master/image.png)](https://github.com/dioxusLabs/file-explorer/)
|
||||
[![File ExplorerExample](https://github.com/DioxusLabs/file-explorer-example/raw/master/image.png)](https://github.com/dioxusLabs/file-explorer/)
|
||||
|
||||
### Mobile Support
|
||||
---
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
- [Intro to Elements](concepts/vnodes.md)
|
||||
- [Intro to Components](concepts/components.md)
|
||||
- [Reusing, Importing, and Exporting Components](concepts/exporting_components.md)
|
||||
- [Passing children and attributes](concepts/component_children.md)
|
||||
- [Conditional Rendering](concepts/conditional_rendering.md)
|
||||
- [Lists](concepts/lists.md)
|
||||
- [Adding Interactivity](concepts/interactivity.md)
|
||||
- [Event handlers](concepts/event_handlers.md)
|
||||
- [Hooks and Internal State](concepts/hooks.md)
|
||||
- [Fundamental Hooks and `use_hook`](concepts/usestate.md)
|
||||
- [User Input and Controlled Components](concepts/errorhandling.md)
|
||||
- [Event handlers](concepts/event_handlers.md)
|
||||
- [User Input and Controlled Components](concepts/user_input.md)
|
||||
- [Lifecycle, updates, and effects](concepts/lifecycles.md)
|
||||
|
||||
<!-- Responding to Events
|
||||
|
|
|
@ -49,9 +49,9 @@ let mut state = use_state(&cx, || "red");
|
|||
|
||||
cx.render(rsx!(
|
||||
Container {
|
||||
Light { color: "red", enabled: format_args!("{}", state == "red") }
|
||||
Light { color: "yellow", enabled: format_args!("{}", state == "yellow") }
|
||||
Light { color: "green", enabled: format_args!("{}", state == "green") }
|
||||
Light { color: "red", enabled: state == "red", }
|
||||
Light { color: "yellow", enabled: state == "yellow", }
|
||||
Light { color: "green", enabled: state == "green", }
|
||||
|
||||
onclick: move |_| {
|
||||
state.set(match *state {
|
||||
|
|
|
@ -20,7 +20,7 @@ some state or event occurs outside of the component. For instance, the `use_cont
|
|||
particular context.
|
||||
|
||||
```rust
|
||||
fn use_context<I>(cx: Context<T>) -> I {
|
||||
fn use_context<I>(cx: Scope<T>) -> I {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ Concurrent mode provides a mechanism for building efficient asynchronous compone
|
|||
To make a component asynchronous, simply change its function signature to async.
|
||||
|
||||
```rust
|
||||
fn Example(cx: Context<()>) -> Vnode {
|
||||
fn Example(cx: Scope) -> Vnode {
|
||||
rsx!{ <div> "Hello world!" </div> }
|
||||
}
|
||||
```
|
||||
|
@ -13,7 +13,7 @@ fn Example(cx: Context<()>) -> Vnode {
|
|||
becomes
|
||||
|
||||
```rust
|
||||
async fn Example(cx: Context<()>) -> Vnode {
|
||||
async fn Example(cx: Scope) -> Vnode {
|
||||
rsx!{ <div> "Hello world!" </div> }
|
||||
}
|
||||
```
|
||||
|
@ -21,7 +21,7 @@ async fn Example(cx: Context<()>) -> Vnode {
|
|||
Now, logic in components can be awaited to delay updates of the component and its children. Like so:
|
||||
|
||||
```rust
|
||||
async fn Example(cx: Context<()>) -> Vnode {
|
||||
async fn Example(cx: Scope) -> Vnode {
|
||||
let name = fetch_name().await;
|
||||
rsx!{ <div> "Hello {name}" </div> }
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ Instead, we suggest using hooks and future combinators that can safely utilize t
|
|||
As part of our Dioxus hooks crate, we provide a data loader hook which pauses a component until its async dependencies are ready. This caches requests, reruns the fetch if dependencies have changed, and provides the option to render something else while the component is loading.
|
||||
|
||||
```rust
|
||||
async fn ExampleLoader(cx: Context<()>) -> Vnode {
|
||||
async fn ExampleLoader(cx: Scope) -> Vnode {
|
||||
/*
|
||||
Fetch, pause the component from rendering at all.
|
||||
|
||||
|
@ -50,8 +50,8 @@ async fn ExampleLoader(cx: Context<()>) -> Vnode {
|
|||
This API stores the result on the Context object, so the loaded data is taken as reference.
|
||||
*/
|
||||
let name: &Result<SomeStructure> = use_fetch_data("http://example.com/json", ())
|
||||
.place_holder(|cx, props|rsx!{<div> "loading..." </div>})
|
||||
.delayed_place_holder(1000, |cx, props|rsx!{ <div> "still loading..." </div>})
|
||||
.place_holder(|cx| rsx!{<div> "loading..." </div>})
|
||||
.delayed_place_holder(1000, |cx| rsx!{ <div> "still loading..." </div>})
|
||||
.await;
|
||||
|
||||
match name {
|
||||
|
@ -62,7 +62,7 @@ async fn ExampleLoader(cx: Context<()>) -> Vnode {
|
|||
```
|
||||
|
||||
```rust
|
||||
async fn Example(cx: Context<()>) -> DomTree {
|
||||
async fn Example(cx: Scope) -> DomTree {
|
||||
// Diff this set between the last set
|
||||
// Check if we have any outstanding tasks?
|
||||
//
|
||||
|
|
|
@ -21,17 +21,19 @@ fn test() -> DomTree {
|
|||
}
|
||||
}
|
||||
|
||||
static TestComponent: Component<()> = |cx, props|html!{<div>"Hello world"</div>};
|
||||
static TestComponent: Component<()> = |cx| html!{<div>"Hello world"</div>};
|
||||
|
||||
static TestComponent: Component<()> = |cx, props|{
|
||||
static TestComponent: Component<()> = |cx|{
|
||||
let g = "BLAH";
|
||||
html! {
|
||||
<div> "Hello world" </div>
|
||||
}
|
||||
};
|
||||
|
||||
#[functional_component]
|
||||
static TestComponent: Component<{ name: String }> = |cx, props|html! { <div> "Hello {name}" </div> };
|
||||
#[inline_props]
|
||||
fn test_component(cx: Scope, name: String) -> Element {
|
||||
rsx!(cx, "Hello, {name}")
|
||||
}
|
||||
```
|
||||
|
||||
## Why this behavior?
|
||||
|
@ -41,7 +43,7 @@ static TestComponent: Component<{ name: String }> = |cx, props|html! { <div> "He
|
|||
Take a component likes this:
|
||||
|
||||
```rust
|
||||
fn test(cx: Context<()>) -> DomTree {
|
||||
fn test(cx: Scope) -> DomTree {
|
||||
let Bundle { alpha, beta, gamma } = use_context::<SomeContext>(cx);
|
||||
html! {
|
||||
<div>
|
||||
|
|
|
@ -11,7 +11,7 @@ By default, Dioxus will only try to diff subtrees of components with dynamic con
|
|||
Your component today might look something like this:
|
||||
|
||||
```rust
|
||||
fn Comp(cx: Context<()>) -> DomTree {
|
||||
fn Comp(cx: Scope) -> DomTree {
|
||||
let (title, set_title) = use_state(&cx, || "Title".to_string());
|
||||
cx.render(rsx!{
|
||||
input {
|
||||
|
@ -25,7 +25,7 @@ fn Comp(cx: Context<()>) -> DomTree {
|
|||
This component is fairly straightforward - the input updates its own value on every change. However, every call to set_title will re-render the component. If we add a large list, then every time we update the title input, Dioxus will need to diff the entire list, over, and over, and over. This is **a lot** of wasted clock-cycles!
|
||||
|
||||
```rust
|
||||
fn Comp(cx: Context<()>) -> DomTree {
|
||||
fn Comp(cx: Scope) -> DomTree {
|
||||
let (title, set_title) = use_state(&cx, || "Title".to_string());
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
|
@ -48,7 +48,7 @@ Many experienced React developers will just say "this is bad design" - but we co
|
|||
We can use signals to generate a two-way binding between data and the input box. Our text input is now just a two-line component!
|
||||
|
||||
```rust
|
||||
fn Comp(cx: Context<()>) -> DomTree {
|
||||
fn Comp(cx: Scope) -> DomTree {
|
||||
let mut title = use_signal(&cx, || String::from("Title"));
|
||||
cx.render(rsx!(input { value: title }))
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ fn Comp(cx: Context<()>) -> DomTree {
|
|||
For a slightly more interesting example, this component calculates the sum between two numbers, but totally skips the diffing process.
|
||||
|
||||
```rust
|
||||
fn Calculator(cx: Context<()>) -> DomTree {
|
||||
fn Calculator(cx: Scope) -> DomTree {
|
||||
let mut a = use_signal(&cx, || 0);
|
||||
let mut b = use_signal(&cx, || 0);
|
||||
let mut c = a + b;
|
||||
|
@ -96,7 +96,7 @@ Sometimes you want a signal to propagate across your app, either through far-awa
|
|||
|
||||
```rust
|
||||
const TITLE: Atom<String> = || "".to_string();
|
||||
const Provider: Component<()> = |cx, props|{
|
||||
const Provider: Component<()> = |cx|{
|
||||
let title = use_signal(&cx, &TITLE);
|
||||
rsx!(cx, input { value: title })
|
||||
};
|
||||
|
@ -105,7 +105,7 @@ const Provider: Component<()> = |cx, props|{
|
|||
If we use the `TITLE` atom in another component, we can cause updates to flow between components without calling render or diffing either component trees:
|
||||
|
||||
```rust
|
||||
const Receiver: Component<()> = |cx, props|{
|
||||
const Receiver: Component<()> = |cx|{
|
||||
let title = use_signal(&cx, &TITLE);
|
||||
log::info!("This will only be called once!");
|
||||
rsx!(cx,
|
||||
|
@ -132,7 +132,7 @@ Dioxus automatically understands how to use your signals when mixed with iterato
|
|||
|
||||
```rust
|
||||
const DICT: AtomFamily<String, String> = |_| {};
|
||||
const List: Component<()> = |cx, props|{
|
||||
const List: Component<()> = |cx|{
|
||||
let dict = use_signal(&cx, &DICT);
|
||||
cx.render(rsx!(
|
||||
ul {
|
||||
|
|
|
@ -30,7 +30,7 @@ Due to their importance in the hierarchy, Components - not nodes - are treated a
|
|||
|
||||
```rust
|
||||
|
||||
fn Subtree<P>(cx: Context, props: P) -> DomTree {
|
||||
fn Subtree<P>(cx: Scope<P>) -> DomTree {
|
||||
|
||||
}
|
||||
|
||||
|
|
217
docs/guide/src/concepts/component_children.md
Normal file
217
docs/guide/src/concepts/component_children.md
Normal file
|
@ -0,0 +1,217 @@
|
|||
# Passing children and attributes
|
||||
|
||||
Often times, you'll want to wrap some important functionality *around* your state, not directly nested *inside* another component. In these cases, you'll want to pass elements and attributes into a component and let the component place them appropriately.
|
||||
|
||||
In this chapter, you'll learn about:
|
||||
- Passing elements into components
|
||||
- Passing attributes into components
|
||||
|
||||
|
||||
## The use case
|
||||
|
||||
Let's say you're building a user interface and want to make some part of it clickable to another website. You would normally start with the HTML `<a>` tag, like so:
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
a {
|
||||
href: "https://google.com"
|
||||
"Link to google"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
But, what if we wanted to style our `<a>` tag? Or wrap it with some helper icon? We could abstract our RSX into its own component:
|
||||
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct ClickableProps<'a> {
|
||||
href: &'a str,
|
||||
title: &'a str
|
||||
}
|
||||
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
href: "{cx.props.href}"
|
||||
"{cx.props.title}"
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
And then use it in our code like so:
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
clickable(
|
||||
href: "https://google.com"
|
||||
title: "Link to Google"
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
Let's say we don't just want the text to be clickable, but we want another element, like an image, to be clickable. How do we implement that?
|
||||
|
||||
## Passing children
|
||||
|
||||
If we want to pass an image into our component, we can just adjust our props and component to allow any `Element`.
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct ClickableProps<'a> {
|
||||
href: &'a str,
|
||||
body: Element<'a>
|
||||
}
|
||||
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
href: "{cx.props.href}"
|
||||
{&cx.props.body}
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Then, at the call site, we can render some nodes and pass them in:
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
clickable(
|
||||
href: "https://google.com"
|
||||
body: cx.render(rsx!(
|
||||
img { src: "https://www.google.com/logos/doodles/2021/seasonal-holidays-2021-6753651837109324-6752733080595603-cst.gif" }
|
||||
))
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
This pattern can become tedious in some instances, so Dioxus actually performs an implicit conversion of any `rsx` calls inside components into `Elements` at the `children` field. This means you must explicitly declare if a component can take children.
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct ClickableProps<'a> {
|
||||
href: &'a str,
|
||||
children: Element<'a>
|
||||
}
|
||||
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
href: "{cx.props.href}"
|
||||
{&cx.props.children}
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
And to call `clickable`:
|
||||
```rust
|
||||
rsx!(
|
||||
clickable(
|
||||
href: "https://google.com"
|
||||
img { src: "https://www.google.com/logos/doodles/2021/seasonal-holidays-2021-6753651837109324-6752733080595603-cst.gif" }
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
> Note: Passing children into components will break any memoization due to the associated lifetime.
|
||||
|
||||
While technically allowed, it's an antipattern to pass children more than once in a component and will probably break your app significantly.
|
||||
|
||||
However, because the `Element` is transparently a `VNode`, we can actually match on it to extract the nodes themselves, in case we are expecting a specific format:
|
||||
|
||||
```rust
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
match cx.props.children {
|
||||
Some(VNode::Text(text)) => {
|
||||
// ...
|
||||
}
|
||||
_ => {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Passing attributes
|
||||
|
||||
In the cases where you need to pass arbitrary element properties into a component - say to add more functionality to the `<a>` tag, Dioxus will accept any quoted fields. This is similar to adding arbitrary fields to regular elements using quotes.
|
||||
|
||||
```rust
|
||||
|
||||
rsx!(
|
||||
clickable(
|
||||
"class": "blue-button",
|
||||
"style": "background: red;"
|
||||
)
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
For a component to accept these attributes, you must add an `attributes` field to your component's properties. We can use the spread syntax to add these attributes to whatever nodes are in our component.
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct ClickableProps<'a> {
|
||||
attributes: Attributes<'a>
|
||||
}
|
||||
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
..{cx.props.attributes},
|
||||
"Any link, anywhere"
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
The quoted escapes are a great way to make your components more flexible.
|
||||
|
||||
|
||||
## Passing handlers
|
||||
|
||||
Dioxus also provides some implicit conversions from listener attributes into an `EventHandler` for any field on components that starts with `on`. IE `onclick`, `onhover`, etc. For properties, we want to define our `on` fields as an event handler:
|
||||
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct ClickableProps<'a> {
|
||||
onclick: EventHandler<'a, MouseEvent>
|
||||
}
|
||||
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
onclick: move |evt| cx.props.onclick.call(evt)
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Then, we can attach a listener at the call site:
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
clickable(
|
||||
onclick: move |_| log::info!("Clicked"),
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
Currently, Dioxus does not support an arbitrary amount of listeners - they must be strongly typed in `Properties`. If you need this use case, you can pass in an element with these listeners, or dip down into the `NodeFactory` API.
|
||||
|
||||
|
||||
## Wrapping up
|
||||
|
||||
In this chapter, we learned:
|
||||
- How to pass arbitrary nodes through the tree
|
||||
- How the `children` field works on component properties
|
||||
- How the `attributes` field works on component properties
|
||||
- How to convert `listeners` into `EventHandlers` for components
|
||||
- How to extend any node with custom attributes and children
|
||||
|
||||
Next chapter, we'll talk about conditionally rendering parts of your user interface.
|
||||
|
|
@ -84,22 +84,22 @@ struct PostProps {
|
|||
|
||||
And our render function:
|
||||
```rust
|
||||
fn Post(cx: Context, props: &PostProps) -> Element {
|
||||
fn Post(cx: Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
div { class: "post-container"
|
||||
VoteButton {
|
||||
score: props.score,
|
||||
score: cx.props.score,
|
||||
}
|
||||
TitleCard {
|
||||
title: props.title,
|
||||
url: props.url,
|
||||
title: cx.props.title,
|
||||
url: cx.props.url,
|
||||
}
|
||||
MetaCard {
|
||||
original_poster: props.original_poster,
|
||||
post_time: props.post_time,
|
||||
original_poster: cx.props.original_poster,
|
||||
post_time: cx.props.post_time,
|
||||
}
|
||||
ActionCard {
|
||||
post_id: props.id
|
||||
post_id: cx.props.id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -110,7 +110,7 @@ When declaring a component in `rsx!`, we can pass in properties using the tradit
|
|||
|
||||
Let's take a look at the `VoteButton` component. For now, we won't include any interactivity - just the rendering the score and buttons to the screen.
|
||||
|
||||
Most of your Components will look exactly like this: a Props struct and a render function. Every component must take a tuple of `Context` and `&Props` and return an `Element`.
|
||||
Most of your Components will look exactly like this: a Props struct and a render function. Every component must take a tuple of `Scope` and `&Props` and return an `Element`.
|
||||
|
||||
As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`.
|
||||
|
||||
|
@ -120,7 +120,7 @@ struct VoteButtonProps {
|
|||
score: i32
|
||||
}
|
||||
|
||||
fn VoteButton(cx: Context, props: &VoteButtonProps) -> Element {
|
||||
fn VoteButton(cx: Scope<VoteButtonProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
div { class: "votebutton"
|
||||
div { class: "arrow up" }
|
||||
|
@ -145,7 +145,7 @@ struct TitleCardProps<'a> {
|
|||
title: &'a str,
|
||||
}
|
||||
|
||||
fn TitleCard(cx: Context, props: &TitleCardProps) -> Element {
|
||||
fn TitleCard(cx: Scope<TitleCardProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{cx.props.title}" }
|
||||
})
|
||||
|
@ -156,9 +156,9 @@ For users of React: Dioxus knows *not* to memoize components that borrow propert
|
|||
|
||||
This means that during the render process, a newer version of `TitleCardProps` will never be compared with a previous version, saving some clock cycles.
|
||||
|
||||
## The `Context` object
|
||||
## The `Scope` object
|
||||
|
||||
Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Context` parameter in the component declaration.
|
||||
Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration.
|
||||
|
||||
Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state?
|
||||
|
||||
|
@ -171,10 +171,10 @@ function Component(props) {
|
|||
```
|
||||
|
||||
|
||||
Because Dioxus needs to work with the rules of Rust it uses the `Context` object to maintain some internal bookkeeping. That's what the `Context` object is: a place for the component to store state, manage listeners, and allocate elements. Advanced users of Dioxus will want to learn how to properly leverage the `Context` object to build robust and performant extensions for Dioxus.
|
||||
Because Dioxus needs to work with the rules of Rust it uses the `Scope` object to maintain some internal bookkeeping. That's what the `Scope` object is: a place for the component to store state, manage listeners, and allocate elements. Advanced users of Dioxus will want to learn how to properly leverage the `Scope` object to build robust and performant extensions for Dioxus.
|
||||
|
||||
```rust
|
||||
fn Post(cx: Context, props: &PostProps) -> Element {
|
||||
fn Post(cx: Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!("hello"))
|
||||
}
|
||||
```
|
||||
|
@ -186,6 +186,6 @@ For more references on components, make sure to check out:
|
|||
|
||||
- [Components in depth]()
|
||||
- [Lifecycles]()
|
||||
- [The Context object]()
|
||||
- [The Scope object]()
|
||||
- [Optional Prop fields]()
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ struct AppProps {
|
|||
Now that we have a "logged_in" flag accessible in our props, we can render two different screens:
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &AppProps) -> Element {
|
||||
fn App(cx: Scope<AppProps>) -> Element {
|
||||
if props.logged_in {
|
||||
cx.render(rsx!{
|
||||
DashboardScreen {}
|
||||
|
@ -48,7 +48,7 @@ Rust provides us algebraic datatypes: enums that can contain values. Using the `
|
|||
For instance, we could run a function that returns a Result:
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
fn App(cx: Scope)-> Element {
|
||||
match get_name() {
|
||||
Ok(name) => cx.render(rsx!( "Hello, {name}!" )),
|
||||
Err(err) => cx.render(rsx!( "Sorry, I don't know your name, because an error occurred: {err}" )),
|
||||
|
@ -58,7 +58,7 @@ fn App(cx: Context, props: &())-> Element {
|
|||
|
||||
We can even match against values:
|
||||
```rust
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
fn App(cx: Scope)-> Element {
|
||||
match get_name() {
|
||||
"jack" => cx.render(rsx!( "Hey Jack, how's Diane?" )),
|
||||
"diane" => cx.render(rsx!( "Hey Diana, how's Jack?" )),
|
||||
|
@ -72,7 +72,7 @@ Do note: the `rsx!` macro returns a `Closure`, an anonymous function that has a
|
|||
To make patterns like these less verbose, the `rsx!` macro accepts an optional first argument on which it will call `render`. Our previous component can be shortened with this alternative syntax:
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
fn App(cx: Scope)-> Element {
|
||||
match get_name() {
|
||||
"jack" => rsx!(cx, "Hey Jack, how's Diane?" ),
|
||||
"diane" => rsx!(cx, "Hey Diana, how's Jack?" ),
|
||||
|
@ -83,13 +83,13 @@ fn App(cx: Context, props: &())-> Element {
|
|||
|
||||
This syntax even enables us to write a one-line component:
|
||||
```rust
|
||||
static App: Component<()> = |cx, props| rsx!(cx, "hello world!");
|
||||
static App: Component<()> = |cx| rsx!(cx, "hello world!");
|
||||
```
|
||||
|
||||
Alternatively, for match statements, we can just return the builder itself and pass it into a final, single call to `cx.render`:
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
fn App(cx: Scope)-> Element {
|
||||
let greeting = match get_name() {
|
||||
"jack" => rsx!("Hey Jack, how's Diane?" ),
|
||||
"diane" => rsx!("Hey Diana, how's Jack?" ),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Event handlers
|
||||
# Handling Events
|
||||
|
||||
In the overview for this section, we mentioned how we can modify the state of our component by responding to user-generated events inside of event listeners.
|
||||
|
||||
|
|
|
@ -1 +1,188 @@
|
|||
# Hooks and Internal State
|
||||
|
||||
In the [Adding Interactivity](./interactivity.md) section, we briefly covered the concept of hooks and state stored internal to components.
|
||||
|
||||
In this section, we'll dive a bit deeper into hooks, exploring both the theory and mechanics.
|
||||
|
||||
|
||||
|
||||
## Theory of Hooks
|
||||
|
||||
Over the past several decades, computer scientists and engineers have long sought the "right way" of designing user interfaces. With each new programming language, novel features are unlocked that change the paradigm in which user interfaces are coded.
|
||||
|
||||
Generally, a number of patterns have emerged, each with their own strengths and tradeoffs.
|
||||
|
||||
Broadly, there are two types of GUI structures:
|
||||
|
||||
- Immediate GUIs: re-render the entire screen on every update
|
||||
- Retained GUIs: only re-render the portion of the screen that changed
|
||||
|
||||
Typically, immediate-mode GUIs are simpler to write but can slow down as more features, like styling, are added.
|
||||
|
||||
Many GUIs today - including Dioxus - are written in *Retained mode* - your code changes the data of the user interface but the renderer is responsible for actually drawing to the screen. In these cases, our GUI's state sticks around as the UI is rendered.
|
||||
|
||||
Dioxus, following in the footsteps of React, provides a "Reactive" model for you to design your UI. This model emphasizes one-way data flow and encapsulation. Essentially, your UI code should be as predictable as possible.
|
||||
|
||||
## Mechanics of Hooks
|
||||
In order to have state stick around between renders, Dioxus provides the `hook` through the `use_hook` API. This gives us a mutable reference to data returned from the initialization function.
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string(), |hook| hook);
|
||||
|
||||
//
|
||||
}
|
||||
```
|
||||
|
||||
We can even modify this value directly from an event handler:
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string(), |hook| hook);
|
||||
|
||||
cx.render(rsx!(
|
||||
button {
|
||||
onclick: move |_| name.push_str(".."),
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Mechanically, each call to `use_hook` provides us with `&mut T` for a new value.
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string(), |hook| hook);
|
||||
let age: &mut u32 = cx.use_hook(|| 10, |hook| hook);
|
||||
let friends: &mut Vec<String> = cx.use_hook(|| vec!["Jane Doe".to_string()], |hook| hook);
|
||||
|
||||
//
|
||||
}
|
||||
```
|
||||
|
||||
Internally, Dioxus is creating a list of hook values with each call to `use_hook` advancing the index of the list to return the next value.
|
||||
|
||||
Our internal HookList would look something like:
|
||||
|
||||
```rust
|
||||
[
|
||||
Hook<String>,
|
||||
Hook<u32>,
|
||||
Hook<String>,
|
||||
]
|
||||
```
|
||||
|
||||
This is why hooks called out of order will fail - if we try to downcast a `Hook<String>` to `Hook<u32>`, Dioxus has no choice but to panic. We do provide a `try_use_hook` but you should never need that in practice.
|
||||
|
||||
This pattern might seem strange at first, but it can be a significant upgrade over structs as blobs of state, which tend to be difficult to use in [Rust given the ownership system](https://rust-lang.github.io/rfcs/2229-capture-disjoint-fields.html).
|
||||
|
||||
## Building new Hooks
|
||||
|
||||
However, most hooks you'll interact with *don't* return an `&mut T` since this is not very useful in a real-world situation.
|
||||
|
||||
Consider when we try to pass our `&mut String` into two different handlers:
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string(), |hook| hook);
|
||||
|
||||
cx.render(rsx!(
|
||||
button { onclick: move |_| name.push_str("yes"), }
|
||||
button { onclick: move |_| name.push_str("no"), }
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Rust will not allow this to compile! We cannot `Copy` unique mutable references - they are, by definition, unique. However, we *can* reborrow our `&mut T` as an `&T` which are non-unique references and share those between handlers:
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &String = &*cx.use_hook(|| "John Doe".to_string());
|
||||
|
||||
cx.render(rsx!(
|
||||
button { onclick: move |_| log::info!("{}", name), }
|
||||
button { onclick: move |_| log::info!("{}", name), }
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
So, for any custom hook we want to design, we need to enable mutation through [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html) - IE move to runtime [borrow checking](https://doc.rust-lang.org/1.8.0/book/references-and-borrowing.html). We might incur a tiny runtime cost for each time we grab a new value from the hook, but this cost is extremely minimal.
|
||||
|
||||
This example uses the `Cell` type to let us replace the value through interior mutability. `Cell` has practically zero overhead, but is slightly more limited that its `RefCell` cousin.
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &Cell<&'static str> = cx.use_hook(|| "John Doe", |hook| hook);
|
||||
|
||||
cx.render(rsx!(
|
||||
button { onclick: move |_| name.set("John"), }
|
||||
button { onclick: move |_| name.set("Jane"), }
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
## Driving state updates through hooks
|
||||
|
||||
Hooks like `use_state` and `use_ref` wrap this runtime borrow checking in a type that *does* implement `Copy`. Additionally, they also mark the component as "dirty" whenever a new value has been set. This way, whenever `use_state` has a new value `set`, the component knows to update.
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name = use_state(&cx, || "Jack");
|
||||
|
||||
cx.render(rsx!(
|
||||
"Hello, {name}"
|
||||
button { onclick: move |_| name.set("John"), }
|
||||
button { onclick: move |_| name.set("Jane"), }
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Internally, our `set` function looks something like this:
|
||||
|
||||
```rust
|
||||
impl<'a, T> UseState<'a, T> {
|
||||
fn set(&self, new: T) {
|
||||
// Replace the value in the cell
|
||||
self.value.set(new);
|
||||
|
||||
// Mark our component as dirty
|
||||
self.cx.needs_update();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Most hooks we provide implement `Deref` on their values since they are essentially smart pointers. To access the underlying value, you'll often need to use the deref operator:
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name = use_state(&cx, || "Jack");
|
||||
|
||||
match *name {
|
||||
"Jack" => {}
|
||||
"Jill" => {}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// ..
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Hooks provided by the `Dioxus-Hooks` package
|
||||
|
||||
By default, we bundle a handful of hooks in the Dioxus-Hooks package. Feel free to click on each hook to view its definition and associated documentation.
|
||||
|
||||
- [use_state](https://docs.rs/dioxus_hooks/use_state) - store state with ergonomic updates
|
||||
- [use_ref](https://docs.rs/dioxus_hooks/use_ref) - store non-clone state with a refcell
|
||||
- [use_future](https://docs.rs/dioxus_hooks/use_future) - store a future to be polled after initialization
|
||||
- [use_coroutine](https://docs.rs/dioxus_hooks/use_coroutine) - store a future that can be stopped/started/communicated with
|
||||
- [use_noderef](https://docs.rs/dioxus_hooks/use_noderef) - store a handle to the native element
|
||||
- [use_callback](https://docs.rs/dioxus_hooks/use_callback) - store a callback that implements PartialEq for memoization
|
||||
- [use_provide_context](https://docs.rs/dioxus_hooks/use_provide_context) - expose state to descendent components
|
||||
- [use_context](https://docs.rs/dioxus_hooks/use_context) - consume state provided by `use_provide_context`
|
||||
|
||||
## Wrapping up
|
||||
|
||||
In this chapter, we learned about the mechanics and intricacies of storing state inside a component.
|
||||
|
||||
In the next chapter, we'll cover event listeners in similar depth, and how to combine the two to build interactive components.
|
||||
|
|
|
@ -35,10 +35,10 @@ fn main() {
|
|||
When Dioxus renders your app, it will pass an immutable reference of `PostProps` to your `Post` component. Here, you can pass the state down into children.
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &PostProps) -> Element {
|
||||
fn App(cx: Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
Title { title: &props.title }
|
||||
Score { score: &props.score }
|
||||
Title { title: &cx.props.title }
|
||||
Score { score: &cx.props.score }
|
||||
// etc
|
||||
})
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ Instead, you'll want to store state internally in your components and let *that*
|
|||
The most common hook you'll use for storing state is `use_state`. `use_state` provides a slot for some data that allows you to read and update the value without accidentally mutating it.
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
fn App(cx: Scope)-> Element {
|
||||
let post = use_state(&cx, || {
|
||||
PostData {
|
||||
id: Uuid::new_v4(),
|
||||
|
@ -111,7 +111,7 @@ When responding to user-triggered events, we'll want to "listen" for an event on
|
|||
For example, let's say we provide a button to generate a new post. Whenever the user clicks the button, they get a new post. To achieve this functionality, we'll want to attach a function to the `on_click` method of `button`. Whenever the button is clicked, our function will run, and we'll get new Post data to work with.
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
fn App(cx: Scope)-> Element {
|
||||
let post = use_state(&cx, || PostData::new());
|
||||
|
||||
cx.render(rsx!{
|
||||
|
@ -128,24 +128,27 @@ We'll dive much deeper into event listeners later.
|
|||
|
||||
### Updating state asynchronously
|
||||
|
||||
We can also update our state outside of event listeners with `tasks`. `Tasks` are asynchronous blocks of our component that have the ability to cleanly interact with values, hooks, and other data in the component. `Tasks` are one-shot - if they don't complete before the component is updated by another `.set` call, then they won't finish.
|
||||
We can also update our state outside of event listeners with `coroutines`. `Coroutines` are asynchronous blocks of our component that have the ability to cleanly interact with values, hooks, and other data in the component. Since coroutines stick around between renders, the data in them must be valid for the `'static` lifetime. We must explicitly declare which values our task will rely on to avoid the `stale props` problem common in React.
|
||||
|
||||
We can use tasks in our components to build a tiny stopwatch that ticks every second.
|
||||
|
||||
```rust
|
||||
> Note: The `use_future` hook will start our coroutine immediately. The `use_coroutine` hook provides more flexibility over starting and stopping futures on the fly.
|
||||
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
```rust
|
||||
fn App(cx: Scope)-> Element {
|
||||
let mut sec_elapsed = use_state(&cx, || 0);
|
||||
|
||||
cx.spawn_task(async move {
|
||||
TimeoutFuture::from_ms(1000).await;
|
||||
sec_elapsed += 1;
|
||||
use_future(&cx, || {
|
||||
let mut sec_elapsed = sec_elapsed.to_owned();
|
||||
async move {
|
||||
loop {
|
||||
TimeoutFuture::from_ms(1000).await;
|
||||
sec_elapsed += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.render(rsx!{
|
||||
div { "Current stopwatch time: {sec_elapsed}" }
|
||||
})
|
||||
|
||||
rsx!(cx, div { "Current stopwatch time: {sec_elapsed}" })
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -74,9 +74,9 @@ struct PostListProps<'a> {
|
|||
Next, we're going to define our component:
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &PostList) -> Element {
|
||||
fn App(cx: Scope<PostList?) -> Element {
|
||||
// First, we create a new iterator by mapping the post array
|
||||
let posts = props.posts.iter().map(|post| rsx!{
|
||||
let posts = cx.props.posts.iter().map(|post| rsx!{
|
||||
Post {
|
||||
title: post.title,
|
||||
age: post.age,
|
||||
|
|
|
@ -138,7 +138,7 @@ However, with lists, Dioxus does not exactly know how to determine which element
|
|||
In these cases, it is vitally important to specify a "key" alongside the element. Keys should be persistent between renders.
|
||||
|
||||
```rust
|
||||
fn render_list(cx: Context, items: HashMap<String, Todo>) -> DomTree {
|
||||
fn render_list(cx: Scope, items: HashMap<String, Todo>) -> DomTree {
|
||||
rsx!(cx, ul {
|
||||
{items.iter().map(|key, item| {
|
||||
li {
|
||||
|
@ -219,7 +219,7 @@ cx.render(rsx!{
|
|||
{true.and_then(|f| rsx!( h1 {"Conditional Rendering"} ))}
|
||||
|
||||
// Child nodes
|
||||
{cx.children()}
|
||||
{cx.props.children}
|
||||
|
||||
// Any expression that is `IntoVNode`
|
||||
{expr}
|
||||
|
|
1
docs/guide/src/concepts/user_input.md
Normal file
1
docs/guide/src/concepts/user_input.md
Normal file
|
@ -0,0 +1 @@
|
|||
# User Input and Controlled Components
|
|
@ -11,7 +11,6 @@ In this chapter, we'll cover:
|
|||
Because Dioxus is mostly used with HTML/CSS renderers, the default Element "collection" is HTML. Provided the `html` feature is not disabled, we can declare Elements using the `rsx!` macro:
|
||||
|
||||
```rust
|
||||
#use dioxus::prelude::*;
|
||||
rsx!(
|
||||
div {}
|
||||
)
|
||||
|
@ -19,7 +18,6 @@ rsx!(
|
|||
As you might expect, we can render this call using Dioxus-SSR to produce valid HTML:
|
||||
|
||||
```rust
|
||||
#use dioxus::prelude::*;
|
||||
dioxus::ssr::render_lazy(rsx!(
|
||||
div {}
|
||||
))
|
||||
|
|
|
@ -95,7 +95,7 @@ fn main() {
|
|||
dioxus::desktop::launch(App);
|
||||
}
|
||||
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
fn App(cx: Scope) -> Element {
|
||||
cx.render(rsx! (
|
||||
div { "Hello, world!" }
|
||||
))
|
||||
|
@ -125,17 +125,17 @@ fn main() {
|
|||
Finally, our app. Every component in Dioxus is a function that takes in `Context` and `Props` and returns an `Element`.
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
fn App(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div { "Hello, world!" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Writing `fn App(cx: Context, props: &()) -> Element {` might become tedious. Rust will also let you write functions as static closures, but these types of Components cannot have props that borrow data.
|
||||
Writing `fn App(cx: Scope) -> Element {` might become tedious. Rust will also let you write functions as static closures, but these types of Components cannot have props that borrow data.
|
||||
|
||||
```rust
|
||||
static App: Component<()> = |cx, props| cx.render(rsx!(div { "Hello, world!" }));
|
||||
static App: Component<()> = |cx| cx.render(rsx!(div { "Hello, world!" }));
|
||||
```
|
||||
|
||||
### What is this `Context` object?
|
||||
|
|
|
@ -1 +1,8 @@
|
|||
# Contributors
|
||||
|
||||
All pull requests (including those made by a team member) must be approved by at least one other team member.
|
||||
Larger, more nuanced decisions about design, architecture, breaking changes, trade offs, etc are made by team consensus.
|
||||
|
||||
Contributors to this guide:
|
||||
- [jkelleyrtp](https://github.com/jkelleyrtp)
|
||||
- [alexkirsz](https://github.com/alexkirsz)
|
||||
|
|
|
@ -11,3 +11,13 @@ So far, we've covered the basics of Dioxus. We've talked about:
|
|||
|
||||
In this chapter, we'll build a real-world weather app that combines everything we've learned into a cute application that you can run locally. It'll let us monitor different locations simultaneously and periodically check for updates.
|
||||
|
||||
The app is running on the web [here]() but is also available:
|
||||
|
||||
- as a desktop app
|
||||
- a mobile app
|
||||
- and as an API endpoint here
|
||||
|
||||
![Weather App Image](static/weather_app.png)
|
||||
|
||||
|
||||
Let's get started! Head on to the next chapter where we create our app.
|
||||
|
|
|
@ -1 +1,55 @@
|
|||
# New app
|
||||
|
||||
To get started, let's create a new Rust project for our app.
|
||||
|
||||
```shell
|
||||
$ cargo new --bin weatherapp
|
||||
$ cd weatherapp
|
||||
```
|
||||
|
||||
Make sure our project builds by default
|
||||
|
||||
```shell
|
||||
$ cargo run
|
||||
|
||||
Compiling weatherapp v0.1.0
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.41s
|
||||
Running `target/debug/weatherapp`
|
||||
Hello, world!
|
||||
```
|
||||
|
||||
## Adding Dioxus Desktop as a dependency
|
||||
|
||||
We can either edit our Cargo.toml directly:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dioxus = { version = "*", features = ["desktop"]}
|
||||
```
|
||||
|
||||
or use `cargo-edit` to add it via the CLI:
|
||||
|
||||
```shell
|
||||
$ cargo add dioxus --features desktop
|
||||
```
|
||||
|
||||
## Setting up a hello world
|
||||
|
||||
Let's edit the project's `main.rs` and add the skeleton of
|
||||
|
||||
```rust
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx!(
|
||||
div { "hello world!" }
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Making sure things run
|
||||
|
|
|
@ -17,7 +17,7 @@ pub static App: Component<()> = |cx| {
|
|||
|
||||
let (async_count, dir) = (count.for_async(), *direction);
|
||||
|
||||
let (task, _) = use_coroutine(cx, move || async move {
|
||||
let task = use_coroutine(&cx, move || async move {
|
||||
loop {
|
||||
TimeoutFuture::new(250).await;
|
||||
*async_count.get_mut() += dir;
|
||||
|
|
|
@ -40,7 +40,7 @@ struct C1Props<'a> {
|
|||
}
|
||||
|
||||
fn Child1<'a>(cx: Scope<'a, C1Props<'a>>) -> Element {
|
||||
let (left, right) = props.text.split_once("=").unwrap();
|
||||
let (left, right) = cx.props.text.split_once("=").unwrap();
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
|
@ -58,7 +58,7 @@ struct C2Props<'a> {
|
|||
fn Child2<'a>(cx: Scope<'a, C2Props<'a>>) -> Element {
|
||||
cx.render(rsx! {
|
||||
Child3 {
|
||||
text: props.text
|
||||
text: cx.props.text
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -124,6 +124,6 @@ fn CalculatorKey<'a>(cx: Scope<'a, CalculatorKeyProps<'a>>) -> Element {
|
|||
rsx!(cx, button {
|
||||
class: "calculator-key {cx.props.name}"
|
||||
onclick: {cx.props.onclick}
|
||||
{&props.children}
|
||||
{&cx.props.children}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -58,15 +58,15 @@ static App: Component<()> = |cx| {
|
|||
#[derive(Props)]
|
||||
struct HorseyProps<'a> {
|
||||
pos: i32,
|
||||
children: ScopeChildren<'a>,
|
||||
children: Element<'a>,
|
||||
}
|
||||
|
||||
fn Horsey<'a>((cx, props): ScopeState<'a, HorseyProps<'a>>) -> Element {
|
||||
fn Horsey<'a>(cx: Scope<'a, HorseyProps<'a>>) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
button { "pause" }
|
||||
div {
|
||||
{&props.children}
|
||||
{&cx.props.children}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
simple_logger::init_with_level(log::Level::Debug);
|
||||
// simple_logger::init_with_level(log::Level::Debug);
|
||||
dioxus::desktop::launch_cfg(App, |c| {
|
||||
c.with_window(|w| {
|
||||
w.with_resizable(true).with_inner_size(
|
||||
|
|
|
@ -97,7 +97,7 @@ struct ActionButtonProps<'a> {
|
|||
|
||||
fn ActionButton<'a>(cx: Scope<'a, ActionButtonProps<'a>>) -> Element {
|
||||
rsx!(cx, div { class: "col-sm-6 smallpad"
|
||||
button { class:"btn btn-primary btn-block", r#type: "button", id: "{cx.props.id}", onclick: move |_| (props.onclick)(),
|
||||
button { class:"btn btn-primary btn-block", r#type: "button", id: "{cx.props.id}", onclick: move |_| (cx.props.onclick)(),
|
||||
"{cx.props.name}"
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(App);
|
||||
dioxus::desktop::launch(app);
|
||||
}
|
||||
|
||||
fn App((cx, props): ScopeState<()>) -> Element {
|
||||
fn app(cx: Scope<()>) -> Element {
|
||||
cx.render(rsx! (
|
||||
div { "Hello, world!" }
|
||||
))
|
||||
|
|
|
@ -14,9 +14,9 @@ use dioxus::ssr;
|
|||
|
||||
fn main() {
|
||||
let vdom = VirtualDom::new(App);
|
||||
let content = ssr::render_vdom(&vdom, |f| f.pre_render(true));
|
||||
let content = ssr::render_vdom_cfg(&vdom, |f| f.pre_render(true));
|
||||
|
||||
dioxus::desktop::launch(App, |c| c.with_prerendered(content));
|
||||
dioxus::desktop::launch_cfg(App, |c| c.with_prerendered(content));
|
||||
}
|
||||
|
||||
static App: Component<()> = |cx| {
|
||||
|
|
|
@ -33,11 +33,7 @@ fn main() {
|
|||
AppendChildren { many: 1 },
|
||||
];
|
||||
|
||||
dioxus_desktop::run(APP, (), |c| c.with_edits(edits));
|
||||
}
|
||||
let app: Component<()> = |cx| rsx!(cx, div { "some app" });
|
||||
|
||||
const APP: Component<()> = |(cx, _props)| {
|
||||
rsx!(cx, div {
|
||||
"some app"
|
||||
})
|
||||
};
|
||||
dioxus_desktop::run(app, (), |c| c.with_edits(edits));
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
//! the RefCell will panic and crash. You can use `try_get_mut` or `.modify` to avoid this problem, or just not hold two
|
||||
//! RefMuts at the same time.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use dioxus::desktop::wry::application::dpi::LogicalSize;
|
||||
use dioxus::events::*;
|
||||
use dioxus::prelude::*;
|
||||
|
@ -73,16 +75,16 @@ static App: Component<()> = |cx| {
|
|||
#[derive(Props)]
|
||||
struct CalculatorKeyProps<'a> {
|
||||
name: &'static str,
|
||||
onclick: &'a dyn Fn(MouseEvent),
|
||||
children: ScopeChildren<'a>,
|
||||
onclick: &'a dyn Fn(Arc<MouseEvent>),
|
||||
children: Element<'a>,
|
||||
}
|
||||
|
||||
fn CalculatorKey<'a>((cx, props): ScopeState<'a, CalculatorKeyProps<'a>>) -> Element<'a> {
|
||||
fn CalculatorKey<'a>(cx: Scope<'a, CalculatorKeyProps<'a>>) -> Element {
|
||||
cx.render(rsx! {
|
||||
button {
|
||||
class: "calculator-key {cx.props.name}"
|
||||
onclick: {cx.props.onclick}
|
||||
{&props.children}
|
||||
onclick: move |e| (cx.props.onclick)(e)
|
||||
{&cx.props.children}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -168,7 +170,7 @@ impl Calculator {
|
|||
self.cur_val = self.display_value.parse::<f64>().unwrap();
|
||||
self.waiting_for_operand = true;
|
||||
}
|
||||
fn handle_keydown(&mut self, evt: KeyboardEvent) {
|
||||
fn handle_keydown(&mut self, evt: Arc<KeyboardEvent>) {
|
||||
match evt.key_code {
|
||||
KeyCode::Backspace => self.backspace(),
|
||||
KeyCode::Num0 => self.input_digit(0),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus::router::Router;
|
||||
use dioxus_router::{use_router, Link};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum Route {
|
||||
|
@ -24,7 +24,7 @@ pub enum Route {
|
|||
}
|
||||
|
||||
static App: Component<()> = |cx| {
|
||||
let route = use_router(cx, Route::parse)?;
|
||||
let route = use_router(&cx, Route::parse);
|
||||
|
||||
cx.render(rsx! {
|
||||
nav {
|
||||
|
@ -61,26 +61,4 @@ impl Route {
|
|||
}
|
||||
}
|
||||
|
||||
impl Routable for Route {
|
||||
fn from_path(path: &str, params: &std::collections::HashMap<&str, &str>) -> Option<Self> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn to_path(&self) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn routes() -> Vec<&'static str> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn not_found_route() -> Option<Self> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn recognize(pathname: &str) -> Option<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
//! - Allow top-level fragments
|
||||
//!
|
||||
fn main() {
|
||||
dioxus::desktop::launch(Example);
|
||||
dioxus::desktop::launch(EXAMPLE);
|
||||
}
|
||||
|
||||
/// When trying to return "nothing" to Dioxus, you'll need to specify the type parameter or Rust will be sad.
|
||||
|
@ -49,7 +49,7 @@ const NONE_ELEMENT: Option<()> = None;
|
|||
use baller::Baller;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub static Example: Component<()> = |cx| {
|
||||
pub static EXAMPLE: Component<()> = |cx| {
|
||||
let formatting = "formatting!";
|
||||
let formatting_tuple = ("a", "b");
|
||||
let lazy_fmt = format_args!("lazily formatted text");
|
||||
|
@ -173,12 +173,12 @@ pub static Example: Component<()> = |cx| {
|
|||
Taller { a: "asd", div {"hello world!"} }
|
||||
|
||||
// helper functions
|
||||
{helper(cx, "hello world!")}
|
||||
{helper(&cx, "hello world!")}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
fn helper(cx: Scope, text: &str) -> Element {
|
||||
fn helper<'a>(cx: &'a ScopeState, text: &str) -> Element<'a> {
|
||||
rsx!(cx, p { "{text}" })
|
||||
}
|
||||
|
||||
|
@ -188,7 +188,7 @@ mod baller {
|
|||
pub struct BallerProps {}
|
||||
|
||||
/// This component totally balls
|
||||
pub fn Baller(_: ScopeState<BallerProps>) -> Element {
|
||||
pub fn Baller(_: Scope<BallerProps>) -> Element {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
@ -196,13 +196,11 @@ mod baller {
|
|||
#[derive(Props)]
|
||||
pub struct TallerProps<'a> {
|
||||
a: &'static str,
|
||||
|
||||
#[builder(default)]
|
||||
children: ScopeChildren<'a>,
|
||||
children: Element<'a>,
|
||||
}
|
||||
|
||||
/// This component is taller than most :)
|
||||
pub fn Taller<'a>(_: ScopeState<'a, TallerProps<'a>>) -> Element {
|
||||
pub fn Taller<'a>(_: Scope<'a, TallerProps<'a>>) -> Element {
|
||||
let b = true;
|
||||
todo!()
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
#![allow(non_upper_case_globals)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::ssr;
|
||||
|
||||
fn main() {
|
||||
let mut vdom = VirtualDom::new(App);
|
||||
vdom.rebuild_in_place().expect("Rebuilding failed");
|
||||
let mut vdom = VirtualDom::new(APP);
|
||||
let _ = vdom.rebuild();
|
||||
println!("{}", ssr::render_vdom(&vdom));
|
||||
}
|
||||
|
||||
static App: Component<()> = |cx| {
|
||||
static APP: Component<()> = |cx| {
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
h1 { "Title" }
|
||||
|
@ -17,10 +15,3 @@ static App: Component<()> = |cx| {
|
|||
}
|
||||
))
|
||||
};
|
||||
|
||||
struct MyProps<'a> {
|
||||
text: &'a str,
|
||||
}
|
||||
fn App2<'a>(cx: Scope<'a, MyProps<'a>>) -> Element {
|
||||
None
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use dioxus::prelude::*;
|
|||
|
||||
fn main() {
|
||||
use dioxus::desktop::wry::application::platform::macos::*;
|
||||
dioxus::desktop::launch(App, |c| {
|
||||
dioxus::desktop::launch_cfg(App, |c| {
|
||||
c.with_window(|w| {
|
||||
w.with_fullsize_content_view(true)
|
||||
.with_titlebar_buttons_hidden(false)
|
||||
|
|
|
@ -6,29 +6,24 @@ use std::time::Duration;
|
|||
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::desktop::launch(App);
|
||||
dioxus::desktop::launch(app);
|
||||
}
|
||||
|
||||
static App: Component<()> = |cx| {
|
||||
fn app(cx: Scope<()>) -> Element {
|
||||
let mut count = use_state(&cx, || 0);
|
||||
|
||||
cx.push_task(async move {
|
||||
cx.push_future(|| async move {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
println!("setting count");
|
||||
count += 1;
|
||||
// count.set(10);
|
||||
// *count += 1;
|
||||
// let c = count.get() + 1;
|
||||
// count.set(c);
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
// button {
|
||||
// onclick: move |_| count +=1 ,
|
||||
// "Click me!"
|
||||
// }
|
||||
button {
|
||||
onclick: move |_| count +=1 ,
|
||||
"Click me!"
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,9 +23,9 @@ pub struct TodoItem {
|
|||
|
||||
const STYLE: &str = include_str!("./assets/todomvc.css");
|
||||
const App: Component<()> = |cx| {
|
||||
let mut draft = use_state(&cx, || "".to_string());
|
||||
let mut todos = use_state(&cx, || HashMap::<u32, Rc<TodoItem>>::new());
|
||||
let mut filter = use_state(&cx, || FilterState::All);
|
||||
let draft = use_state(&cx, || "".to_string());
|
||||
let todos = use_state(&cx, || HashMap::<u32, Rc<TodoItem>>::new());
|
||||
let filter = use_state(&cx, || FilterState::All);
|
||||
|
||||
let todolist = todos
|
||||
.iter()
|
||||
|
@ -86,9 +86,9 @@ pub struct TodoEntryProps {
|
|||
}
|
||||
|
||||
pub fn TodoEntry(cx: Scope<TodoEntryProps>) -> Element {
|
||||
let mut is_editing = use_state(&cx, || false);
|
||||
let mut contents = use_state(&cx, || String::from(""));
|
||||
let todo = &props.todo;
|
||||
let is_editing = use_state(&cx, || false);
|
||||
let contents = use_state(&cx, || String::from(""));
|
||||
let todo = &cx.props.todo;
|
||||
|
||||
rsx!(cx, li {
|
||||
"{todo.id}"
|
||||
|
|
|
@ -45,8 +45,8 @@ struct RowProps {
|
|||
row_id: usize,
|
||||
label: Label,
|
||||
}
|
||||
fn Row((cx, props): ScopeState<RowProps>) -> Element {
|
||||
let [adj, col, noun] = props.label.0;
|
||||
fn Row(cx: Scope<RowProps>) -> Element {
|
||||
let [adj, col, noun] = cx.props.label.0;
|
||||
cx.render(rsx! {
|
||||
tr {
|
||||
td { class:"col-md-1", "{cx.props.row_id}" }
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
name = "dioxus-core-macro"
|
||||
version = "0.1.2"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "Core macro for Dioxus Virtual DOM"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
|
|
22
packages/core-macro/examples/inline_props.rs
Normal file
22
packages/core-macro/examples/inline_props.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use dioxus_core_macro::{inline_props, Props};
|
||||
|
||||
fn main() {}
|
||||
|
||||
type Element<'a> = ();
|
||||
|
||||
pub struct Scope<'a, T> {
|
||||
props: &'a T,
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
pub fn component(
|
||||
cx: Scope,
|
||||
chkk: String,
|
||||
chkk2: String,
|
||||
r: u32,
|
||||
cat: &'a str,
|
||||
drd: String,
|
||||
e: String,
|
||||
) -> Element {
|
||||
let r = chkk.len();
|
||||
}
|
107
packages/core-macro/src/inlineprops.rs
Normal file
107
packages/core-macro/src/inlineprops.rs
Normal file
|
@ -0,0 +1,107 @@
|
|||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
token, Block, FnArg, Generics, Ident, Pat, Result, ReturnType, Token, Visibility,
|
||||
};
|
||||
|
||||
pub struct InlinePropsBody {
|
||||
pub vis: syn::Visibility,
|
||||
pub fn_token: Token![fn],
|
||||
pub ident: Ident,
|
||||
pub cx_token: Box<Pat>,
|
||||
pub generics: Generics,
|
||||
pub paren_token: token::Paren,
|
||||
pub inputs: Punctuated<FnArg, Token![,]>,
|
||||
// pub fields: FieldsNamed,
|
||||
pub output: ReturnType,
|
||||
pub block: Box<Block>,
|
||||
}
|
||||
|
||||
/// The custom rusty variant of parsing rsx!
|
||||
impl Parse for InlinePropsBody {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let vis: Visibility = input.parse()?;
|
||||
|
||||
let fn_token = input.parse()?;
|
||||
let ident = input.parse()?;
|
||||
let generics = input.parse()?;
|
||||
|
||||
let content;
|
||||
let paren_token = syn::parenthesized!(content in input);
|
||||
|
||||
let first_arg: FnArg = content.parse()?;
|
||||
let cx_token = {
|
||||
match first_arg {
|
||||
FnArg::Receiver(_) => panic!("first argument must not be a reciver argument"),
|
||||
FnArg::Typed(f) => f.pat,
|
||||
}
|
||||
};
|
||||
|
||||
let _: Result<Token![,]> = content.parse();
|
||||
|
||||
let inputs = syn::punctuated::Punctuated::parse_terminated(&content)?;
|
||||
|
||||
let output = input.parse()?;
|
||||
|
||||
let block = input.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
vis,
|
||||
fn_token,
|
||||
ident,
|
||||
generics,
|
||||
paren_token,
|
||||
inputs,
|
||||
output,
|
||||
block,
|
||||
cx_token,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize the same way, regardless of flavor
|
||||
impl ToTokens for InlinePropsBody {
|
||||
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
|
||||
let Self {
|
||||
vis,
|
||||
ident,
|
||||
generics,
|
||||
inputs,
|
||||
output,
|
||||
block,
|
||||
cx_token,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let fields = inputs.iter().map(|f| {
|
||||
quote! { #vis #f }
|
||||
});
|
||||
|
||||
let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site());
|
||||
|
||||
let field_names = inputs.iter().filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => todo!(),
|
||||
FnArg::Typed(t) => Some(&t.pat),
|
||||
});
|
||||
|
||||
let modifiers = if generics.params.is_empty() {
|
||||
quote! { #[derive(Props, PartialEq)] }
|
||||
} else {
|
||||
quote! { #[derive(Props)] }
|
||||
};
|
||||
|
||||
out_tokens.append_all(quote! {
|
||||
#modifiers
|
||||
#vis struct #struct_name #generics {
|
||||
#(#fields),*
|
||||
}
|
||||
|
||||
#vis fn #ident #generics (#cx_token: Scope<'a, #struct_name #generics>) #output {
|
||||
let #struct_name { #(#field_names),* } = &cx.props;
|
||||
#block
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@ use proc_macro::TokenStream;
|
|||
use quote::ToTokens;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
pub(crate) mod htm;
|
||||
pub(crate) mod ifmt;
|
||||
pub(crate) mod inlineprops;
|
||||
pub(crate) mod props;
|
||||
pub(crate) mod router;
|
||||
pub(crate) mod rsx;
|
||||
|
@ -214,3 +214,37 @@ pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
|
|||
let input = parse_macro_input!(input as Routable);
|
||||
routable_derive_impl(input).into()
|
||||
}
|
||||
|
||||
/// Derive props for a component within the component definition.
|
||||
///
|
||||
/// This macro provides a simple transformation from `Scope<{}>` to `Scope<P>`,
|
||||
/// removing some boilerplate when defining props.
|
||||
///
|
||||
/// You don't *need* to use this macro at all, but it can be helpful in cases where
|
||||
/// you would be repeating a lot of the usual Rust boilerplate.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// #[inline_props]
|
||||
/// fn app(cx: Scope<{ bob: String }>) -> Element {
|
||||
/// cx.render(rsx!("hello, {bob}"))
|
||||
/// }
|
||||
///
|
||||
/// // is equivalent to
|
||||
///
|
||||
/// #[derive(PartialEq, Props)]
|
||||
/// struct AppProps {
|
||||
/// bob: String,
|
||||
/// }
|
||||
///
|
||||
/// fn app(cx: Scope<AppProps>) -> Element {
|
||||
/// cx.render(rsx!("hello, {bob}"))
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn inline_props(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
match syn::parse::<inlineprops::InlinePropsBody>(s) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,6 @@ fern = { version = "0.6.0", features = ["colored"] }
|
|||
rand = { version = "0.8.4", features = ["small_rng"] }
|
||||
simple_logger = "1.13.0"
|
||||
dioxus-core-macro = { path = "../core-macro", version = "^0.1.2" }
|
||||
# dioxus-hooks = { path = "../hooks" }
|
||||
criterion = "0.3.5"
|
||||
thiserror = "1.0.30"
|
||||
|
||||
|
|
|
@ -46,11 +46,9 @@ fn main() {
|
|||
VNodes::Element(vel) => dbg!(vel),
|
||||
_ => todo!()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Internals
|
||||
|
||||
Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus borrows these concepts:
|
||||
|
|
|
@ -5,11 +5,11 @@ use dioxus_html as dioxus_elements;
|
|||
|
||||
fn main() {}
|
||||
|
||||
fn App(cx: Scope<()>) -> Element {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx!(div {
|
||||
App2 {
|
||||
app2(
|
||||
p: "asd"
|
||||
}
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ struct Borrowed<'a> {
|
|||
p: &'a str,
|
||||
}
|
||||
|
||||
fn App2<'a>(cx: Scope<'a, Borrowed<'a>>) -> Element {
|
||||
fn app2<'a>(cx: Scope<'a, Borrowed<'a>>) -> Element {
|
||||
let g = eat2(&cx);
|
||||
rsx!(cx, "")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! This module contains the stateful DiffMachine and all methods to diff VNodes, their properties, and their children.
|
||||
//! This module contains the stateful DiffState and all methods to diff VNodes, their properties, and their children.
|
||||
//!
|
||||
//! The [`DiffMachine`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set
|
||||
//! The [`DiffState`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set
|
||||
//! of mutations for the RealDom to apply.
|
||||
//!
|
||||
//! ## Notice:
|
||||
|
@ -86,14 +86,14 @@
|
|||
//! - Certain web-dom-specific optimizations.
|
||||
//!
|
||||
//! More info on how to improve this diffing algorithm:
|
||||
//! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/
|
||||
//! - <https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/>
|
||||
|
||||
use crate::innerlude::*;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use DomEdit::*;
|
||||
|
||||
/// Our DiffMachine is an iterative tree differ.
|
||||
/// Our DiffState is an iterative tree differ.
|
||||
///
|
||||
/// It uses techniques of a stack machine to allow pausing and restarting of the diff algorithm. This
|
||||
/// was originally implemented using recursive techniques, but Rust lacks the ability to call async functions recursively,
|
||||
|
@ -490,7 +490,7 @@ impl<'bump> DiffState<'bump> {
|
|||
(Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new),
|
||||
|
||||
// The normal pathway still works, but generates slightly weird instructions
|
||||
// This pathway just ensures we get create and replace
|
||||
// This pathway ensures uses the ReplaceAll, not the InsertAfter and remove
|
||||
(Placeholder(_), Fragment(new)) => {
|
||||
self.stack
|
||||
.create_children(new.children, MountType::Replace { old: old_node });
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
//! This can be done either through boxing directly, or by using dynamic-sized-types and a custom allocator. In our case,
|
||||
//! we build a tiny alloactor in the stack and allocate the closure into that.
|
||||
//!
|
||||
//! The logic for this was borrowed from https://docs.rs/stack_dst/0.6.1/stack_dst/. Unfortunately, this crate does not
|
||||
//! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
|
||||
//! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
|
||||
|
||||
use crate::prelude::{NodeFactory, VNode};
|
||||
use crate::innerlude::{NodeFactory, VNode};
|
||||
use std::mem;
|
||||
|
||||
/// A concrete type provider for closures that build VNode structures.
|
||||
|
@ -35,15 +35,15 @@ enum StackNodeStorage<'a, 'b> {
|
|||
}
|
||||
|
||||
impl<'a, 'b> LazyNodes<'a, 'b> {
|
||||
pub fn new_some<F>(_val: F) -> Option<Self>
|
||||
pub fn new_some<F>(_val: F) -> Self
|
||||
where
|
||||
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
|
||||
{
|
||||
Some(Self::new(_val))
|
||||
Self::new(_val)
|
||||
}
|
||||
|
||||
/// force this call onto the stack
|
||||
pub fn new_boxed<F>(_val: F) -> Option<Self>
|
||||
pub fn new_boxed<F>(_val: F) -> Self
|
||||
where
|
||||
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
|
||||
{
|
||||
|
@ -55,9 +55,9 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||
fac.map(inner)
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
Self {
|
||||
inner: StackNodeStorage::Heap(Box::new(val)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<F>(_val: F) -> Self
|
||||
|
@ -249,9 +249,7 @@ fn it_works() {
|
|||
let caller = LazyNodes::new_some(|f| {
|
||||
//
|
||||
f.text(format_args!("hello world!"))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
});
|
||||
let g = caller.call(factory);
|
||||
|
||||
dbg!(g);
|
||||
|
@ -291,7 +289,6 @@ fn it_drops() {
|
|||
log::debug!("main closure");
|
||||
f.fragment_from_iter(it)
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let _ = caller.call(factory);
|
||||
|
|
|
@ -57,19 +57,25 @@ pub(crate) mod innerlude {
|
|||
/// // ...
|
||||
/// };
|
||||
/// ```
|
||||
pub type Component<P> = fn(Scope<P>) -> Element;
|
||||
pub type Component<P = ()> = fn(Scope<P>) -> Element;
|
||||
|
||||
/// A list of attributes
|
||||
///
|
||||
pub type Attributes<'a> = Option<&'a [Attribute<'a>]>;
|
||||
}
|
||||
|
||||
pub use crate::innerlude::{
|
||||
Attribute, Component, DioxusElement, DomEdit, Element, ElementId, EventHandler, EventPriority,
|
||||
IntoVNode, LazyNodes, Listener, Mutations, NodeFactory, Properties, SchedulerMsg, Scope,
|
||||
ScopeId, ScopeState, UserEvent, VElement, VFragment, VNode, VirtualDom,
|
||||
ScopeId, ScopeState, TaskId, UserEvent, VComponent, VElement, VFragment, VNode, VPlaceholder,
|
||||
VText, VirtualDom,
|
||||
};
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::innerlude::Scope;
|
||||
pub use crate::innerlude::{
|
||||
Component, DioxusElement, Element, EventHandler, LazyNodes, NodeFactory, ScopeState,
|
||||
Attributes, Component, DioxusElement, Element, EventHandler, LazyNodes, NodeFactory,
|
||||
ScopeState,
|
||||
};
|
||||
pub use crate::nodes::VNode;
|
||||
pub use crate::virtual_dom::{fc_to_builder, Fragment, Properties};
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
//! Instructions returned by the VirtualDOM on how to modify the Real DOM.
|
||||
//!
|
||||
//! This module contains an internal API to generate these instructions.
|
||||
//!
|
||||
//! Beware that changing code in this module will break compatibility with
|
||||
//! interpreters for these types of DomEdits.
|
||||
|
||||
use crate::innerlude::*;
|
||||
use std::{any::Any, fmt::Debug};
|
||||
|
|
|
@ -20,7 +20,7 @@ use std::{
|
|||
///
|
||||
/// VNodes are designed to be lightweight and used with with a bump allocator. To create a VNode, you can use either of:
|
||||
///
|
||||
/// - the [`rsx`] macro
|
||||
/// - the `rsx!` macro
|
||||
/// - the [`NodeFactory`] API
|
||||
pub enum VNode<'src> {
|
||||
/// Text VNodes simply bump-allocated (or static) string slices
|
||||
|
@ -51,7 +51,7 @@ pub enum VNode<'src> {
|
|||
/// key: "a",
|
||||
/// onclick: |e| log::info!("clicked"),
|
||||
/// hidden: "true",
|
||||
/// style: { background_color: "red" }
|
||||
/// style: { background_color: "red" },
|
||||
/// "hello"
|
||||
/// }
|
||||
/// });
|
||||
|
@ -59,7 +59,7 @@ pub enum VNode<'src> {
|
|||
/// if let VNode::Element(velement) = node {
|
||||
/// assert_eq!(velement.tag_name, "div");
|
||||
/// assert_eq!(velement.namespace, None);
|
||||
/// assert_eq!(velement.key, Some("a));
|
||||
/// assert_eq!(velement.key, Some("a"));
|
||||
/// }
|
||||
/// ```
|
||||
Element(&'src VElement<'src>),
|
||||
|
@ -720,6 +720,14 @@ impl<'a> IntoVNode<'a> for Option<LazyNodes<'a, '_>> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> IntoIterator for LazyNodes<'a, 'b> {
|
||||
type Item = LazyNodes<'a, 'b>;
|
||||
type IntoIter = std::iter::Once<Self::Item>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
std::iter::once(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> {
|
||||
fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> {
|
||||
self.call(cx)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::{innerlude::*, unsafe_utils::extend_vnode};
|
||||
use bumpalo::Bump;
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use fxhash::FxHashMap;
|
||||
use slab::Slab;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
|
@ -11,9 +13,6 @@ use std::{
|
|||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::{innerlude::*, unsafe_utils::extend_vnode};
|
||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||
|
||||
pub(crate) type FcSlot = *const ();
|
||||
|
||||
pub(crate) struct Heuristic {
|
||||
|
@ -26,14 +25,13 @@ pub(crate) struct Heuristic {
|
|||
//
|
||||
// has an internal heuristics engine to pre-allocate arenas to the right size
|
||||
pub(crate) struct ScopeArena {
|
||||
pub pending_futures: RefCell<FxHashSet<ScopeId>>,
|
||||
pub scope_counter: Cell<usize>,
|
||||
pub sender: UnboundedSender<SchedulerMsg>,
|
||||
pub scope_gen: Cell<usize>,
|
||||
pub bump: Bump,
|
||||
pub scopes: RefCell<FxHashMap<ScopeId, *mut ScopeState>>,
|
||||
pub heuristics: RefCell<FxHashMap<FcSlot, Heuristic>>,
|
||||
pub free_scopes: RefCell<Vec<*mut ScopeState>>,
|
||||
pub nodes: RefCell<Slab<*const VNode<'static>>>,
|
||||
pub tasks: Rc<TaskQueue>,
|
||||
}
|
||||
|
||||
impl ScopeArena {
|
||||
|
@ -61,14 +59,13 @@ impl ScopeArena {
|
|||
debug_assert_eq!(root_id, 0);
|
||||
|
||||
Self {
|
||||
scope_counter: Cell::new(0),
|
||||
scope_gen: Cell::new(0),
|
||||
bump,
|
||||
pending_futures: RefCell::new(FxHashSet::default()),
|
||||
scopes: RefCell::new(FxHashMap::default()),
|
||||
heuristics: RefCell::new(FxHashMap::default()),
|
||||
free_scopes: RefCell::new(Vec::new()),
|
||||
nodes: RefCell::new(nodes),
|
||||
sender,
|
||||
tasks: TaskQueue::new(sender),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,8 +89,8 @@ impl ScopeArena {
|
|||
subtree: u32,
|
||||
) -> ScopeId {
|
||||
// Increment the ScopeId system. ScopeIDs are never reused
|
||||
let new_scope_id = ScopeId(self.scope_counter.get());
|
||||
self.scope_counter.set(self.scope_counter.get() + 1);
|
||||
let new_scope_id = ScopeId(self.scope_gen.get());
|
||||
self.scope_gen.set(self.scope_gen.get() + 1);
|
||||
|
||||
// Get the height of the scope
|
||||
let height = parent_scope
|
||||
|
@ -129,9 +126,9 @@ impl ScopeArena {
|
|||
height,
|
||||
container,
|
||||
new_scope_id,
|
||||
self.sender.clone(),
|
||||
parent_scope,
|
||||
vcomp,
|
||||
self.tasks.clone(),
|
||||
self.heuristics
|
||||
.borrow()
|
||||
.get(&fc_ptr)
|
||||
|
@ -236,15 +233,11 @@ impl ScopeArena {
|
|||
// - We've dropped all references to the wip bump frame with "ensure_drop_safety"
|
||||
unsafe { scope.reset_wip_frame() };
|
||||
|
||||
let mut items = scope.items.borrow_mut();
|
||||
|
||||
// just forget about our suspended nodes while we're at it
|
||||
items.tasks.clear();
|
||||
let items = scope.items.borrow();
|
||||
|
||||
// guarantee that we haven't screwed up - there should be no latent references anywhere
|
||||
debug_assert!(items.listeners.is_empty());
|
||||
debug_assert!(items.borrowed_props.is_empty());
|
||||
debug_assert!(items.tasks.is_empty());
|
||||
}
|
||||
|
||||
// safety: this is definitely not dropped
|
||||
|
@ -263,10 +256,6 @@ impl ScopeArena {
|
|||
let props = scope.props.borrow();
|
||||
let render = props.as_ref().unwrap();
|
||||
if let Some(node) = render.render(scope) {
|
||||
if !scope.items.borrow().tasks.is_empty() {
|
||||
self.pending_futures.borrow_mut().insert(id);
|
||||
}
|
||||
|
||||
let frame = scope.wip_frame();
|
||||
let node = frame.bump.alloc(node);
|
||||
frame.node.set(unsafe { extend_vnode(node) });
|
||||
|
@ -351,7 +340,7 @@ impl ScopeArena {
|
|||
/// cx.render(rsx!{ div {"Hello, {cx.props.name}"} })
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Scope<'a, P> {
|
||||
pub struct Scope<'a, P = ()> {
|
||||
pub scope: &'a ScopeState,
|
||||
pub props: &'a P,
|
||||
}
|
||||
|
@ -382,6 +371,14 @@ impl<'a, P> std::ops::Deref for Scope<'a, P> {
|
|||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct ScopeId(pub usize);
|
||||
|
||||
/// A task's unique identifier.
|
||||
///
|
||||
/// `TaskId` is a `usize` that is unique across the entire VirtualDOM and across time. TaskIDs will never be reused
|
||||
/// once a Task has been completed.
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct TaskId(pub usize);
|
||||
|
||||
/// Every component in Dioxus is represented by a `ScopeState`.
|
||||
///
|
||||
/// Scopes contain the state for hooks, the component's props, and other lifecycle information.
|
||||
|
@ -396,12 +393,10 @@ pub struct ScopeState {
|
|||
pub(crate) container: ElementId,
|
||||
pub(crate) our_arena_idx: ScopeId,
|
||||
pub(crate) height: u32,
|
||||
pub(crate) sender: UnboundedSender<SchedulerMsg>,
|
||||
|
||||
// todo: subtrees
|
||||
pub(crate) is_subtree_root: Cell<bool>,
|
||||
pub(crate) subtree: Cell<u32>,
|
||||
|
||||
pub(crate) props: RefCell<Option<Box<dyn AnyProps>>>,
|
||||
|
||||
// nodes, items
|
||||
|
@ -416,12 +411,12 @@ pub struct ScopeState {
|
|||
|
||||
// shared state -> todo: move this out of scopestate
|
||||
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
|
||||
pub(crate) tasks: Rc<TaskQueue>,
|
||||
}
|
||||
|
||||
pub struct SelfReferentialItems<'a> {
|
||||
pub(crate) listeners: Vec<&'a Listener<'a>>,
|
||||
pub(crate) borrowed_props: Vec<&'a VComponent<'a>>,
|
||||
pub(crate) tasks: Vec<Pin<BumpBox<'a, dyn Future<Output = ()>>>>,
|
||||
}
|
||||
|
||||
// Public methods exposed to libraries and components
|
||||
|
@ -430,13 +425,12 @@ impl ScopeState {
|
|||
height: u32,
|
||||
container: ElementId,
|
||||
our_arena_idx: ScopeId,
|
||||
sender: UnboundedSender<SchedulerMsg>,
|
||||
parent_scope: Option<*mut ScopeState>,
|
||||
vcomp: Box<dyn AnyProps>,
|
||||
tasks: Rc<TaskQueue>,
|
||||
(node_capacity, hook_capacity): (usize, usize),
|
||||
) -> Self {
|
||||
ScopeState {
|
||||
sender,
|
||||
container,
|
||||
our_arena_idx,
|
||||
parent_scope,
|
||||
|
@ -450,12 +444,12 @@ impl ScopeState {
|
|||
|
||||
generation: 0.into(),
|
||||
|
||||
tasks,
|
||||
shared_contexts: Default::default(),
|
||||
|
||||
items: RefCell::new(SelfReferentialItems {
|
||||
listeners: Default::default(),
|
||||
borrowed_props: Default::default(),
|
||||
tasks: Default::default(),
|
||||
}),
|
||||
|
||||
hook_arena: Bump::new(),
|
||||
|
@ -570,7 +564,7 @@ impl ScopeState {
|
|||
///
|
||||
/// ## Notice: you should prefer using prepare_update and get_scope_id
|
||||
pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
|
||||
let (chan, id) = (self.sender.clone(), self.scope_id());
|
||||
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
||||
Rc::new(move || {
|
||||
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
|
||||
})
|
||||
|
@ -578,11 +572,11 @@ impl ScopeState {
|
|||
|
||||
/// Schedule an update for any component given its ScopeId.
|
||||
///
|
||||
/// A component's ScopeId can be obtained from `use_hook` or the [`Context::scope_id`] method.
|
||||
/// A component's ScopeId can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
|
||||
///
|
||||
/// This method should be used when you want to schedule an update for a component
|
||||
pub fn schedule_update_any(&self) -> Rc<dyn Fn(ScopeId)> {
|
||||
let chan = self.sender.clone();
|
||||
let chan = self.tasks.sender.clone();
|
||||
Rc::new(move |id| {
|
||||
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
|
||||
})
|
||||
|
@ -599,7 +593,10 @@ impl ScopeState {
|
|||
///
|
||||
/// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted.
|
||||
pub fn needs_update_any(&self, id: ScopeId) {
|
||||
let _ = self.sender.unbounded_send(SchedulerMsg::Immediate(id));
|
||||
let _ = self
|
||||
.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::Immediate(id));
|
||||
}
|
||||
|
||||
/// Get the Root Node of this scope
|
||||
|
@ -662,34 +659,19 @@ impl ScopeState {
|
|||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
///
|
||||
/// The future is forcibly dropped if the component is not ready by the next render
|
||||
pub fn push_task<'src, F>(&'src self, fut: impl FnOnce() -> F + 'src) -> usize
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
F::Output: 'src,
|
||||
F: 'src,
|
||||
{
|
||||
self.sender
|
||||
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
// wake up the scheduler if it is sleeping
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx))
|
||||
.unwrap();
|
||||
|
||||
// wrap it in a type that will actually drop the contents
|
||||
//
|
||||
// Safety: we just made the pointer above and will promise not to alias it!
|
||||
// The main reason we do this through from_raw is because Bumpalo's box does
|
||||
// not support unsized coercion
|
||||
let fut: &mut dyn Future<Output = ()> = self.bump().alloc(fut());
|
||||
let boxed_fut: BumpBox<dyn Future<Output = ()>> = unsafe { BumpBox::from_raw(fut) };
|
||||
let pinned_fut: Pin<BumpBox<_>> = boxed_fut.into();
|
||||
self.tasks.push_fut(fut)
|
||||
}
|
||||
|
||||
// erase the 'src lifetime for self-referential storage
|
||||
// todo: provide a miri test around this
|
||||
// concerned about segfaulting
|
||||
let self_ref_fut = unsafe { std::mem::transmute(pinned_fut) };
|
||||
|
||||
// Push the future into the tasks
|
||||
let mut items = self.items.borrow_mut();
|
||||
items.tasks.push(self_ref_fut);
|
||||
items.tasks.len() - 1
|
||||
// todo: attach some state to the future to know if we should poll it
|
||||
pub fn remove_future(&self, id: TaskId) {
|
||||
self.tasks.remove_fut(id);
|
||||
}
|
||||
|
||||
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
|
||||
|
@ -707,14 +689,11 @@ impl ScopeState {
|
|||
/// cx.render(lazy_tree)
|
||||
/// }
|
||||
///```
|
||||
pub fn render<'src>(&'src self, rsx: Option<LazyNodes<'src, '_>>) -> Option<VNode<'src>> {
|
||||
pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Option<VNode<'src>> {
|
||||
let fac = NodeFactory {
|
||||
bump: &self.wip_frame().bump,
|
||||
};
|
||||
match rsx {
|
||||
Some(s) => Some(s.call(fac)),
|
||||
None => todo!("oh no no nodes"),
|
||||
}
|
||||
Some(rsx.call(fac))
|
||||
}
|
||||
|
||||
/// Store a value between renders
|
||||
|
@ -813,11 +792,6 @@ impl ScopeState {
|
|||
self.generation.set(self.generation.get() + 1);
|
||||
}
|
||||
|
||||
/// Get the [`Bump`] of the WIP frame.
|
||||
pub(crate) fn bump(&self) -> &Bump {
|
||||
&self.wip_frame().bump
|
||||
}
|
||||
|
||||
// todo: disable bookkeeping on drop (unncessary)
|
||||
pub(crate) fn reset(&mut self) {
|
||||
// first: book keaping
|
||||
|
@ -834,11 +808,9 @@ impl ScopeState {
|
|||
let SelfReferentialItems {
|
||||
borrowed_props,
|
||||
listeners,
|
||||
tasks,
|
||||
} = self.items.get_mut();
|
||||
borrowed_props.clear();
|
||||
listeners.clear();
|
||||
tasks.clear();
|
||||
self.frames[0].reset();
|
||||
self.frames[1].reset();
|
||||
|
||||
|
@ -884,6 +856,43 @@ impl BumpFrame {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TaskQueue {
|
||||
pub(crate) tasks: RefCell<FxHashMap<TaskId, InnerTask>>,
|
||||
gen: Cell<usize>,
|
||||
sender: UnboundedSender<SchedulerMsg>,
|
||||
}
|
||||
pub(crate) type InnerTask = Pin<Box<dyn Future<Output = ()>>>;
|
||||
impl TaskQueue {
|
||||
fn new(sender: UnboundedSender<SchedulerMsg>) -> Rc<Self> {
|
||||
Rc::new(Self {
|
||||
tasks: RefCell::new(FxHashMap::default()),
|
||||
gen: Cell::new(0),
|
||||
sender,
|
||||
})
|
||||
}
|
||||
fn push_fut(&self, task: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
let pinned = Box::pin(task);
|
||||
let id = self.gen.get();
|
||||
self.gen.set(id + 1);
|
||||
let tid = TaskId(id);
|
||||
|
||||
self.tasks.borrow_mut().insert(tid, pinned);
|
||||
tid
|
||||
}
|
||||
fn remove_fut(&self, id: TaskId) {
|
||||
if let Ok(mut tasks) = self.tasks.try_borrow_mut() {
|
||||
let _ = tasks.remove(&id);
|
||||
} else {
|
||||
// todo: it should be okay to remote a fut while the queue is being polled
|
||||
// However, it's not currently possible to do that.
|
||||
log::debug!("Unable to remove task from task queue. This is probably a bug.");
|
||||
}
|
||||
}
|
||||
pub(crate) fn has_tasks(&self) -> bool {
|
||||
!self.tasks.borrow().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sizeof() {
|
||||
dbg!(std::mem::size_of::<ScopeState>());
|
||||
|
|
|
@ -7,7 +7,6 @@ use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
|||
use futures_util::{Future, StreamExt};
|
||||
use fxhash::FxHashSet;
|
||||
use indexmap::IndexSet;
|
||||
use smallvec::SmallVec;
|
||||
use std::{any::Any, collections::VecDeque, iter::FromIterator, pin::Pin, sync::Arc, task::Poll};
|
||||
|
||||
/// A virtual node s ystem that progresses user events and diffs UI trees.
|
||||
|
@ -15,7 +14,7 @@ use std::{any::Any, collections::VecDeque, iter::FromIterator, pin::Pin, sync::A
|
|||
///
|
||||
/// ## Guide
|
||||
///
|
||||
/// Components are defined as simple functions that take [`Context`] and a [`Properties`] type and return an [`Element`].
|
||||
/// Components are defined as simple functions that take [`Scope`] and return an [`Element`].
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// #[derive(Props, PartialEq)]
|
||||
|
@ -312,16 +311,16 @@ impl VirtualDom {
|
|||
}
|
||||
|
||||
if self.pending_messages.is_empty() {
|
||||
if self.scopes.pending_futures.borrow().is_empty() {
|
||||
self.pending_messages
|
||||
.push_front(self.channel.1.next().await.unwrap());
|
||||
} else {
|
||||
if self.scopes.tasks.has_tasks() {
|
||||
use futures_util::future::{select, Either};
|
||||
|
||||
match select(PollTasks(&mut self.scopes), self.channel.1.next()).await {
|
||||
Either::Left((_, _)) => {}
|
||||
Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()),
|
||||
}
|
||||
} else {
|
||||
self.pending_messages
|
||||
.push_front(self.channel.1.next().await.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -345,8 +344,8 @@ impl VirtualDom {
|
|||
|
||||
pub fn process_message(&mut self, msg: SchedulerMsg) {
|
||||
match msg {
|
||||
SchedulerMsg::NewTask(id) => {
|
||||
self.scopes.pending_futures.borrow_mut().insert(id);
|
||||
SchedulerMsg::NewTask(_id) => {
|
||||
// uh, not sure? I think end up re-polling it anyways
|
||||
}
|
||||
SchedulerMsg::Event(event) => {
|
||||
if let Some(element) = event.element {
|
||||
|
@ -552,11 +551,11 @@ impl VirtualDom {
|
|||
/// let dom = VirtualDom::new(Base);
|
||||
/// let nodes = dom.render_nodes(rsx!("div"));
|
||||
/// ```
|
||||
pub fn render_vnodes<'a>(&'a self, lazy_nodes: Option<LazyNodes<'a, '_>>) -> &'a VNode<'a> {
|
||||
pub fn render_vnodes<'a>(&'a self, lazy_nodes: LazyNodes<'a, '_>) -> &'a VNode<'a> {
|
||||
let scope = self.scopes.get_scope(ScopeId(0)).unwrap();
|
||||
let frame = scope.wip_frame();
|
||||
let factory = NodeFactory { bump: &frame.bump };
|
||||
let node = lazy_nodes.unwrap().call(factory);
|
||||
let node = lazy_nodes.call(factory);
|
||||
frame.bump.alloc(node)
|
||||
}
|
||||
|
||||
|
@ -594,7 +593,7 @@ impl VirtualDom {
|
|||
/// let dom = VirtualDom::new(Base);
|
||||
/// let nodes = dom.render_nodes(rsx!("div"));
|
||||
/// ```
|
||||
pub fn create_vnodes<'a>(&'a self, nodes: Option<LazyNodes<'a, '_>>) -> Mutations<'a> {
|
||||
pub fn create_vnodes<'a>(&'a self, nodes: LazyNodes<'a, '_>) -> Mutations<'a> {
|
||||
let mut machine = DiffState::new(&self.scopes);
|
||||
machine.stack.element_stack.push(ElementId(0));
|
||||
machine
|
||||
|
@ -619,8 +618,8 @@ impl VirtualDom {
|
|||
/// ```
|
||||
pub fn diff_lazynodes<'a>(
|
||||
&'a self,
|
||||
left: Option<LazyNodes<'a, '_>>,
|
||||
right: Option<LazyNodes<'a, '_>>,
|
||||
left: LazyNodes<'a, '_>,
|
||||
right: LazyNodes<'a, '_>,
|
||||
) -> (Mutations<'a>, Mutations<'a>) {
|
||||
let (old, new) = (self.render_vnodes(left), self.render_vnodes(right));
|
||||
|
||||
|
@ -703,39 +702,26 @@ impl<'a> Future for PollTasks<'a> {
|
|||
type Output = ();
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||
let mut all_pending = true;
|
||||
let mut any_pending = false;
|
||||
|
||||
let mut unfinished_tasks: SmallVec<[_; 10]> = smallvec::smallvec![];
|
||||
let mut scopes_to_clear: SmallVec<[_; 10]> = smallvec::smallvec![];
|
||||
let mut tasks = self.0.tasks.tasks.borrow_mut();
|
||||
let mut to_remove = vec![];
|
||||
|
||||
// Poll every scope manually
|
||||
for fut in self.0.pending_futures.borrow().iter().copied() {
|
||||
let scope = self.0.get_scope(fut).expect("Scope should never be moved");
|
||||
|
||||
let mut items = scope.items.borrow_mut();
|
||||
|
||||
// really this should just be retain_mut but that doesn't exist yet
|
||||
while let Some(mut task) = items.tasks.pop() {
|
||||
if task.as_mut().poll(cx).is_ready() {
|
||||
all_pending = false
|
||||
} else {
|
||||
unfinished_tasks.push(task);
|
||||
}
|
||||
// this would be better served by retain
|
||||
for (id, task) in tasks.iter_mut() {
|
||||
if task.as_mut().poll(cx).is_ready() {
|
||||
to_remove.push(*id);
|
||||
} else {
|
||||
any_pending = true;
|
||||
}
|
||||
|
||||
if unfinished_tasks.is_empty() {
|
||||
scopes_to_clear.push(fut);
|
||||
}
|
||||
|
||||
items.tasks.extend(unfinished_tasks.drain(..));
|
||||
}
|
||||
|
||||
for scope in scopes_to_clear {
|
||||
self.0.pending_futures.borrow_mut().remove(&scope);
|
||||
for id in to_remove {
|
||||
tasks.remove(&id);
|
||||
}
|
||||
|
||||
// Resolve the future if any singular task is ready
|
||||
match all_pending {
|
||||
match any_pending {
|
||||
true => Poll::Pending,
|
||||
false => Poll::Ready(()),
|
||||
}
|
||||
|
@ -947,7 +933,7 @@ impl<'a> Properties for FragmentProps<'a> {
|
|||
#[allow(non_upper_case_globals, non_snake_case)]
|
||||
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
|
||||
let i = cx.props.0.as_ref().map(|f| f.decouple());
|
||||
cx.render(Some(LazyNodes::new(|f| f.fragment_from_iter(i))))
|
||||
cx.render(LazyNodes::new(|f| f.fragment_from_iter(i)))
|
||||
}
|
||||
|
||||
/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus
|
||||
|
|
|
@ -21,7 +21,7 @@ fn new_dom<P: 'static + Send>(app: Component<P>, props: P) -> VirtualDom {
|
|||
|
||||
#[test]
|
||||
fn test_original_diff() {
|
||||
static APP: Component<()> = |cx| {
|
||||
static APP: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
div {
|
||||
|
@ -57,7 +57,7 @@ fn test_original_diff() {
|
|||
|
||||
#[test]
|
||||
fn create() {
|
||||
static APP: Component<()> = |cx| {
|
||||
static APP: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
div {
|
||||
|
@ -120,7 +120,7 @@ fn create() {
|
|||
|
||||
#[test]
|
||||
fn create_list() {
|
||||
static APP: Component<()> = |cx| {
|
||||
static APP: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
{(0..3).map(|f| rsx!{ div {
|
||||
"hello"
|
||||
|
@ -169,7 +169,7 @@ fn create_list() {
|
|||
|
||||
#[test]
|
||||
fn create_simple() {
|
||||
static APP: Component<()> = |cx| {
|
||||
static APP: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {}
|
||||
div {}
|
||||
|
@ -207,7 +207,7 @@ fn create_simple() {
|
|||
}
|
||||
#[test]
|
||||
fn create_components() {
|
||||
static App: Component<()> = |cx| {
|
||||
static App: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
Child { "abc1" }
|
||||
Child { "abc2" }
|
||||
|
@ -273,7 +273,7 @@ fn create_components() {
|
|||
}
|
||||
#[test]
|
||||
fn anchors() {
|
||||
static App: Component<()> = |cx| {
|
||||
static App: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
{true.then(|| rsx!{ div { "hello" } })}
|
||||
{false.then(|| rsx!{ div { "goodbye" } })}
|
||||
|
|
|
@ -802,3 +802,68 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
|
|||
]
|
||||
);
|
||||
}
|
||||
|
||||
// noticed some weird behavior in the desktop interpreter
|
||||
// just making sure it doesnt happen in the core implementation
|
||||
#[test]
|
||||
fn remove_list() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
(0..10).rev().take(5).map(|i| {
|
||||
rsx! { Fragment { key: "{i}", "{i}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
(0..10).rev().take(2).map(|i| {
|
||||
rsx! { Fragment { key: "{i}", "{i}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (create, changes) = dom.diff_lazynodes(left, right);
|
||||
|
||||
// dbg!(create);
|
||||
// dbg!(changes);
|
||||
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
// remove 5, 4, 3
|
||||
Remove { root: 3 },
|
||||
Remove { root: 4 },
|
||||
Remove { root: 5 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// noticed some weird behavior in the desktop interpreter
|
||||
// just making sure it doesnt happen in the core implementation
|
||||
#[test]
|
||||
fn remove_list_nokeyed() {
|
||||
let dom = new_dom();
|
||||
|
||||
let left = rsx!({
|
||||
(0..10).rev().take(5).map(|i| {
|
||||
rsx! { Fragment { "{i}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let right = rsx!({
|
||||
(0..10).rev().take(2).map(|i| {
|
||||
rsx! { Fragment { "{i}" }}
|
||||
})
|
||||
});
|
||||
|
||||
let (create, changes) = dom.diff_lazynodes(left, right);
|
||||
|
||||
assert_eq!(
|
||||
changes.edits,
|
||||
[
|
||||
// remove 5, 4, 3
|
||||
Remove { root: 3 },
|
||||
Remove { root: 4 },
|
||||
Remove { root: 5 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use dioxus_hooks::*;
|
|||
use dioxus_html as dioxus_elements;
|
||||
|
||||
fn main() {
|
||||
simple_logger::init().unwrap();
|
||||
// simple_logger::init().unwrap();
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,9 @@ fn app(cx: Scope<()>) -> Element {
|
|||
let mut count = use_state(&cx, || 0);
|
||||
log::debug!("count is {:?}", count);
|
||||
|
||||
cx.push_task(|| async move {
|
||||
cx.push_future(|| async move {
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
println!("count is now {:?}", count);
|
||||
count += 1;
|
||||
});
|
||||
|
||||
|
@ -33,4 +34,4 @@ fn app(cx: Scope<()>) -> Element {
|
|||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ use tao::{
|
|||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::{
|
||||
application::event_loop::EventLoopProxy,
|
||||
webview::RpcRequest,
|
||||
webview::{WebView, WebViewBuilder},
|
||||
};
|
||||
|
@ -37,7 +38,7 @@ pub fn launch_cfg(
|
|||
launch_with_props(root, (), config_builder)
|
||||
}
|
||||
|
||||
pub fn launch_with_props<P: Properties + 'static + Send + Sync>(
|
||||
pub fn launch_with_props<P: 'static + Send>(
|
||||
root: Component<P>,
|
||||
props: P,
|
||||
builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
|
||||
|
@ -51,7 +52,7 @@ struct Response<'a> {
|
|||
edits: Vec<DomEdit<'a>>,
|
||||
}
|
||||
|
||||
pub fn run<T: 'static + Send + Sync>(
|
||||
pub fn run<T: 'static + Send>(
|
||||
root: Component<T>,
|
||||
props: T,
|
||||
user_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
|
||||
|
@ -59,51 +60,61 @@ pub fn run<T: 'static + Send + Sync>(
|
|||
let mut desktop_cfg = DesktopConfig::new();
|
||||
user_builder(&mut desktop_cfg);
|
||||
|
||||
let mut state = DesktopController::new_on_tokio(root, props);
|
||||
let event_loop = EventLoop::with_user_event();
|
||||
let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy());
|
||||
let quit_hotkey = Accelerator::new(SysMods::Cmd, KeyCode::KeyQ);
|
||||
let modifiers = ModifiersState::default();
|
||||
let event_loop = EventLoop::new();
|
||||
|
||||
log::debug!("Starting event loop");
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => state.new_window(&desktop_cfg, event_loop),
|
||||
Event::NewEvents(StartCause::Init) => desktop.new_window(&desktop_cfg, event_loop),
|
||||
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => state.close_window(window_id, control_flow),
|
||||
} => {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
|
||||
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if quit_hotkey.matches(&modifiers, &event.physical_key) {
|
||||
state.close_window(window_id, control_flow);
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if quit_hotkey.matches(&modifiers, &event.physical_key) {
|
||||
desktop.close_window(window_id, control_flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::Resized(_) | WindowEvent::Moved(_) => {
|
||||
if let Some(view) = state.webviews.get_mut(&window_id) {
|
||||
let _ = view.resize();
|
||||
WindowEvent::Resized(_) | WindowEvent::Moved(_) => {
|
||||
if let Some(view) = desktop.webviews.get_mut(&window_id) {
|
||||
let _ = view.resize();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we want to shuttle all of these events into the user's app or provide some handler
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we want to shuttle all of these events into the user's app or provide some handler
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::MainEventsCleared => state.try_load_ready_webviews(),
|
||||
Event::UserEvent(_evt) => {
|
||||
desktop.try_load_ready_webviews();
|
||||
}
|
||||
Event::MainEventsCleared => {
|
||||
desktop.try_load_ready_webviews();
|
||||
}
|
||||
Event::Resumed => {}
|
||||
Event::Suspended => {}
|
||||
Event::LoopDestroyed => {}
|
||||
|
||||
Event::RedrawRequested(_id) => {}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub enum UserWindowEvent {
|
||||
Start,
|
||||
Update,
|
||||
}
|
||||
|
||||
pub struct DesktopController {
|
||||
webviews: HashMap<WindowId, WebView>,
|
||||
sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
|
@ -115,7 +126,11 @@ pub struct DesktopController {
|
|||
impl DesktopController {
|
||||
// Launch the virtualdom on its own thread managed by tokio
|
||||
// returns the desktop state
|
||||
pub fn new_on_tokio<P: Send + 'static>(root: Component<P>, props: P) -> Self {
|
||||
pub fn new_on_tokio<P: Send + 'static>(
|
||||
root: Component<P>,
|
||||
props: P,
|
||||
evt: EventLoopProxy<UserWindowEvent>,
|
||||
) -> Self {
|
||||
let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
|
||||
let pending_edits = edit_queue.clone();
|
||||
|
||||
|
@ -130,7 +145,6 @@ impl DesktopController {
|
|||
.unwrap();
|
||||
|
||||
runtime.block_on(async move {
|
||||
// LocalSet::new().block_on(&runtime, async move {
|
||||
let mut dom =
|
||||
VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver));
|
||||
|
||||
|
@ -150,6 +164,7 @@ impl DesktopController {
|
|||
.unwrap()
|
||||
.push_front(serde_json::to_string(&edit.edits).unwrap());
|
||||
}
|
||||
let _ = evt.send_event(UserWindowEvent::Update);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -164,7 +179,11 @@ impl DesktopController {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn new_window(&mut self, cfg: &DesktopConfig, event_loop: &EventLoopWindowTarget<()>) {
|
||||
pub fn new_window(
|
||||
&mut self,
|
||||
cfg: &DesktopConfig,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
) {
|
||||
let builder = cfg.window.clone().with_menu({
|
||||
// create main menubar menu
|
||||
let mut menu_bar_menu = MenuBar::new();
|
||||
|
|
|
@ -10,5 +10,11 @@ pub use use_shared_state::*;
|
|||
mod usecoroutine;
|
||||
pub use usecoroutine::*;
|
||||
|
||||
mod usemodel;
|
||||
pub use usemodel::*;
|
||||
mod usefuture;
|
||||
pub use usefuture::*;
|
||||
|
||||
mod usesuspense;
|
||||
pub use usesuspense::*;
|
||||
|
||||
// mod usemodel;
|
||||
// pub use usemodel::*;
|
||||
|
|
|
@ -1,48 +1,74 @@
|
|||
use dioxus_core::ScopeState;
|
||||
use dioxus_core::{ScopeState, TaskId};
|
||||
use std::future::Future;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
use std::{cell::Cell, pin::Pin, rc::Rc};
|
||||
/*
|
||||
|
||||
pub fn use_coroutine<'a, F: Future<Output = ()> + 'static>(
|
||||
|
||||
|
||||
let g = use_coroutine(&cx, || {
|
||||
// clone the items in
|
||||
async move {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
*/
|
||||
pub fn use_coroutine<'a, F>(
|
||||
cx: &'a ScopeState,
|
||||
mut f: impl FnMut() -> F + 'a,
|
||||
) -> CoroutineHandle<'a> {
|
||||
create_future: impl FnOnce() -> F,
|
||||
) -> CoroutineHandle<'a>
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
cx.use_hook(
|
||||
move |_| State {
|
||||
running: Default::default(),
|
||||
fut: Default::default(),
|
||||
submit: Default::default(),
|
||||
move |_| {
|
||||
let f = create_future();
|
||||
let id = cx.push_future(f);
|
||||
State {
|
||||
running: Default::default(),
|
||||
id
|
||||
// pending_fut: Default::default(),
|
||||
// running_fut: Default::default(),
|
||||
}
|
||||
},
|
||||
|state| {
|
||||
let fut_slot = state.fut.clone();
|
||||
let running = state.running.clone();
|
||||
let submit: Box<dyn FnOnce() + 'a> = Box::new(move || {
|
||||
let g = async move {
|
||||
running.set(true);
|
||||
f().await;
|
||||
running.set(false);
|
||||
};
|
||||
let p: Pin<Box<dyn Future<Output = ()>>> = Box::pin(g);
|
||||
fut_slot
|
||||
.borrow_mut()
|
||||
.replace(unsafe { std::mem::transmute(p) });
|
||||
});
|
||||
// state.pending_fut.set(Some(Box::pin(f)));
|
||||
|
||||
let submit = unsafe { std::mem::transmute(submit) };
|
||||
state.submit.get_mut().replace(submit);
|
||||
// if let Some(fut) = state.running_fut.as_mut() {
|
||||
// cx.push_future(fut);
|
||||
// }
|
||||
|
||||
if state.running.get() {
|
||||
let mut fut = state.fut.borrow_mut();
|
||||
cx.push_task(|| fut.as_mut().unwrap().as_mut());
|
||||
} else {
|
||||
// make sure to drop the old future
|
||||
if let Some(fut) = state.fut.borrow_mut().take() {
|
||||
drop(fut);
|
||||
}
|
||||
}
|
||||
// if let Some(fut) = state.running_fut.take() {
|
||||
// state.running.set(true);
|
||||
// fut.resume();
|
||||
// }
|
||||
|
||||
// let submit: Box<dyn FnOnce() + 'a> = Box::new(move || {
|
||||
// let g = async move {
|
||||
// running.set(true);
|
||||
// create_future().await;
|
||||
// running.set(false);
|
||||
// };
|
||||
// let p: Pin<Box<dyn Future<Output = ()>>> = Box::pin(g);
|
||||
// fut_slot
|
||||
// .borrow_mut()
|
||||
// .replace(unsafe { std::mem::transmute(p) });
|
||||
// });
|
||||
|
||||
// let submit = unsafe { std::mem::transmute(submit) };
|
||||
// state.submit.get_mut().replace(submit);
|
||||
|
||||
// if state.running.get() {
|
||||
// // let mut fut = state.fut.borrow_mut();
|
||||
// // cx.push_task(|| fut.as_mut().unwrap().as_mut());
|
||||
// } else {
|
||||
// // make sure to drop the old future
|
||||
// if let Some(fut) = state.fut.borrow_mut().take() {
|
||||
// drop(fut);
|
||||
// }
|
||||
// }
|
||||
CoroutineHandle { cx, inner: state }
|
||||
},
|
||||
)
|
||||
|
@ -50,14 +76,20 @@ pub fn use_coroutine<'a, F: Future<Output = ()> + 'static>(
|
|||
|
||||
struct State {
|
||||
running: Rc<Cell<bool>>,
|
||||
submit: RefCell<Option<Box<dyn FnOnce()>>>,
|
||||
fut: Rc<RefCell<Option<Pin<Box<dyn Future<Output = ()>>>>>>,
|
||||
id: TaskId,
|
||||
// the way this is structure, you can toggle the coroutine without re-rendering the comppnent
|
||||
// this means every render *generates* the future, which is a bit of a waste
|
||||
// todo: allocate pending futures in the bump allocator and then have a true promotion
|
||||
// pending_fut: Cell<Option<Pin<Box<dyn Future<Output = ()> + 'static>>>>,
|
||||
// running_fut: Option<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||
// running_fut: Rc<RefCell<Option<Pin<Box<dyn Future<Output = ()> + 'static>>>>>
|
||||
}
|
||||
|
||||
pub struct CoroutineHandle<'a> {
|
||||
cx: &'a ScopeState,
|
||||
inner: &'a State,
|
||||
}
|
||||
|
||||
impl Clone for CoroutineHandle<'_> {
|
||||
fn clone(&self) -> Self {
|
||||
CoroutineHandle {
|
||||
|
@ -73,16 +105,23 @@ impl<'a> CoroutineHandle<'a> {
|
|||
if self.is_running() {
|
||||
return;
|
||||
}
|
||||
if let Some(submit) = self.inner.submit.borrow_mut().take() {
|
||||
submit();
|
||||
let mut fut = self.inner.fut.borrow_mut();
|
||||
self.cx.push_task(|| fut.as_mut().unwrap().as_mut());
|
||||
}
|
||||
|
||||
// if let Some(submit) = self.inner.pending_fut.take() {
|
||||
// submit();
|
||||
// let inner = self.inner;
|
||||
// self.cx.push_task(submit());
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.inner.running.get()
|
||||
}
|
||||
|
||||
pub fn resume(&self) {}
|
||||
pub fn resume(&self) {
|
||||
// self.cx.push_task(fut)
|
||||
}
|
||||
|
||||
pub fn stop(&self) {}
|
||||
|
||||
pub fn restart(&self) {}
|
||||
}
|
||||
|
|
56
packages/hooks/src/usefuture.rs
Normal file
56
packages/hooks/src/usefuture.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use dioxus_core::{ScopeState, TaskId};
|
||||
use std::{cell::Cell, future::Future, rc::Rc};
|
||||
|
||||
pub fn use_future<'a, T: 'static, F: Future<Output = T> + 'static>(
|
||||
cx: &'a ScopeState,
|
||||
f: impl FnOnce() -> F,
|
||||
) -> (Option<&T>, FutureHandle<'a, T>) {
|
||||
cx.use_hook(
|
||||
|_| {
|
||||
//
|
||||
let fut = f();
|
||||
let slot = Rc::new(Cell::new(None));
|
||||
let updater = cx.schedule_update();
|
||||
|
||||
let _slot = slot.clone();
|
||||
let new_fut = async move {
|
||||
let res = fut.await;
|
||||
_slot.set(Some(res));
|
||||
updater();
|
||||
};
|
||||
let task = cx.push_future(new_fut);
|
||||
|
||||
UseFutureInner {
|
||||
needs_regen: true,
|
||||
slot,
|
||||
value: None,
|
||||
task: Some(task),
|
||||
}
|
||||
},
|
||||
|state| {
|
||||
if let Some(value) = state.slot.take() {
|
||||
state.value = Some(value);
|
||||
state.task = None;
|
||||
}
|
||||
(
|
||||
state.value.as_ref(),
|
||||
FutureHandle {
|
||||
cx,
|
||||
value: Cell::new(None),
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
struct UseFutureInner<T> {
|
||||
needs_regen: bool,
|
||||
value: Option<T>,
|
||||
slot: Rc<Cell<Option<T>>>,
|
||||
task: Option<TaskId>,
|
||||
}
|
||||
|
||||
pub struct FutureHandle<'a, T> {
|
||||
cx: &'a ScopeState,
|
||||
value: Cell<Option<T>>,
|
||||
}
|
|
@ -89,45 +89,15 @@ pub fn use_model_coroutine<'a, T, F: Future<Output = ()> + 'static>(
|
|||
}
|
||||
},
|
||||
|inner| {
|
||||
if let Some(task) = inner.task.get_mut() {
|
||||
cx.push_task(|| task);
|
||||
}
|
||||
// if let Some(task) = inner.task.get_mut() {
|
||||
// cx.push_task(|| task);
|
||||
// }
|
||||
//
|
||||
todo!()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub struct UseModelCoroutine {}
|
||||
|
||||
struct UseModelTaskInner {
|
||||
task: RefCell<Option<ModelTask>>,
|
||||
}
|
||||
|
||||
impl UseModelCoroutine {
|
||||
pub fn start(&self) {}
|
||||
}
|
||||
|
||||
pub struct ModelAsync<T> {
|
||||
_p: PhantomData<T>,
|
||||
}
|
||||
impl<T> ModelAsync<T> {
|
||||
pub fn write(&self) -> RefMut<'_, T> {
|
||||
todo!()
|
||||
}
|
||||
pub fn read(&self) -> Ref<'_, T> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppModels {}
|
||||
|
||||
impl AppModels {
|
||||
pub fn get<T: 'static>(&self) -> ModelAsync<T> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for UseModel<'_, T> {}
|
||||
impl<'a, T> Clone for UseModel<'a, T> {
|
||||
fn clone(&self) -> Self {
|
||||
|
|
|
@ -171,7 +171,11 @@ impl<'a, T: 'static> UseState<'a, T> {
|
|||
}
|
||||
|
||||
impl<'a, T: 'static + ToOwned<Owned = T>> UseState<'a, T> {
|
||||
/// Gain mutable access to the new value. This method is only available when the value is a `ToOwned` type.
|
||||
/// Gain mutable access to the new value via RefMut.
|
||||
///
|
||||
/// If `modify` is called, then the component will re-render.
|
||||
///
|
||||
/// This method is only available when the value is a `ToOwned` type.
|
||||
///
|
||||
/// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value.
|
||||
///
|
||||
|
@ -283,11 +287,16 @@ pub struct AsyncUseState<T: 'static> {
|
|||
wip: Rc<RefCell<Option<T>>>,
|
||||
}
|
||||
|
||||
impl<T: ToOwned> AsyncUseState<T> {
|
||||
impl<T: ToOwned<Owned = T>> AsyncUseState<T> {
|
||||
pub fn get_mut<'a>(&'a self) -> RefMut<'a, T> {
|
||||
// make sure we get processed
|
||||
// self.needs_update();
|
||||
|
||||
{
|
||||
let mut wip = self.wip.borrow_mut();
|
||||
if wip.is_none() {
|
||||
*wip = Some(self.inner.as_ref().to_owned());
|
||||
}
|
||||
(self.re_render)();
|
||||
}
|
||||
// Bring out the new value, cloning if it we need to
|
||||
// "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this
|
||||
RefMut::map(self.wip.borrow_mut(), |slot| {
|
||||
|
@ -301,9 +310,10 @@ impl<T> AsyncUseState<T> {
|
|||
(self.re_render)();
|
||||
*self.wip.borrow_mut() = Some(val);
|
||||
}
|
||||
pub fn get(&self) -> &T {
|
||||
self.inner.as_ref()
|
||||
}
|
||||
|
||||
// pub fn get(&self) -> Ref<'_, T> {
|
||||
// self.wip.borrow
|
||||
// }
|
||||
|
||||
pub fn get_rc(&self) -> &Rc<T> {
|
||||
&self.inner
|
||||
|
|
44
packages/hooks/src/usesuspense.rs
Normal file
44
packages/hooks/src/usesuspense.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use std::{cell::Cell, future::Future, rc::Rc};
|
||||
|
||||
use dioxus_core::{Element, ScopeState, TaskId};
|
||||
|
||||
pub fn use_suspense<R: 'static, F: Future<Output = R> + 'static>(
|
||||
cx: &ScopeState,
|
||||
create_future: impl FnOnce() -> F,
|
||||
render: impl FnOnce(&R) -> Element,
|
||||
) -> Element {
|
||||
cx.use_hook(
|
||||
|_| {
|
||||
let fut = create_future();
|
||||
|
||||
let wip_value: Rc<Cell<Option<R>>> = Default::default();
|
||||
|
||||
let wip = wip_value.clone();
|
||||
let new_fut = async move {
|
||||
let val = fut.await;
|
||||
wip.set(Some(val));
|
||||
};
|
||||
|
||||
let task = cx.push_future(new_fut);
|
||||
SuspenseInner {
|
||||
task,
|
||||
value: None,
|
||||
wip_value,
|
||||
}
|
||||
},
|
||||
|sus| {
|
||||
if let Some(value) = sus.value.as_ref() {
|
||||
render(&value)
|
||||
} else {
|
||||
// generate a placeholder node if the future isnt ready
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
struct SuspenseInner<R> {
|
||||
task: TaskId,
|
||||
wip_value: Rc<Cell<Option<R>>>,
|
||||
value: Option<R>,
|
||||
}
|
|
@ -94,6 +94,8 @@ pub trait GlobalAttributes {
|
|||
title;
|
||||
translate;
|
||||
|
||||
role;
|
||||
|
||||
/// dangerous_inner_html is Dioxus's replacement for using innerHTML in the browser DOM. In general, setting
|
||||
/// HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS)
|
||||
/// attack. So, you can set HTML directly from Dioxus, but you have to type out dangerous_inner_html to remind
|
||||
|
@ -838,6 +840,7 @@ pub trait SvgAttributes {
|
|||
requiredFeatures: "requiredFeatures",
|
||||
restart: "restart",
|
||||
result: "result",
|
||||
role: "role",
|
||||
rotate: "rotate",
|
||||
rx: "rx",
|
||||
ry: "ry",
|
||||
|
|
|
@ -2,14 +2,28 @@ mod utils;
|
|||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use dioxus::Attribute;
|
||||
use dioxus_core as dioxus;
|
||||
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_core_macro::{rsx, Props};
|
||||
use dioxus_core_macro::{format_args_f, rsx, Props};
|
||||
use dioxus_html as dioxus_elements;
|
||||
// use wasm_bindgen::{JsCast, JsValue};
|
||||
|
||||
use crate::utils::strip_slash_suffix;
|
||||
|
||||
/// Initialize the app's router service and provide access to `Link` components
|
||||
pub fn use_router<R: 'static>(cx: &ScopeState, f: impl Fn(&str) -> R) -> &R {
|
||||
let r = f("/");
|
||||
cx.use_hook(
|
||||
|_| {
|
||||
//
|
||||
r
|
||||
},
|
||||
|f| f,
|
||||
)
|
||||
}
|
||||
|
||||
pub trait Routable: 'static + Send + Clone + PartialEq {}
|
||||
impl<T> Routable for T where T: 'static + Send + Clone + PartialEq {}
|
||||
|
||||
|
@ -30,20 +44,29 @@ pub struct LinkProps<'a, R: Routable> {
|
|||
/// Link { to: Route::Home, href: Route::as_url }
|
||||
///
|
||||
/// ```
|
||||
href: fn(&R) -> String,
|
||||
#[props(default, setter(strip_option))]
|
||||
href: Option<&'a str>,
|
||||
|
||||
#[props(default, setter(strip_option))]
|
||||
class: Option<&'a str>,
|
||||
|
||||
#[builder(default)]
|
||||
children: Element<'a>,
|
||||
|
||||
#[props(default)]
|
||||
attributes: Option<&'a [Attribute<'a>]>,
|
||||
}
|
||||
|
||||
pub fn Link<'a, R: Routable>(cx: Scope<'a, LinkProps<'a, R>>) -> Element {
|
||||
let service = todo!();
|
||||
// let service = todo!();
|
||||
// let service: todo!() = use_router_service::<R>(&cx)?;
|
||||
// cx.render(rsx! {
|
||||
// a {
|
||||
// href: format_args!("{}", (cx.props.href)(&cx.props.to)),
|
||||
// onclick: move |_| service.push_route(cx.props.to.clone()),
|
||||
// // todo!() {&cx.props.children},
|
||||
// }
|
||||
// })
|
||||
let class = cx.props.class.unwrap_or("");
|
||||
cx.render(rsx! {
|
||||
a {
|
||||
href: "#",
|
||||
class: "{class}",
|
||||
{&cx.props.children}
|
||||
// onclick: move |_| service.push_route(cx.props.to.clone()),
|
||||
// href: format_args!("{}", (cx.props.href)(&cx.props.to)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ fn main() {
|
|||
static App: Component<()> = |cx| {
|
||||
let mut count = use_state(&cx, || 0);
|
||||
|
||||
cx.push_task(|| async move {
|
||||
cx.push_future(|| async move {
|
||||
TimeoutFuture::new(100).await;
|
||||
count += 1;
|
||||
});
|
||||
|
|
|
@ -15,26 +15,27 @@ fn main() {
|
|||
}
|
||||
|
||||
static App: Component<()> = |cx| {
|
||||
let doggo = cx.suspend(|| async move {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Doggo {
|
||||
message: String,
|
||||
}
|
||||
todo!("suspense is broken")
|
||||
// let doggo = suspend(|| async move {
|
||||
// #[derive(serde::Deserialize)]
|
||||
// struct Doggo {
|
||||
// message: String,
|
||||
// }
|
||||
|
||||
let src = reqwest::get("https://dog.ceo/api/breeds/image/random")
|
||||
.await
|
||||
.expect("Failed to fetch doggo")
|
||||
.json::<Doggo>()
|
||||
.await
|
||||
.expect("Failed to parse doggo")
|
||||
.message;
|
||||
// let src = reqwest::get("https://dog.ceo/api/breeds/image/random")
|
||||
// .await
|
||||
// .expect("Failed to fetch doggo")
|
||||
// .json::<Doggo>()
|
||||
// .await
|
||||
// .expect("Failed to parse doggo")
|
||||
// .message;
|
||||
|
||||
rsx!(cx, img { src: "{src}" })
|
||||
});
|
||||
// rsx!(cx, img { src: "{src}" })
|
||||
// });
|
||||
|
||||
rsx!(cx, div {
|
||||
h1 {"One doggo coming right up"}
|
||||
button { onclick: move |_| cx.needs_update(), "Get a new doggo" }
|
||||
{doggo}
|
||||
})
|
||||
// rsx!(cx, div {
|
||||
// h1 {"One doggo coming right up"}
|
||||
// button { onclick: move |_| cx.needs_update(), "Get a new doggo" }
|
||||
// {doggo}
|
||||
// })
|
||||
};
|
||||
|
|
|
@ -89,7 +89,7 @@ mod ric_raf;
|
|||
/// }
|
||||
/// ```
|
||||
pub fn launch(root_component: Component<()>) {
|
||||
launch_with_props(root_component, ());
|
||||
launch_with_props(root_component, (), |c| c);
|
||||
}
|
||||
|
||||
/// Launches the VirtualDOM from the specified component function and props.
|
||||
|
|
|
@ -209,7 +209,7 @@ pub mod events {
|
|||
|
||||
pub mod prelude {
|
||||
pub use dioxus_core::prelude::*;
|
||||
pub use dioxus_core_macro::{format_args_f, rsx, Props, Routable};
|
||||
pub use dioxus_core_macro::{format_args_f, inline_props, rsx, Props, Routable};
|
||||
pub use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
pub use dioxus_hooks::*;
|
||||
pub use dioxus_html as dioxus_elements;
|
||||
|
|
Loading…
Reference in a new issue