From 45ebcf6f0e9f1f86601e83d83a55797e21628a85 Mon Sep 17 00:00:00 2001 From: JtotheThree Date: Fri, 7 Jan 2022 20:01:52 -0600 Subject: [PATCH 001/256] form prevent_default --- packages/html/src/elements.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 1a2f36e57..4b53f91f0 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1047,7 +1047,7 @@ impl input { /// - `text` /// - `time` /// - `url` - /// - `week` + /// - `week` pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { cx.attr("type", val, None, false) } @@ -1106,6 +1106,12 @@ impl a { } } +impl form { + pub fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { + cx.attr("dioxus-prevent-default", val, None, false) + } +} + builder_constructors! { // SVG components /// Build a From e69fbb4baa3de3902a746a0cb2b06338e33dcaa9 Mon Sep 17 00:00:00 2001 From: JtotheThree Date: Fri, 7 Jan 2022 20:20:52 -0600 Subject: [PATCH 002/256] login_form example --- examples/login_form.rs | 89 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 examples/login_form.rs diff --git a/examples/login_form.rs b/examples/login_form.rs new file mode 100644 index 000000000..fdd1843ce --- /dev/null +++ b/examples/login_form.rs @@ -0,0 +1,89 @@ +//! This example demonstrates the following: +//! Futures in a callback, Router, and Forms + +use dioxus::events::*; +use dioxus::prelude::*; +use dioxus::router::{Link, Router, Route, RouterService}; + +fn main() { + dioxus::desktop::launch(APP); +} + +static APP: Component = |cx| { + cx.render(rsx!{ + Router { + Route { to: "/", home() } + Route { to: "/login", login() } + } + }) +}; + +fn home(cx: Scope) -> Element { + cx.render(rsx! { + h1 { "Welcome Home" } + Link { to: "/login", "Login" } + }) +} + + +fn login(cx: Scope) -> Element { + let username = use_state(&cx, String::new); + let password = use_state(&cx, String::new); + + let service = cx.consume_context::()?; + + let onsubmit = move |_| { + cx.push_future({ + let (username, password) = (username.get().clone(), password.get().clone()); + let service = service.clone(); + + async move { + let params = [ + ("username", username.to_string()), + ("password", password.to_string()) + ]; + + let resp = reqwest::Client::new() + .post("http://localhost/login") + .form(¶ms) + .send() + .await; + + match resp { + Ok(data) => { + // Parse data from here, such as storing a response token + service.push_route("/"); + } + Err(err) => {} //Handle any errors from the fetch here + } + } + }); + }; + + cx.render(rsx!{ + h1 { "Login" } + form { + onsubmit: onsubmit, + // Prevent the default behavior of
to post + prevent_default: "onsubmit", + input { + oninput: move |evt| username.set(evt.value.clone()) + } + label { + "Username" + } + br {} + input { + oninput: move |evt| password.set(evt.value.clone()), + r#type: "password" + } + label { + "Password" + } + br {} + button { + "Login" + } + } + }) +} \ No newline at end of file From bdf234d72804d984e286e4c559e1be5ef7455785 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 9 Jan 2022 22:28:07 -0500 Subject: [PATCH 003/256] docs: improve components and elements --- docs/guide/.vscode/spellright.dict | 2 + docs/guide/src/SUMMARY.md | 9 +- docs/guide/src/elements/components.md | 36 ---- docs/guide/src/elements/lists.md | 21 +- docs/guide/src/elements/propsmacro.md | 48 ++++- docs/guide/src/elements/special_attributes.md | 181 ++++++++++++++++++ docs/guide/src/elements/vnodes.md | 6 +- docs/guide/src/interactivity/user_input.md | 2 + docs/guide/src/state/index.md | 19 +- 9 files changed, 267 insertions(+), 57 deletions(-) create mode 100644 docs/guide/.vscode/spellright.dict create mode 100644 docs/guide/src/elements/special_attributes.md diff --git a/docs/guide/.vscode/spellright.dict b/docs/guide/.vscode/spellright.dict new file mode 100644 index 000000000..0c8e14f46 --- /dev/null +++ b/docs/guide/.vscode/spellright.dict @@ -0,0 +1,2 @@ +oninput +Webview diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 2e77d4ab8..94a38800d 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -5,12 +5,13 @@ - [Hello, World!](hello_world.md) - [Describing the UI](elements/index.md) - [Intro to Elements](elements/vnodes.md) - - [Intro to Components](elements/components.md) - - [The Props Macro](elements/propsmacro.md) - - [Reusing, Importing, and Exporting Components](elements/exporting_components.md) - - [Passing children and attributes](elements/component_children.md) - [Conditional Rendering](elements/conditional_rendering.md) - [Lists](elements/lists.md) + - [Special Attributes](elements/special_attributes.md) +- [Components](elements/components.md) + - [Component Properties](elements/propsmacro.md) + - [Reusing, Importing, and Exporting Components](elements/exporting_components.md) + - [Component Children and Attributes](elements/component_children.md) - [Adding Interactivity](interactivity/index.md) - [Hooks and Internal State](interactivity/hooks.md) - [Event handlers](interactivity/event_handlers.md) diff --git a/docs/guide/src/elements/components.md b/docs/guide/src/elements/components.md index 8567e9270..bd22bb88c 100644 --- a/docs/guide/src/elements/components.md +++ b/docs/guide/src/elements/components.md @@ -156,42 +156,6 @@ 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 inline_props macro - -Yes - *another* macro! However, this one is entirely optional. - -For internal components, we provide the `inline_props` macro, which will let you embed your `Props` definition right into the function arguments of your component. - -Our title card above would be transformed from: - -```rust -#[derive(Props, PartialEq)] -struct TitleCardProps { - title: String, -} - -fn TitleCard(cx: Scope) -> Element { - cx.render(rsx!{ - h1 { "{cx.props.title}" } - }) -} -``` - -to: - -```rust -#[inline_props] -fn TitleCard(cx: Scope, title: String) -> Element { - cx.render(rsx!{ - h1 { "{title}" } - }) -} -``` - -Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality. - -However, it's great for quickly throwing together an app without dealing with *any* extra boilerplate. - ## The `Scope` object 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. diff --git a/docs/guide/src/elements/lists.md b/docs/guide/src/elements/lists.md index 32dbfd833..4d1d3fdf1 100644 --- a/docs/guide/src/elements/lists.md +++ b/docs/guide/src/elements/lists.md @@ -75,19 +75,16 @@ Next, we're going to define our component: ```rust fn App(cx: Scope) -> Element { - // First, we create a new iterator by mapping the post array - let posts = cx.props.posts.iter().map(|post| rsx!{ - Post { - title: post.title, - age: post.age, - original_poster: post.original_poster - } - }); - - // Finally, we render the post list inside of a container cx.render(rsx!{ - ul { class: "post-list" - {posts} + ul { class: "post-list", + // we can drop an iterator directly into our elements + cx.props.posts.iter().map(|post| rsx!{ + Post { + title: post.title, + age: post.age, + original_poster: post.original_poster + } + }) } }) } diff --git a/docs/guide/src/elements/propsmacro.md b/docs/guide/src/elements/propsmacro.md index 7426a73bf..0821afef7 100644 --- a/docs/guide/src/elements/propsmacro.md +++ b/docs/guide/src/elements/propsmacro.md @@ -1 +1,47 @@ -# The Props Macro +# Component Properties + +All component `properties` must implement the `Properties` trait. The `Props` macro automatically derives this trait but adds some additional functionality. In this section, we'll learn about: + +- Using the props macro +- Memoization through PartialEq +- Optional fields on props +- The inline_props macro + + + + +## The inline_props macro + +Yes - *another* macro! However, this one is entirely optional. + +For internal components, we provide the `inline_props` macro, which will let you embed your `Props` definition right into the function arguments of your component. + +Our title card above would be transformed from: + +```rust +#[derive(Props, PartialEq)] +struct TitleCardProps { + title: String, +} + +fn TitleCard(cx: Scope) -> Element { + cx.render(rsx!{ + h1 { "{cx.props.title}" } + }) +} +``` + +to: + +```rust +#[inline_props] +fn TitleCard(cx: Scope, title: String) -> Element { + cx.render(rsx!{ + h1 { "{title}" } + }) +} +``` + +Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality. + +However, it's great for quickly throwing together an app without dealing with *any* extra boilerplate. diff --git a/docs/guide/src/elements/special_attributes.md b/docs/guide/src/elements/special_attributes.md new file mode 100644 index 000000000..b76b19fd7 --- /dev/null +++ b/docs/guide/src/elements/special_attributes.md @@ -0,0 +1,181 @@ +# Special Attributes + +Dioxus tries its hardest to stay close to React, but there are some divergences and "special behavior" that you should review before moving on. + +In this section, we'll cover special attributes built into Dioxus: + +- `dangerous_inner_html` +- Boolean attributes +- `prevent_default` +- `..Attributes` +- event handlers as string attributes +- `value`, `checked`, and `selected` + +## The HTML escape hatch: `dangerous_inner_html` + +One thing you might've missed from React is the ability to render raw HTML directly to the DOM. If you're working with pre-rendered assets, output from templates, or output from a JS library, then you might want to pass HTML directly instead of going through Dioxus. In these instances, reach for `dangerous_inner_html`. + +For example, shipping a markdown-to-Dioxus converter might significantly bloat your final application size. Instead, you'll want to pre-render your markdown to HTML and then include the HTML directly in your output. We use this approach for the `http://dioxuslabs.com` site: + + +```rust +fn BlogPost(cx: Scope) -> Element { + let contents = include_str!("../post.html"); + cx.render(rsx!{ + div { + class: "markdown", + dangerous_inner_html: "{contents}", + } + }) +} +``` + +> Note! + +This attribute is called "dangerous_inner_html" because it is DANGEROUS. If you're not careful, you can easily expose cross-site-scripting (XSS) attacks to your users. If you're handling untrusted input, make sure to escape your HTML before passing it into `dangerous_inner_html`. + + +## Boolean Attributes + +Most attributes, when rendered, will be rendered exactly as the input you provided. However, some attributes are considered "boolean" attributes and just their presence determines whether or not they affect the output. For these attributes, a provided value of `"false"` will cause them to be removed from the target element. + +So the input of: + +```rust +rsx!{ + div { + hidden: "false", + "hello" + } +} +``` +would actually render an output of +```html +
hello
+``` + +Notice how `hidden` is not present in the final output? + +Not all attributes work like this however. Only *these specific attributes* are whitelisted to have this behavior: + +- `allowfullscreen` +- `allowpaymentrequest` +- `async` +- `autofocus` +- `autoplay` +- `checked` +- `controls` +- `default` +- `defer` +- `disabled` +- `formnovalidate` +- `hidden` +- `ismap` +- `itemscope` +- `loop` +- `multiple` +- `muted` +- `nomodule` +- `novalidate` +- `open` +- `playsinline` +- `readonly` +- `required` +- `reversed` +- `selected` +- `truespeed` + +For any other attributes, a value of `"false"` will be sent directly to the DOM. + +## `prevent_default` + +Currently, preventing default on events from an event handler is not possible from Desktop/Mobile. Until this is supported, it's possible to prevent default using the `prevent_default` attribute. + +> Note: you cannot conditionally prevent default with this approach. This is a limitation until synchronous event handling is available across the Webview boundary + +To use `prevent_default`, simple use the `prevent_default` attribute and set it to the name of the event handler you want to prevent default on. We can attach this attribute multiple times for multiple attributes. + +```rust +rsx!{ + input { + oninput: move |_| {}, + prevent_default: "oninput", + + onclick: move |_| {}, + prevent_default: "onclick", + } +} +``` + +## `..Attributes` + +Just like Dioxus supports spreading component props into components, we also support spreading attributes into elements. This lets you pass any arbitrary attributes through components into elements. + + +```rust +#[derive(Props)] +pub struct InputProps<'a> { + pub children: Element<'a>, + pub attributes: Attribute<'a> +} + +pub fn StateInput<'a>(cx: Scope<'a, InputProps<'a>>) -> Element { + cx.render(rsx! ( + input { + ..cx.props.attributes, + &cx.props.children, + } + )) +} +``` + +## Controlled inputs and `value`, `checked`, and `selected` + + +In Dioxus, there is a distinction between controlled and uncontrolled inputs. Most inputs you'll use are "controlled," meaning we both drive the `value` of the input and react to the `oninput`. + +Controlled components: +```rust +let value = use_state(&cx, || String::from("hello world")); + +rsx! { + input { + oninput: move |evt| value.set(evt.value.clone()), + value: "{value}", + } +} +``` + +With uncontrolled inputs, we won't actually drive the value from the component. This has its advantages when we don't want to re-render the component when the user inputs a value. We could either select the element directly - something Dioxus doesn't support across platforms - or we could handle `oninput` and modify a value without causing an update: + +```rust +let value = use_ref(&cx, || String::from("hello world")); + +rsx! { + input { + oninput: move |evt| *value.write_silent() = evt.value.clone(), + // no "value" is driven + } +} +``` + +## Strings for handlers like `onclick` + +For element fields that take a handler like `onclick` or `oninput`, Dioxus will let you attach a closure. Alternatively, you can also pass a string using normal attribute syntax and assign this attribute on the DOM. + +This lets you escape into JavaScript (only if your renderer can execute JavaScript). + +```rust +rsx!{ + div { + // handle oninput with rust + oninput: move |_| {}, + + // or handle oninput with javascript + oninput: "alert('hello world')", + } +} + +``` + +## Wrapping up diff --git a/docs/guide/src/elements/vnodes.md b/docs/guide/src/elements/vnodes.md index b407ed02f..5ccae75f3 100644 --- a/docs/guide/src/elements/vnodes.md +++ b/docs/guide/src/elements/vnodes.md @@ -81,13 +81,13 @@ let name = "Bob"; rsx! ( "hello {name}" ) ``` -Unfortunately, you cannot drop in arbitrary expressions directly into the string literal. In the cases where we need to compute a complex value, we'll want to use `format_args!` directly. Due to specifics of how the `rsx!` macro (we'll cover later), our call to `format_args` must be contained within curly braces *and* square braces. +Unfortunately, you cannot drop in arbitrary expressions directly into the string literal. In the cases where we need to compute a complex value, we'll want to use `format_args!` directly. Due to specifics of how the `rsx!` macro (we'll cover later), our call to `format_args` must be contained within square braces. ```rust -rsx!( {[format_args!("Hello {}", if enabled { "Jack" } else { "Bob" } )]} ) +rsx!( {format_args!("Hello {}", if enabled { "Jack" } else { "Bob" } )] ) ``` -Alternatively, `&str` can be included directly, though it must be inside of an array: +Alternatively, `&str` can be included directly, though it must be inside of square braces: ```rust rsx!( "Hello ", [if enabled { "Jack" } else { "Bob" }] ) diff --git a/docs/guide/src/interactivity/user_input.md b/docs/guide/src/interactivity/user_input.md index 2424e7f14..746443071 100644 --- a/docs/guide/src/interactivity/user_input.md +++ b/docs/guide/src/interactivity/user_input.md @@ -1 +1,3 @@ # User Input and Controlled Components + +Handling user input is one of the most common things your app will do, but it can be tricky diff --git a/docs/guide/src/state/index.md b/docs/guide/src/state/index.md index 9b3e054fc..3740dd29d 100644 --- a/docs/guide/src/state/index.md +++ b/docs/guide/src/state/index.md @@ -4,5 +4,22 @@ Every app you'll build with Dioxus will have some sort of state that needs to be In this chapter, we'll cover the various ways to manage state, the appropriate terminology, various patterns, and then take an overview - ## Terminology + + + + + + +## Important hook: `use_state` + + + + + +## Important hook: `use_ref` + + + + +## `provide_context` and `consume_context` From 1560e2daca086f7914b5e371ff15425db53e5afd Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 11 Jan 2022 01:11:47 -0500 Subject: [PATCH 004/256] docs: add more docs --- docs/guide/.vscode/spellright.dict | 1 + docs/guide/src/SUMMARY.md | 7 +- docs/guide/src/async/fetching.md | 3 + docs/guide/src/async/index.md | 3 + docs/guide/src/elements/composing.md | 43 ++++++ docs/guide/src/elements/lists.md | 45 ++---- docs/guide/src/elements/propsmacro.md | 145 ++++++++++++++++++ docs/guide/src/elements/special_attributes.md | 25 ++- .../guide/src/interactivity/event_handlers.md | 2 + docs/guide/src/interactivity/lifecycles.md | 2 + docs/guide/src/interactivity/user_input.md | 3 + docs/guide/src/state/errorhandling.md | 1 + docs/guide/src/state/index.md | 1 + docs/guide/src/state/liftingstate.md | 3 + docs/guide/src/state/localstate.md | 3 + docs/guide/src/state/sharedstate.md | 6 + docs/guide/src/tutorial/components.md | 2 + docs/guide/src/tutorial/state.md | 2 + docs/guide/src/tutorial/structure.md | 3 + docs/guide/src/tutorial/styling.md | 3 + 20 files changed, 259 insertions(+), 44 deletions(-) create mode 100644 docs/guide/src/elements/composing.md diff --git a/docs/guide/.vscode/spellright.dict b/docs/guide/.vscode/spellright.dict index 0c8e14f46..572d1f65c 100644 --- a/docs/guide/.vscode/spellright.dict +++ b/docs/guide/.vscode/spellright.dict @@ -1,2 +1,3 @@ oninput Webview +idanarye diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 94a38800d..54d9fcd4c 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -9,9 +9,10 @@ - [Lists](elements/lists.md) - [Special Attributes](elements/special_attributes.md) - [Components](elements/components.md) - - [Component Properties](elements/propsmacro.md) - - [Reusing, Importing, and Exporting Components](elements/exporting_components.md) - - [Component Children and Attributes](elements/component_children.md) + - [Properties](elements/propsmacro.md) + - [Reusing, Importing, and Exporting](elements/exporting_components.md) + - [Children and Attributes](elements/component_children.md) + - [Composing Components](elements/composing.md) - [Adding Interactivity](interactivity/index.md) - [Hooks and Internal State](interactivity/hooks.md) - [Event handlers](interactivity/event_handlers.md) diff --git a/docs/guide/src/async/fetching.md b/docs/guide/src/async/fetching.md index 99e726c7a..b31c3d03b 100644 --- a/docs/guide/src/async/fetching.md +++ b/docs/guide/src/async/fetching.md @@ -1 +1,4 @@ # Fetching + + +This section is currently under construction! 🏗 diff --git a/docs/guide/src/async/index.md b/docs/guide/src/async/index.md index 41a8b9231..4592f5ed3 100644 --- a/docs/guide/src/async/index.md +++ b/docs/guide/src/async/index.md @@ -17,3 +17,6 @@ Writing apps that deal with Send/Sync can be frustrating at times. Under the hoo All async code in your app is polled on a `LocalSet`, so any async code we w + + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/elements/composing.md b/docs/guide/src/elements/composing.md new file mode 100644 index 000000000..e4b00de2e --- /dev/null +++ b/docs/guide/src/elements/composing.md @@ -0,0 +1,43 @@ +# Composing Components + +So far, we've talked about declaring new components and setting up their properties. However, we haven't really talked about how components work together and how your app is updated. + +In this section, we'll talk about: + +- Sharing data between components +- How the UI is updated from input and state changes +- Forcing renders +- How renders propagate +- + + +### Rendering our posts with a PostList component + +Let's start by modeling this problem with a component and some properties. + +For this example, we're going to use the borrowed component syntax since we probably have a large list of posts that we don't want to clone every time we render the Post List. + +```rust +#[derive(Props, PartialEq)] +struct PostListProps<'a> { + posts: &'a [PostData] +} +``` +Next, we're going to define our component: + +```rust +fn App(cx: Scope) -> Element { + cx.render(rsx!{ + ul { class: "post-list", + // we can drop an iterator directly into our elements + cx.props.posts.iter().map(|post| rsx!{ + Post { + title: post.title, + age: post.age, + original_poster: post.original_poster + } + }) + } + }) +} +``` diff --git a/docs/guide/src/elements/lists.md b/docs/guide/src/elements/lists.md index 4d1d3fdf1..cdf9f6e6b 100644 --- a/docs/guide/src/elements/lists.md +++ b/docs/guide/src/elements/lists.md @@ -45,10 +45,21 @@ Finally, we can include this list in the final structure: ```rust rsx!( ul { - {name_list} + name_list } ) ``` +Or, we can include the iterator inline: +```rust +rsx!( + ul { + names.iter().map(|name| rsx!( + li { "{name}" } + )) + } +) +``` + The HTML-rendered version of this list would follow what you would expect: ```html
    @@ -59,38 +70,6 @@ The HTML-rendered version of this list would follow what you would expect:
``` -### Rendering our posts with a PostList component - -Let's start by modeling this problem with a component and some properties. - -For this example, we're going to use the borrowed component syntax since we probably have a large list of posts that we don't want to clone every time we render the Post List. - -```rust -#[derive(Props, PartialEq)] -struct PostListProps<'a> { - posts: &'a [PostData] -} -``` -Next, we're going to define our component: - -```rust -fn App(cx: Scope) -> Element { - cx.render(rsx!{ - ul { class: "post-list", - // we can drop an iterator directly into our elements - cx.props.posts.iter().map(|post| rsx!{ - Post { - title: post.title, - age: post.age, - original_poster: post.original_poster - } - }) - } - }) -} -``` - - ## Filtering Iterators Rust's iterators are extremely powerful, especially when used for filtering tasks. When building user interfaces, you might want to display a list of items filtered by some arbitrary check. diff --git a/docs/guide/src/elements/propsmacro.md b/docs/guide/src/elements/propsmacro.md index 0821afef7..1e4e994b3 100644 --- a/docs/guide/src/elements/propsmacro.md +++ b/docs/guide/src/elements/propsmacro.md @@ -9,6 +9,151 @@ All component `properties` must implement the `Properties` trait. The `Props` ma +## Using the Props Macro + +All `properties` that your components take must implement the `Properties` trait. The simplest props you can use is simply `()` - or no value at all. `Scope` is generic over your component's props and actually defaults to `()`. + +```rust +// this scope +Scope<()> + +// is the same as this scope +Scope +``` + +If we wanted to define a component with its own props, we would create a new struct and tack on the `Props` derive macro: + +```rust +#[derive(Props)] +struct MyProps { + name: String +} +``` +This particular code will not compile - all `Props` must either a) borrow from their parent or b) implement `PartialEq`. Since our props do not borrow from their parent, they are `'static` and must implement PartialEq. + +For an owned example: +```rust +#[derive(Props, PartialEq)] +struct MyProps { + name: String +} +``` + +For a borrowed example: +```rust +#[derive(Props)] +struct MyProps<'a> { + name: &'a str +} +``` + +Then, to use these props in our component, we simply swap out the generic parameter on scope. + +For owned props, we just drop it in: + +```rust +fn Demo(cx: Scope) -> Element { + todo!() +} +``` + +However, for props that borrow data, we need to explicitly declare lifetimes. Rust does not know that our props and our component share the same lifetime, so must explicitly attach a lifetime in two places: + +```rust +fn Demo<'a>(cx: Scope<'a, MyProps<'a>>) -> Element { + todo!() +} +``` + +By putting the `'a` lifetime on Scope and our Props, we can now borrow data from our parent and pass it on to our children. + + +## Memoization + +If you're coming from React, you might be wondering how memoization fits in. For our purpose, memoization is the process in which we check if a component actually needs to be re-rendered when its props change. If a component's properties change but they wouldn't necessarily affect the output, then we don't need to actually re-render the component. + +For example, let's say we have a component that has two children: + +```rust +fn Demo(cx: Scope) -> Element { + let name = use_state(&cx, || String::from("bob")); + let age = use_state(&cx, || 21); + + cx.render(rsx!{ + Name { name: name } + Age { age: age } + }) +} +``` + +If `name` changes but `age` does not, then there is no reason to re-render our `Age` component since the contents of its props did not meaningfully change. + + +Dioxus implements memoization by default, which means you can always rely on props with `PartialEq` or no props at all to act as barriers in your app. This can be extremely useful when building larger apps where properties frequently change. By moving our state into a global state management solution, we can achieve precise, surgical re-renders, improving the performance of our app. + + +However, for components that borrow values from their parents, we cannot safely memoize them. + +For example, this component borrows `&str` - and if the parent re-renders, then the actual reference to `str` will probably be different. Since the data is borrowed, we need to pass a new version down the tree. + +```rust +#[derive(Props)] +struct MyProps<'a> { + name: &'a str +} + +fn Demo<'a>(cx: Scope<'a, MyProps<'a>>) -> Element { + todo!() +} +``` + +TLDR: +- if you see props with a lifetime or generics, it cannot be memoized +- memoization is done automatically through the `PartialEq` trait +- components with empty props can act as memoization barriers + +## Optional Fields + +Dioxus' `Props` macro is very similar to [@idanarye](https://github.com/idanarye)'s [TypedBuilder crate](https://github.com/idanarye/rust-typed-builder) and supports many of the same parameters. + +For example, you can easily create optional fields by attaching the `optional` modifier to a field. + +```rust +#[derive(Props, PartialEq)] +struct MyProps { + name: String, + + #[props(optional)] + description: Option +} + +fn Demo(cx: MyProps) -> Element { + ... +} +``` + +Then, we can completely omit the description field when calling the component: + +```rust +rsx!{ + Demo { + name: "Thing".to_string(), + // description is omitted + } +} +``` + +The `optional` modifier is a combination of two separate modifiers: `default` and `strip_option`. The full list of modifiers includes: + +- `default` - automatically add the field using its `Default` implementation +- `strip_option` - automatically wrap values at the call site in `Some` +- `optional` - combine both `default` and `strip_option` +- `into` - automatically call `into` on the value at the callsite + +For more information on how tags work, check out the [TypedBuilder](https://github.com/idanarye/rust-typed-builder) crate. However, all attributes for props in Dioxus are flattened (no need for `setter` syntax) and the `optional` field is new. + + + ## The inline_props macro diff --git a/docs/guide/src/elements/special_attributes.md b/docs/guide/src/elements/special_attributes.md index b76b19fd7..9a1122a3e 100644 --- a/docs/guide/src/elements/special_attributes.md +++ b/docs/guide/src/elements/special_attributes.md @@ -30,9 +30,7 @@ fn BlogPost(cx: Scope) -> Element { } ``` -> Note! - -This attribute is called "dangerous_inner_html" because it is DANGEROUS. If you're not careful, you can easily expose cross-site-scripting (XSS) attacks to your users. If you're handling untrusted input, make sure to escape your HTML before passing it into `dangerous_inner_html`. +> Note! This attribute is called "dangerous_inner_html" because it is DANGEROUS. If you're not careful, you can easily expose cross-site-scripting (XSS) attacks to your users. If you're handling untrusted input, make sure to escape your HTML before passing it into `dangerous_inner_html`. ## Boolean Attributes @@ -87,13 +85,13 @@ Not all attributes work like this however. Only *these specific attributes* are For any other attributes, a value of `"false"` will be sent directly to the DOM. -## `prevent_default` +## Stopping form input and navigation with `prevent_default` -Currently, preventing default on events from an event handler is not possible from Desktop/Mobile. Until this is supported, it's possible to prevent default using the `prevent_default` attribute. +Currently, calling `prevent_default` on events in EventHandlers is not possible from Desktop/Mobile. Until this is supported, it's possible to prevent default using the `prevent_default` attribute. > Note: you cannot conditionally prevent default with this approach. This is a limitation until synchronous event handling is available across the Webview boundary -To use `prevent_default`, simple use the `prevent_default` attribute and set it to the name of the event handler you want to prevent default on. We can attach this attribute multiple times for multiple attributes. +To use `prevent_default`, simply attach the `prevent_default` attribute to a given element and set it to the name of the event handler you want to prevent default on. We can attach this attribute multiple times for multiple attributes. ```rust rsx!{ @@ -107,7 +105,7 @@ rsx!{ } ``` -## `..Attributes` +## Passing attributes into children: `..Attributes` Just like Dioxus supports spreading component props into components, we also support spreading attributes into elements. This lets you pass any arbitrary attributes through components into elements. @@ -131,7 +129,6 @@ pub fn StateInput<'a>(cx: Scope<'a, InputProps<'a>>) -> Element { ## Controlled inputs and `value`, `checked`, and `selected` - In Dioxus, there is a distinction between controlled and uncontrolled inputs. Most inputs you'll use are "controlled," meaning we both drive the `value` of the input and react to the `oninput`. Controlled components: @@ -179,3 +176,15 @@ rsx!{ ``` ## Wrapping up + +We've reached just about the end of what you can do with elements without venturing into "advanced" territory. + +In this chapter, we learned: +- How to declare elements +- How to conditionally render parts of your UI +- How to render lists +- Which attributes are "special" + +There's more to elements! For further reading, check out: + +- [Custom Elements]() diff --git a/docs/guide/src/interactivity/event_handlers.md b/docs/guide/src/interactivity/event_handlers.md index ee4560b1e..8fcfe6edb 100644 --- a/docs/guide/src/interactivity/event_handlers.md +++ b/docs/guide/src/interactivity/event_handlers.md @@ -1 +1,3 @@ # Event handlers + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/interactivity/lifecycles.md b/docs/guide/src/interactivity/lifecycles.md index 07a02f4ea..fdaa5b5e3 100644 --- a/docs/guide/src/interactivity/lifecycles.md +++ b/docs/guide/src/interactivity/lifecycles.md @@ -1 +1,3 @@ # Lifecycle, updates, and effects + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/interactivity/user_input.md b/docs/guide/src/interactivity/user_input.md index 746443071..1e7ebbcbd 100644 --- a/docs/guide/src/interactivity/user_input.md +++ b/docs/guide/src/interactivity/user_input.md @@ -1,3 +1,6 @@ # User Input and Controlled Components Handling user input is one of the most common things your app will do, but it can be tricky + + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/state/errorhandling.md b/docs/guide/src/state/errorhandling.md index 40436521f..767a314b0 100644 --- a/docs/guide/src/state/errorhandling.md +++ b/docs/guide/src/state/errorhandling.md @@ -9,3 +9,4 @@ fn App((cx, props): Component) -> Element { } ``` +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/state/index.md b/docs/guide/src/state/index.md index 3740dd29d..ed3a7e135 100644 --- a/docs/guide/src/state/index.md +++ b/docs/guide/src/state/index.md @@ -7,6 +7,7 @@ In this chapter, we'll cover the various ways to manage state, the appropriate t ## Terminology +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/state/liftingstate.md b/docs/guide/src/state/liftingstate.md index 3e906a0ca..fec62d2c7 100644 --- a/docs/guide/src/state/liftingstate.md +++ b/docs/guide/src/state/liftingstate.md @@ -1 +1,4 @@ # Lifting State + + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/state/localstate.md b/docs/guide/src/state/localstate.md index 29085ac45..e54c1e45f 100644 --- a/docs/guide/src/state/localstate.md +++ b/docs/guide/src/state/localstate.md @@ -1 +1,4 @@ # Local State + + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/state/sharedstate.md b/docs/guide/src/state/sharedstate.md index ec1ede1d1..148b6eb66 100644 --- a/docs/guide/src/state/sharedstate.md +++ b/docs/guide/src/state/sharedstate.md @@ -1 +1,7 @@ # Global State + + +cx.provide_context() +cx.consume_context() + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/tutorial/components.md b/docs/guide/src/tutorial/components.md index bfbb760b3..82bae6cd7 100644 --- a/docs/guide/src/tutorial/components.md +++ b/docs/guide/src/tutorial/components.md @@ -1 +1,3 @@ # Defining Components + +This section is currently under construction! 🏗 diff --git a/docs/guide/src/tutorial/state.md b/docs/guide/src/tutorial/state.md index 35df690d6..6026c7546 100644 --- a/docs/guide/src/tutorial/state.md +++ b/docs/guide/src/tutorial/state.md @@ -1 +1,3 @@ # Defining State + +This section is currently under construction! 🏗 diff --git a/docs/guide/src/tutorial/structure.md b/docs/guide/src/tutorial/structure.md index 1cbb47579..eaf8afe43 100644 --- a/docs/guide/src/tutorial/structure.md +++ b/docs/guide/src/tutorial/structure.md @@ -1 +1,4 @@ # Structuring our app + + +This section is currently under construction! 🏗 diff --git a/docs/guide/src/tutorial/styling.md b/docs/guide/src/tutorial/styling.md index 3e90761b5..b25db14d2 100644 --- a/docs/guide/src/tutorial/styling.md +++ b/docs/guide/src/tutorial/styling.md @@ -1 +1,4 @@ # Styling + + +This section is currently under construction! 🏗 From baf722de11efca34706e11fc80bcdc28efcabf5d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 15 Jan 2022 20:14:51 -0500 Subject: [PATCH 005/256] docs: add a roadmap --- .vscode/spellright.dict | 2 + docs/guide/src/ROADMAP.md | 114 ++++++++++++++++++++++++++++++++++++++ docs/guide/src/SUMMARY.md | 1 + docs/guide/src/setup.md | 31 ++++++++--- 4 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 docs/guide/src/ROADMAP.md diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index bdaf7abb8..1333ce02e 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -69,3 +69,5 @@ Jank noderef reborrow VirtualDoms +bootstrapper +WebkitGtk diff --git a/docs/guide/src/ROADMAP.md b/docs/guide/src/ROADMAP.md new file mode 100644 index 000000000..4f78e6043 --- /dev/null +++ b/docs/guide/src/ROADMAP.md @@ -0,0 +1,114 @@ +# Roadmap & Feature-set + +Before we dive into Dioxus, feel free to take a look at our feature set and roadmap to see if what Dioxus can do today works for you. + +If a feature that you need doesn't exist or you want to contribute to projects on the roadmap, feel free to get involved by [joining the discord](https://discord.gg/XgGxMSkvUM). + +Generally, here's the status of each platform: + +- **Web**: Dioxus is a great choice for pure web-apps - especially for CRUD/complex apps. However, it does lack the ecosystem of React, so you might be missing a component library or some useful hook. + +- **SSR**: Dioxus is a great choice for pre-rendering, hydration, and rendering HTML on a web endpoint. Be warned - the VirtualDom is not (currently) `Send + Sync`. + +- **Desktop**: You can build very competent single-window desktop apps right now. However, multi-window apps require support from Dioxus core and are not ready. + +- **Mobile**: Mobile support is very young. You'll be figuring things out as you go and there are not many support crates for peripherals. + +- **LiveView**: LiveView support is very young. You'll be figuring things out as you go. Thankfully, none of it is too hard and any work can be upstreamed into Dioxus. + +## Features +--- + +| Feature | Status | Description | +| ------------------------- | ------ | -------------------------------------------------------------------- | +| Conditional Rendering | ✅ | if/then to hide/show component | +| Map, Iterator | ✅ | map/filter/reduce to produce rsx! | +| Keyed Components | ✅ | advanced diffing with keys | +| Web | ✅ | renderer for web browser | +| Desktop (webview) | ✅ | renderer for desktop | +| Shared State (Context) | ✅ | share state through the tree | +| Hooks | ✅ | memory cells in components | +| SSR | ✅ | render directly to string | +| Component Children | ✅ | cx.children() as a list of nodes | +| Headless components | ✅ | components that don't return real elements | +| Fragments | ✅ | multiple elements without a real root | +| Manual Props | ✅ | Manually pass in props with spread syntax | +| Controlled Inputs | ✅ | stateful wrappers around inputs | +| CSS/Inline Styles | ✅ | syntax for inline styles/attribute groups | +| Custom elements | ✅ | Define new element primitives | +| Suspense | ✅ | schedule future render from future/promise | +| Integrated error handling | ✅ | Gracefully handle errors with ? syntax | +| NodeRef | ✅ | gain direct access to nodes | +| Re-hydration | ✅ | Pre-render to HTML to speed up first contentful paint | +| Jank-Free Rendering | ✅ | Large diffs are segmented across frames for silky-smooth transitions | +| Effects | ✅ | Run effects after a component has been committed to render | +| Portals | 🛠 | Render nodes outside of the traditional tree structure | +| Cooperative Scheduling | 🛠 | Prioritize important events over non-important events | +| Server Components | 🛠 | Hybrid components for SPA and Server | +| Bundle Splitting | 👀 | Efficiently and asynchronously load the app | +| Lazy Components | 👀 | Dynamically load the new components as the page is loaded | +| 1st class global state | ✅ | redux/recoil/mobx on top of context | +| Runs natively | ✅ | runs as a portable binary w/o a runtime (Node) | +| Subtree Memoization | ✅ | skip diffing static element subtrees | +| High-efficiency templates | 🛠 | rsx! calls are translated to templates on the DOM's side | +| Compile-time correct | ✅ | Throw errors on invalid template layouts | +| Heuristic Engine | ✅ | track component memory usage to minimize future allocations | +| Fine-grained reactivity | 👀 | Skip diffing for fine-grain updates | + +- ✅ = implemented and working +- 🛠 = actively being worked on +- 👀 = not yet implemented or being worked on +- ❓ = not sure if will or can implement + + +## Roadmap +--- + + +Core: +- [x] Release of Dioxus Core +- [x] Upgrade documentation to include more theory and be more comprehensive +- [ ] Support for HTML-side templates for lightning-fast dom manipulation +- [ ] Support for multiple renderers for same virtualdom (subtrees) +- [ ] Support for ThreadSafe (Send + Sync) +- [ ] Support for Portals + +SSR +- [x] SSR Support + Hydration +- [ ] Integrated suspense support for SSR + +Desktop +- [ ] Declarative window management +- [ ] Templates for building/bundling +- [ ] Fully native renderer +- [ ] Access to Canvas/WebGL context natively + +Mobile +- [ ] Mobile standard library + - [ ] GPS + - [ ] Camera + - [ ] filesystem + - [ ] Biometrics + - [ ] WiFi + - [ ] Bluetooth + - [ ] Notifications + - [ ] Clipboard + - [ ] + +Bundling (CLI) +- [x] translation from HTML into RSX +- [ ] dev server +- [ ] live reload +- [ ] translation from JSX into RSX +- [ ] hot module replacement +- [ ] code splitting +- [ ] asset macros +- [ ] css pipeline +- [ ] image pipeline + +Essential hooks +- [ ] Router +- [ ] Global state management +- [ ] Resize observer + + diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 54d9fcd4c..d5b7055b5 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -1,6 +1,7 @@ # Summary - [Introduction](README.md) +- [Roadmap](ROADMAP.md) - [Getting Setup](setup.md) - [Hello, World!](hello_world.md) - [Describing the UI](elements/index.md) diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 9b49f6cb5..4aebc69fd 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -20,7 +20,7 @@ Dioxus requires a few main things to get up and running: Dioxus integrates very well with the Rust-Analyzer IDE plugin which will provide appropriate syntax highlighting, code navigation, folding, and more. -### Installing Rust +## Installing Rust Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler. @@ -30,24 +30,39 @@ Once installed, make sure to install wasm32-unknown-unknown as a target if you' rustup target add wasm32-unknown-unknown ``` -### Platform-Specific Dependencies +## Platform-Specific Dependencies If you are running a modern, mainstream operating system, you should need no additional setup to build WebView-based Desktop apps. However, if you are running an older version of Windows or a flavor of linux with no default web rendering engine, you might need to install some additional dependencies. -For windows users: download the [bootstrapper for Webview2 from Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) -For linux users, we need the development libraries for libgtk. +### Windows -```` +Windows Desktop apps depend on WebView2 - a library which should be installed in all modern Windows distributions. If you have Edge installed, then Dioxus will work fine. If you *don't* have Webview2, [then you can install it through Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). MS provides 3 options: + +- 1) A tiny "evergreen" *bootstrapper* which will fetch an installer from Microsoft's CDN +- 2) A tiny *installer* which will fetch Webview2 from Microsoft's CDN +- 3) A statically linked version of Webview2 in your final binary for offline users + +For development purposes, use Option 1. + +### Linux + +Webview Linux apps require WebkitGtk. When distributing, this can be part of your dependency tree in your `.rpm` or `.deb`. However, it's very likely that your users will already have WebkitGtk. + +``` sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev ``` -When distributing onto older Windows platforms or less-mainstream + +If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/en/docs/get-started/setup-linux). +### macOS + +Currently - everything for macOS is built right in! However, you might run into an issue if you're using nightly Rust due to some permissions issues in our Tao dependency (which have been resolved but not published). -### Dioxus-CLI for dev server, bundling, etc. +## Dioxus-CLI for dev server, bundling, etc. We also recommend installing the Dioxus CLI. The Dioxus CLI automates building and packaging for various targets and integrates with simulators, development servers, and app deployment. To install the CLI, you'll need cargo (should be automatically installed with Rust): @@ -63,7 +78,7 @@ $ cargo install --force dioxus-cli We provide this 1st-party tool to save you from having to run potentially untrusted code every time you add a crate to your project - as is standard in the NPM ecosystem. -### Suggested extensions +## Suggested extensions If you want to keep your traditional `npm install XXX` workflow for adding packages, you might want to install `cargo-edit` and a few other fun `cargo` extensions: From 58839f47bae3c49cadd4b41da9a5debebb1def99 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 16 Jan 2022 15:56:48 -0500 Subject: [PATCH 006/256] update: modify usestate to be borrowed --- examples/calculator.rs | 6 +- examples/crm.rs | 2 +- examples/disabled.rs | 2 +- examples/dog_app.rs | 2 +- examples/readme.rs | 6 +- examples/todomvc.rs | 34 ++-- packages/hooks/src/usestate.rs | 212 +++++++++++++++++++++++++ packages/hooks/src/usestate/handle.rs | 215 -------------------------- packages/hooks/src/usestate/mod.rs | 78 ---------- packages/hooks/src/usestate/owned.rs | 99 ------------ 10 files changed, 237 insertions(+), 419 deletions(-) create mode 100644 packages/hooks/src/usestate.rs delete mode 100644 packages/hooks/src/usestate/handle.rs delete mode 100644 packages/hooks/src/usestate/mod.rs delete mode 100644 packages/hooks/src/usestate/owned.rs diff --git a/examples/calculator.rs b/examples/calculator.rs index a849833a3..f7668de88 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -18,7 +18,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let display_value: UseState = use_state(&cx, || String::from("0")); + let display_value = use_state(&cx, || String::from("0")); let input_digit = move |num: u8| { if display_value.get() == "0" { @@ -66,11 +66,11 @@ fn app(cx: Scope) -> Element { class: "calculator-key key-clear", onclick: move |_| { display_value.set(String::new()); - if display_value != "" { + if *display_value != "" { display_value.set("0".into()); } }, - [if display_value == "" { "C" } else { "AC" }] + [if *display_value == "" { "C" } else { "AC" }] } button { class: "calculator-key key-sign", diff --git a/examples/crm.rs b/examples/crm.rs index d4f3d1579..a206e0f43 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -38,7 +38,7 @@ fn app(cx: Scope) -> Element { h1 {"Dioxus CRM Example"} - match *scene { + match scene.get() { Scene::ClientsList => rsx!( div { class: "crm", h2 { margin_bottom: "10px", "List of clients" } diff --git a/examples/disabled.rs b/examples/disabled.rs index 390a5550f..5bfa5bbb8 100644 --- a/examples/disabled.rs +++ b/examples/disabled.rs @@ -11,7 +11,7 @@ fn app(cx: Scope) -> Element { div { button { onclick: move |_| disabled.set(!disabled.get()), - "click to " [if *disabled {"enable"} else {"disable"} ] " the lower button" + "click to " [if **disabled {"enable"} else {"disable"} ] " the lower button" } button { diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 3b35ae174..b4575a9de 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -42,7 +42,7 @@ fn app(cx: Scope) -> Element { )) } div { flex: "50%", - match &*selected_breed { + match selected_breed.get() { Some(breed) => rsx!( Breed { breed: breed.clone() } ), None => rsx!("No Breed selected"), } diff --git a/examples/readme.rs b/examples/readme.rs index a9ea60822..89282babd 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -9,13 +9,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let count = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| *count.modify() += 1, "Up high!" } + button { onclick: move |_| *count.modify() -= 1, "Down low!" } } }) } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 8adb01d34..30d1fbb10 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -19,15 +19,15 @@ pub struct TodoItem { } pub fn app(cx: Scope<()>) -> Element { - let todos = use_state(&cx, || im_rc::HashMap::::default()); + let todos = use_state(&cx, im_rc::HashMap::::default); let filter = use_state(&cx, || FilterState::All); let draft = use_state(&cx, || "".to_string()); - let mut todo_id = use_state(&cx, || 0); + let (todo_id, set_todo_id) = use_state(&cx, || 0).split(); // Filter the todos based on the filter state let mut filtered_todos = todos .iter() - .filter(|(_, item)| match *filter { + .filter(|(_, item)| match **filter { FilterState::All => true, FilterState::Active => !item.checked, FilterState::Completed => item.checked, @@ -56,19 +56,17 @@ pub fn app(cx: Scope<()>) -> Element { autofocus: "true", oninput: move |evt| draft.set(evt.value.clone()), onkeydown: move |evt| { - if evt.key == "Enter" { - if !draft.is_empty() { - todos.modify().insert( - *todo_id, - TodoItem { - id: *todo_id, - checked: false, - contents: draft.get().clone(), - }, - ); - todo_id += 1; - draft.set("".to_string()); - } + if evt.key == "Enter" && !draft.is_empty() { + todos.modify().insert( + *todo_id, + TodoItem { + id: *todo_id, + checked: false, + contents: draft.get().clone(), + }, + ); + set_todo_id(todo_id + 1); + draft.set("".to_string()); } } } @@ -90,7 +88,7 @@ pub fn app(cx: Scope<()>) -> Element { (show_clear_completed).then(|| rsx!( button { class: "clear-completed", - onclick: move |_| todos.modify().retain(|_, todo| todo.checked == false), + onclick: move |_| todos.modify().retain(|_, todo| !todo.checked), "Clear completed" } )) @@ -108,7 +106,7 @@ pub fn app(cx: Scope<()>) -> Element { #[derive(Props)] pub struct TodoEntryProps<'a> { - todos: UseState<'a, im_rc::HashMap>, + todos: &'a UseState>, id: u32, } diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs new file mode 100644 index 000000000..9cfcedf4f --- /dev/null +++ b/packages/hooks/src/usestate.rs @@ -0,0 +1,212 @@ +use dioxus_core::prelude::*; +use std::{ + cell::{Cell, Ref, RefCell, RefMut}, + fmt::{Debug, Display}, + rc::Rc, +}; + +/// Store state between component renders! +/// +/// ## Dioxus equivalent of useState, designed for Rust +/// +/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and +/// modify state between component renders. When the state is updated, the component will re-render. +/// +/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. +/// +/// [`use_state`] exposes a few helper methods to modify the underlying state: +/// - `.set(new)` allows you to override the "work in progress" value with a new value +/// - `.get_mut()` allows you to modify the WIP value +/// - `.get_wip()` allows you to access the WIP value +/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) +/// +/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations +/// will automatically be called on the WIP value. +/// +/// ## Combinators +/// +/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: +/// - `.classic()` and `.split()` convert the hook into the classic React-style hook +/// ```rust +/// let (state, set_state) = use_state(&cx, || 10).split() +/// ``` +/// Usage: +/// +/// ```ignore +/// const Example: Component = |cx| { +/// let counter = use_state(&cx, || 0); +/// +/// cx.render(rsx! { +/// div { +/// h1 { "Counter: {counter}" } +/// button { onclick: move |_| counter.set(**counter + 1), "Increment" } +/// button { onclick: move |_| counter.set(**counter - 1), "Decrement" } +/// } +/// )) +/// } +/// ``` +pub fn use_state<'a, T: 'static>( + cx: &'a ScopeState, + initial_state_fn: impl FnOnce() -> T, +) -> &'a UseState { + let hook = cx.use_hook(move |_| UseState { + current_val: Rc::new(initial_state_fn()), + update_callback: cx.schedule_update(), + wip: Rc::new(RefCell::new(None)), + update_scheuled: Cell::new(false), + }); + + hook.update_scheuled.set(false); + let mut new_val = hook.wip.borrow_mut(); + + if new_val.is_some() { + // if there's only one reference (weak or otherwise), we can just swap the values + if let Some(val) = Rc::get_mut(&mut hook.current_val) { + *val = new_val.take().unwrap(); + } else { + hook.current_val = Rc::new(new_val.take().unwrap()); + } + } + + hook +} + +pub struct UseState { + pub(crate) current_val: Rc, + pub(crate) wip: Rc>>, + pub(crate) update_callback: Rc, + pub(crate) update_scheuled: Cell, +} + +impl Debug for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.current_val) + } +} + +impl UseState { + /// Tell the Dioxus Scheduler that we need to be processed + pub fn needs_update(&self) { + if !self.update_scheuled.get() { + self.update_scheuled.set(true); + (self.update_callback)(); + } + } + + pub fn set(&self, new_val: T) { + *self.wip.borrow_mut() = Some(new_val); + self.needs_update(); + } + + pub fn get(&self) -> &T { + &self.current_val + } + + pub fn get_rc(&self) -> &Rc { + &self.current_val + } + + /// Get the current status of the work-in-progress data + pub fn get_wip(&self) -> Ref> { + self.wip.borrow() + } + + /// Get the current status of the work-in-progress data + pub fn get_wip_mut(&self) -> RefMut> { + self.wip.borrow_mut() + } + + pub fn split(&self) -> (&T, Rc) { + (&self.current_val, self.setter()) + } + + pub fn setter(&self) -> Rc { + let slot = self.wip.clone(); + let callback = self.update_callback.clone(); + Rc::new(move |new| { + callback(); + *slot.borrow_mut() = Some(new) + }) + } + + pub fn wtih(&self, f: impl FnOnce(&mut T)) { + let mut val = self.wip.borrow_mut(); + + if let Some(inner) = val.as_mut() { + f(inner); + } + } + + pub fn for_async(&self) -> UseState { + let UseState { + current_val, + wip, + update_callback, + update_scheuled, + } = self; + + UseState { + current_val: current_val.clone(), + wip: wip.clone(), + update_callback: update_callback.clone(), + update_scheuled: update_scheuled.clone(), + } + } +} + +impl> UseState { + /// Gain mutable access to the new value via [`RefMut`]. + /// + /// If `modify` is called, then the component will re-render. + /// + /// This method is only available when the value is a `ToOwned` type. + /// + /// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value. + /// + /// To get a reference to the current value, use `.get()` + pub fn modify(&self) -> RefMut { + // make sure we get processed + self.needs_update(); + + // Bring out the new value, cloning if it we need to + // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this + RefMut::map(self.wip.borrow_mut(), |slot| { + if slot.is_none() { + *slot = Some(self.current_val.as_ref().to_owned()); + } + slot.as_mut().unwrap() + }) + } + + pub fn inner(self) -> T { + self.current_val.as_ref().to_owned() + } +} + +impl<'a, T> std::ops::Deref for UseState { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.get() + } +} + +// enable displaty for the handle +impl<'a, T: 'static + Display> std::fmt::Display for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.current_val) + } +} + +impl<'a, V, T: PartialEq> PartialEq for UseState { + fn eq(&self, other: &V) -> bool { + self.get() == other + } +} +impl<'a, O, T: std::ops::Not + Copy> std::ops::Not for UseState { + type Output = O; + + fn not(self) -> Self::Output { + !*self.get() + } +} diff --git a/packages/hooks/src/usestate/handle.rs b/packages/hooks/src/usestate/handle.rs deleted file mode 100644 index 2638feaa8..000000000 --- a/packages/hooks/src/usestate/handle.rs +++ /dev/null @@ -1,215 +0,0 @@ -use super::owned::UseStateOwned; -use std::{ - cell::{Ref, RefMut}, - fmt::{Debug, Display}, - rc::Rc, -}; - -pub struct UseState<'a, T: 'static>(pub(crate) &'a UseStateOwned); - -impl Copy for UseState<'_, T> {} - -impl<'a, T: 'static> Clone for UseState<'a, T> { - fn clone(&self) -> Self { - UseState(self.0) - } -} - -impl Debug for UseState<'_, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.0.current_val) - } -} - -impl<'a, T: 'static> UseState<'a, T> { - /// Tell the Dioxus Scheduler that we need to be processed - pub fn needs_update(&self) { - if !self.0.update_scheuled.get() { - self.0.update_scheuled.set(true); - (self.0.update_callback)(); - } - } - - pub fn set(&self, new_val: T) { - *self.0.wip.borrow_mut() = Some(new_val); - self.needs_update(); - } - - pub fn get(&self) -> &'a T { - &self.0.current_val - } - - pub fn get_rc(&self) -> &'a Rc { - &self.0.current_val - } - - /// Get the current status of the work-in-progress data - pub fn get_wip(&self) -> Ref> { - self.0.wip.borrow() - } - - /// Get the current status of the work-in-progress data - pub fn get_wip_mut(&self) -> RefMut> { - self.0.wip.borrow_mut() - } - - pub fn classic(self) -> (&'a T, Rc) { - (&self.0.current_val, self.setter()) - } - - pub fn setter(&self) -> Rc { - let slot = self.0.wip.clone(); - let callback = self.0.update_callback.clone(); - Rc::new(move |new| { - callback(); - *slot.borrow_mut() = Some(new) - }) - } - - pub fn wtih(&self, f: impl FnOnce(&mut T)) { - let mut val = self.0.wip.borrow_mut(); - - if let Some(inner) = val.as_mut() { - f(inner); - } - } - - pub fn for_async(&self) -> UseStateOwned { - let UseStateOwned { - current_val, - wip, - update_callback, - update_scheuled, - } = self.0; - - UseStateOwned { - current_val: current_val.clone(), - wip: wip.clone(), - update_callback: update_callback.clone(), - update_scheuled: update_scheuled.clone(), - } - } -} - -impl<'a, T: 'static + ToOwned> UseState<'a, T> { - /// Gain mutable access to the new value via [`RefMut`]. - /// - /// If `modify` is called, then the component will re-render. - /// - /// This method is only available when the value is a `ToOwned` type. - /// - /// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value. - /// - /// To get a reference to the current value, use `.get()` - pub fn modify(self) -> RefMut<'a, T> { - // make sure we get processed - self.needs_update(); - - // Bring out the new value, cloning if it we need to - // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this - RefMut::map(self.0.wip.borrow_mut(), |slot| { - if slot.is_none() { - *slot = Some(self.0.current_val.as_ref().to_owned()); - } - slot.as_mut().unwrap() - }) - } - - pub fn inner(self) -> T { - self.0.current_val.as_ref().to_owned() - } -} - -impl<'a, T> std::ops::Deref for UseState<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.get() - } -} - -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.current_val) - } -} -impl<'a, V, T: PartialEq> PartialEq for UseState<'a, T> { - fn eq(&self, other: &V) -> bool { - self.get() == other - } -} -impl<'a, O, T: std::ops::Not + Copy> std::ops::Not for UseState<'a, T> { - type Output = O; - - fn not(self) -> Self::Output { - !*self.get() - } -} - -/* - -Convenience methods for UseState. - -Note! - -This is not comprehensive. -This is *just* meant to make common operations easier. -*/ - -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; - -impl<'a, T: Copy + Add> Add for UseState<'a, T> { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.0.current_val.add(rhs) - } -} -impl<'a, T: Copy + Add> AddAssign for UseState<'a, T> { - fn add_assign(&mut self, rhs: T) { - self.set(self.0.current_val.add(rhs)); - } -} - -/// Sub -impl<'a, T: Copy + Sub> Sub for UseState<'a, T> { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.0.current_val.sub(rhs) - } -} -impl<'a, T: Copy + Sub> SubAssign for UseState<'a, T> { - fn sub_assign(&mut self, rhs: T) { - self.set(self.0.current_val.sub(rhs)); - } -} - -/// MUL -impl<'a, T: Copy + Mul> Mul for UseState<'a, T> { - type Output = T; - - fn mul(self, rhs: T) -> Self::Output { - self.0.current_val.mul(rhs) - } -} -impl<'a, T: Copy + Mul> MulAssign for UseState<'a, T> { - fn mul_assign(&mut self, rhs: T) { - self.set(self.0.current_val.mul(rhs)); - } -} - -/// DIV -impl<'a, T: Copy + Div> Div for UseState<'a, T> { - type Output = T; - - fn div(self, rhs: T) -> Self::Output { - self.0.current_val.div(rhs) - } -} -impl<'a, T: Copy + Div> DivAssign for UseState<'a, T> { - fn div_assign(&mut self, rhs: T) { - self.set(self.0.current_val.div(rhs)); - } -} diff --git a/packages/hooks/src/usestate/mod.rs b/packages/hooks/src/usestate/mod.rs deleted file mode 100644 index cf125c2a4..000000000 --- a/packages/hooks/src/usestate/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -mod handle; -mod owned; -pub use handle::*; -pub use owned::*; - -use dioxus_core::prelude::*; -use std::{ - cell::{Cell, RefCell}, - rc::Rc, -}; - -/// Store state between component renders! -/// -/// ## Dioxus equivalent of useState, designed for Rust -/// -/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and -/// modify state between component renders. When the state is updated, the component will re-render. -/// -/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. -/// -/// [`use_state`] exposes a few helper methods to modify the underlying state: -/// - `.set(new)` allows you to override the "work in progress" value with a new value -/// - `.get_mut()` allows you to modify the WIP value -/// - `.get_wip()` allows you to access the WIP value -/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) -/// -/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations -/// will automatically be called on the WIP value. -/// -/// ## Combinators -/// -/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: -/// - `.classic()` and `.split()` convert the hook into the classic React-style hook -/// ```rust -/// let (state, set_state) = use_state(&cx, || 10).split() -/// ``` -/// -/// -/// Usage: -/// -/// ```ignore -/// const Example: Component = |cx| { -/// let counter = use_state(&cx, || 0); -/// -/// cx.render(rsx! { -/// div { -/// h1 { "Counter: {counter}" } -/// button { onclick: move |_| counter += 1, "Increment" } -/// button { onclick: move |_| counter -= 1, "Decrement" } -/// } -/// )) -/// } -/// ``` -pub fn use_state<'a, T: 'static>( - cx: &'a ScopeState, - initial_state_fn: impl FnOnce() -> T, -) -> UseState<'a, T> { - let hook = cx.use_hook(move |_| UseStateOwned { - current_val: Rc::new(initial_state_fn()), - update_callback: cx.schedule_update(), - wip: Rc::new(RefCell::new(None)), - update_scheuled: Cell::new(false), - }); - - hook.update_scheuled.set(false); - let mut new_val = hook.wip.borrow_mut(); - - if new_val.is_some() { - // if there's only one reference (weak or otherwise), we can just swap the values - if let Some(val) = Rc::get_mut(&mut hook.current_val) { - *val = new_val.take().unwrap(); - } else { - hook.current_val = Rc::new(new_val.take().unwrap()); - } - } - - UseState(hook) -} diff --git a/packages/hooks/src/usestate/owned.rs b/packages/hooks/src/usestate/owned.rs deleted file mode 100644 index a1a6bdb73..000000000 --- a/packages/hooks/src/usestate/owned.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::{ - cell::{Cell, Ref, RefCell, RefMut}, - fmt::{Debug, Display}, - rc::Rc, -}; -pub struct UseStateOwned { - // this will always be outdated - pub(crate) current_val: Rc, - pub(crate) wip: Rc>>, - pub(crate) update_callback: Rc, - pub(crate) update_scheuled: Cell, -} - -impl UseStateOwned { - pub fn get(&self) -> Ref> { - self.wip.borrow() - } - - pub fn set(&self, new_val: T) { - *self.wip.borrow_mut() = Some(new_val); - (self.update_callback)(); - } - - pub fn modify(&self) -> RefMut> { - (self.update_callback)(); - self.wip.borrow_mut() - } -} - -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; - -impl Debug for UseStateOwned { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.current_val) - } -} - -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseStateOwned { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.current_val) - } -} - -impl<'a, T: Copy + Add> Add for UseStateOwned { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.current_val.add(rhs) - } -} - -impl<'a, T: Copy + Add> AddAssign for UseStateOwned { - fn add_assign(&mut self, rhs: T) { - self.set(self.current_val.add(rhs)); - } -} - -/// Sub -impl<'a, T: Copy + Sub> Sub for UseStateOwned { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.current_val.sub(rhs) - } -} -impl<'a, T: Copy + Sub> SubAssign for UseStateOwned { - fn sub_assign(&mut self, rhs: T) { - self.set(self.current_val.sub(rhs)); - } -} - -/// MUL -impl<'a, T: Copy + Mul> Mul for UseStateOwned { - type Output = T; - - fn mul(self, rhs: T) -> Self::Output { - self.current_val.mul(rhs) - } -} -impl<'a, T: Copy + Mul> MulAssign for UseStateOwned { - fn mul_assign(&mut self, rhs: T) { - self.set(self.current_val.mul(rhs)); - } -} - -/// DIV -impl<'a, T: Copy + Div> Div for UseStateOwned { - type Output = T; - - fn div(self, rhs: T) -> Self::Output { - self.current_val.div(rhs) - } -} -impl<'a, T: Copy + Div> DivAssign for UseStateOwned { - fn div_assign(&mut self, rhs: T) { - self.set(self.current_val.div(rhs)); - } -} From abfac0d59b620cdf3af77c80aadb07266e0d651b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 17 Jan 2022 16:37:44 -0500 Subject: [PATCH 007/256] wip: move macro lib out of proc macro crate --- Cargo.toml | 5 +- examples/rsx_autocomplete.rs | 27 ++++++ packages/core-macro/Cargo.toml | 1 + packages/core-macro/src/lib.rs | 7 +- packages/macro-inner/Cargo.toml | 13 +++ .../{core-macro => macro-inner}/src/htm.rs | 0 .../{core-macro => macro-inner}/src/ifmt.rs | 0 .../src/inlineprops.rs | 0 packages/macro-inner/src/lib.rs | 5 + .../src/props/mod.rs | 0 .../{core-macro => macro-inner}/src/router.rs | 0 .../src/rsx/component.rs | 8 +- .../src/rsx/element.rs | 93 ++++++++++++------- .../src/rsx/mod.rs | 4 +- .../src/rsx/node.rs | 0 .../src/rsxtemplate.rs | 0 16 files changed, 116 insertions(+), 47 deletions(-) create mode 100644 examples/rsx_autocomplete.rs create mode 100644 packages/macro-inner/Cargo.toml rename packages/{core-macro => macro-inner}/src/htm.rs (100%) rename packages/{core-macro => macro-inner}/src/ifmt.rs (100%) rename packages/{core-macro => macro-inner}/src/inlineprops.rs (100%) create mode 100644 packages/macro-inner/src/lib.rs rename packages/{core-macro => macro-inner}/src/props/mod.rs (100%) rename packages/{core-macro => macro-inner}/src/router.rs (100%) rename packages/{core-macro => macro-inner}/src/rsx/component.rs (98%) rename packages/{core-macro => macro-inner}/src/rsx/element.rs (80%) rename packages/{core-macro => macro-inner}/src/rsx/mod.rs (97%) rename packages/{core-macro => macro-inner}/src/rsx/node.rs (100%) rename packages/{core-macro => macro-inner}/src/rsxtemplate.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 884513a5b..98da05bcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "./packages/core", version = "^0.1.7" } dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.6", optional = true } +dioxus-macro-inner = { path = "./packages/macro-inner", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } @@ -27,7 +28,7 @@ dioxus-mobile = { path = "./packages/mobile", version = "^0.0.3", optional = tru [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro"] +macro = ["dioxus-core-macro", "dioxus-macro-inner"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] @@ -35,6 +36,7 @@ web = ["dioxus-web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] + # "dioxus-router/web" # "dioxus-router/desktop" # desktop = ["dioxus-desktop", "dioxus-router/desktop"] @@ -46,6 +48,7 @@ router = ["dioxus-router"] members = [ "packages/core", "packages/core-macro", + "packages/macro-inner", "packages/html", "packages/hooks", "packages/web", diff --git a/examples/rsx_autocomplete.rs b/examples/rsx_autocomplete.rs new file mode 100644 index 000000000..99afe7521 --- /dev/null +++ b/examples/rsx_autocomplete.rs @@ -0,0 +1,27 @@ +//! This example shows that autocomplete works in RSX + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + onclick: move |_| { + } + // class: "asd", + // style { + // media: "Ad", + // } + // div { + + // } + // { + // let t = String::new(); + // t. + // } + } + }) +} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index ee3dc2883..8f2b81cba 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] +dioxus-macro-inner = { path = "../macro-inner" } once_cell = "1.8" proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 4ad7543a2..3579039b5 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -1,13 +1,8 @@ +use dioxus_macro_inner::*; use proc_macro::TokenStream; use quote::ToTokens; use syn::parse_macro_input; -pub(crate) mod ifmt; -pub(crate) mod inlineprops; -pub(crate) mod props; -pub(crate) mod router; -pub(crate) mod rsx; - #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { use ifmt::*; diff --git a/packages/macro-inner/Cargo.toml b/packages/macro-inner/Cargo.toml new file mode 100644 index 000000000..63fcbbd9b --- /dev/null +++ b/packages/macro-inner/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dioxus-macro-inner" +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +once_cell = "1.8" +proc-macro-error = "1.0.4" +proc-macro2 = { version = "1.0.6" } +quote = "1.0" +syn = { version = "1.0.11", features = ["full", "extra-traits"] } diff --git a/packages/core-macro/src/htm.rs b/packages/macro-inner/src/htm.rs similarity index 100% rename from packages/core-macro/src/htm.rs rename to packages/macro-inner/src/htm.rs diff --git a/packages/core-macro/src/ifmt.rs b/packages/macro-inner/src/ifmt.rs similarity index 100% rename from packages/core-macro/src/ifmt.rs rename to packages/macro-inner/src/ifmt.rs diff --git a/packages/core-macro/src/inlineprops.rs b/packages/macro-inner/src/inlineprops.rs similarity index 100% rename from packages/core-macro/src/inlineprops.rs rename to packages/macro-inner/src/inlineprops.rs diff --git a/packages/macro-inner/src/lib.rs b/packages/macro-inner/src/lib.rs new file mode 100644 index 000000000..69e4b743f --- /dev/null +++ b/packages/macro-inner/src/lib.rs @@ -0,0 +1,5 @@ +pub mod ifmt; +pub mod inlineprops; +pub mod props; +pub mod router; +pub mod rsx; diff --git a/packages/core-macro/src/props/mod.rs b/packages/macro-inner/src/props/mod.rs similarity index 100% rename from packages/core-macro/src/props/mod.rs rename to packages/macro-inner/src/props/mod.rs diff --git a/packages/core-macro/src/router.rs b/packages/macro-inner/src/router.rs similarity index 100% rename from packages/core-macro/src/router.rs rename to packages/macro-inner/src/router.rs diff --git a/packages/core-macro/src/rsx/component.rs b/packages/macro-inner/src/rsx/component.rs similarity index 98% rename from packages/core-macro/src/rsx/component.rs rename to packages/macro-inner/src/rsx/component.rs index 5257022a4..26305359d 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/macro-inner/src/rsx/component.rs @@ -23,10 +23,10 @@ use syn::{ }; pub struct Component { - name: syn::Path, - body: Vec, - children: Vec, - manual_props: Option, + pub name: syn::Path, + pub body: Vec, + pub children: Vec, + pub manual_props: Option, } impl Parse for Component { diff --git a/packages/core-macro/src/rsx/element.rs b/packages/macro-inner/src/rsx/element.rs similarity index 80% rename from packages/core-macro/src/rsx/element.rs rename to packages/macro-inner/src/rsx/element.rs index ebc401b6e..c612e9f1b 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/macro-inner/src/rsx/element.rs @@ -4,19 +4,18 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, - Expr, Ident, LitStr, Result, Token, + Expr, ExprClosure, Ident, LitStr, Result, Token, }; // ======================================= // Parse the VNode::Element type // ======================================= pub struct Element { - name: Ident, - key: Option, - attributes: Vec, - listeners: Vec, - children: Vec, - _is_static: bool, + pub name: Ident, + pub key: Option, + pub attributes: Vec, + pub children: Vec, + pub _is_static: bool, } impl Parse for Element { @@ -28,7 +27,6 @@ impl Parse for Element { syn::braced!(content in stream); let mut attributes: Vec = vec![]; - let mut listeners: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; let mut _el_ref = None; @@ -54,6 +52,7 @@ impl Parse for Element { }); } else { let value = content.parse::()?; + attributes.push(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr::CustomAttrExpression { name, value }, @@ -82,13 +81,24 @@ impl Parse for Element { content.parse::()?; if name_str.starts_with("on") { - listeners.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: content.parse()?, - }, - }); + if content.fork().parse::().is_ok() { + // + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventClosure { + name, + closure: content.parse()?, + }, + }); + } else { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventTokens { + name, + tokens: content.parse()?, + }, + }); + } } else { match name_str.as_str() { "key" => { @@ -182,7 +192,7 @@ impl Parse for Element { name: el_name, attributes, children, - listeners, + // listeners, _is_static: false, }) } @@ -193,14 +203,29 @@ impl ToTokens for Element { let name = &self.name; let children = &self.children; - let listeners = &self.listeners; - let attr = &self.attributes; + // let listeners = &self.listeners; let key = match &self.key { Some(ty) => quote! { Some(format_args_f!(#ty)) }, None => quote! { None }, }; + let listeners = self.attributes.iter().filter(|f| { + if let ElementAttr::EventTokens { .. } = f.attr { + true + } else { + false + } + }); + + let attr = self.attributes.iter().filter(|f| { + if let ElementAttr::EventTokens { .. } = f.attr { + false + } else { + true + } + }); + tokens.append_all(quote! { __cx.element( dioxus_elements::#name, @@ -213,29 +238,29 @@ impl ToTokens for Element { } } -enum ElementAttr { - // attribute: "valuee {}" +pub enum ElementAttr { + /// attribute: "valuee {}" AttrText { name: Ident, value: LitStr }, - // attribute: true, + /// attribute: true, AttrExpression { name: Ident, value: Expr }, - // "attribute": "value {}" + /// "attribute": "value {}" CustomAttrText { name: LitStr, value: LitStr }, - // "attribute": true, + /// "attribute": true, CustomAttrExpression { name: LitStr, value: Expr }, - // // onclick: move |_| {} - // EventClosure { name: Ident, closure: ExprClosure }, + /// onclick: move |_| {} + EventClosure { name: Ident, closure: ExprClosure }, - // onclick: {} + /// onclick: {} EventTokens { name: Ident, tokens: Expr }, } -struct ElementAttrNamed { - el_name: Ident, - attr: ElementAttr, +pub struct ElementAttrNamed { + pub el_name: Ident, + pub attr: ElementAttr, } impl ToTokens for ElementAttrNamed { @@ -263,11 +288,11 @@ impl ToTokens for ElementAttrNamed { __cx.attr( #name, format_args_f!(#value), None, false ) } } - // ElementAttr::EventClosure { name, closure } => { - // quote! { - // dioxus_elements::on::#name(__cx, #closure) - // } - // } + ElementAttr::EventClosure { name, closure } => { + quote! { + dioxus_elements::on::#name(__cx, #closure) + } + } ElementAttr::EventTokens { name, tokens } => { quote! { dioxus_elements::on::#name(__cx, #tokens) diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/macro-inner/src/rsx/mod.rs similarity index 97% rename from packages/core-macro/src/rsx/mod.rs rename to packages/macro-inner/src/rsx/mod.rs index 5ff5d85f7..87b324dfc 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/macro-inner/src/rsx/mod.rs @@ -29,8 +29,8 @@ use syn::{ }; pub struct CallBody { - custom_context: Option, - roots: Vec, + pub custom_context: Option, + pub roots: Vec, } impl Parse for CallBody { diff --git a/packages/core-macro/src/rsx/node.rs b/packages/macro-inner/src/rsx/node.rs similarity index 100% rename from packages/core-macro/src/rsx/node.rs rename to packages/macro-inner/src/rsx/node.rs diff --git a/packages/core-macro/src/rsxtemplate.rs b/packages/macro-inner/src/rsxtemplate.rs similarity index 100% rename from packages/core-macro/src/rsxtemplate.rs rename to packages/macro-inner/src/rsxtemplate.rs From 174d2870625c8f052b041207c15c1062e8356c14 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 17 Jan 2022 23:02:36 -0500 Subject: [PATCH 008/256] wip: extract to rsx module --- Cargo.toml | 4 +- .../{macro-inner => core-macro}/src/ifmt.rs | 0 .../src/inlineprops.rs | 0 .../src/props/mod.rs | 0 packages/macro-inner/src/router.rs | 214 ------------------ packages/macro-inner/src/rsxtemplate.rs | 84 ------- packages/{macro-inner => rsx}/Cargo.toml | 2 +- packages/{macro-inner => rsx}/src/htm.rs | 0 packages/{macro-inner => rsx}/src/lib.rs | 1 - .../{macro-inner => rsx}/src/rsx/component.rs | 0 .../{macro-inner => rsx}/src/rsx/element.rs | 0 packages/{macro-inner => rsx}/src/rsx/mod.rs | 0 packages/{macro-inner => rsx}/src/rsx/node.rs | 0 13 files changed, 3 insertions(+), 302 deletions(-) rename packages/{macro-inner => core-macro}/src/ifmt.rs (100%) rename packages/{macro-inner => core-macro}/src/inlineprops.rs (100%) rename packages/{macro-inner => core-macro}/src/props/mod.rs (100%) delete mode 100644 packages/macro-inner/src/router.rs delete mode 100644 packages/macro-inner/src/rsxtemplate.rs rename packages/{macro-inner => rsx}/Cargo.toml (91%) rename packages/{macro-inner => rsx}/src/htm.rs (100%) rename packages/{macro-inner => rsx}/src/lib.rs (79%) rename packages/{macro-inner => rsx}/src/rsx/component.rs (100%) rename packages/{macro-inner => rsx}/src/rsx/element.rs (100%) rename packages/{macro-inner => rsx}/src/rsx/mod.rs (100%) rename packages/{macro-inner => rsx}/src/rsx/node.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 98da05bcc..c8075c545 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "./packages/core", version = "^0.1.7" } dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.6", optional = true } -dioxus-macro-inner = { path = "./packages/macro-inner", optional = true } +dioxus-macro-inner = { path = "./packages/rsx", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } @@ -48,7 +48,7 @@ router = ["dioxus-router"] members = [ "packages/core", "packages/core-macro", - "packages/macro-inner", + "packages/rsx", "packages/html", "packages/hooks", "packages/web", diff --git a/packages/macro-inner/src/ifmt.rs b/packages/core-macro/src/ifmt.rs similarity index 100% rename from packages/macro-inner/src/ifmt.rs rename to packages/core-macro/src/ifmt.rs diff --git a/packages/macro-inner/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs similarity index 100% rename from packages/macro-inner/src/inlineprops.rs rename to packages/core-macro/src/inlineprops.rs diff --git a/packages/macro-inner/src/props/mod.rs b/packages/core-macro/src/props/mod.rs similarity index 100% rename from packages/macro-inner/src/props/mod.rs rename to packages/core-macro/src/props/mod.rs diff --git a/packages/macro-inner/src/router.rs b/packages/macro-inner/src/router.rs deleted file mode 100644 index 3edad75e3..000000000 --- a/packages/macro-inner/src/router.rs +++ /dev/null @@ -1,214 +0,0 @@ -#![allow(dead_code)] - -use proc_macro2::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant}; - -const AT_ATTR_IDENT: &str = "at"; -const NOT_FOUND_ATTR_IDENT: &str = "not_found"; - -pub struct Routable { - ident: Ident, - ats: Vec, - variants: Punctuated, - not_found_route: Option, -} - -impl Parse for Routable { - fn parse(input: ParseStream) -> syn::Result { - let DeriveInput { ident, data, .. } = input.parse()?; - - let data = match data { - Data::Enum(data) => data, - Data::Struct(s) => { - return Err(syn::Error::new( - s.struct_token.span(), - "expected enum, found struct", - )) - } - Data::Union(u) => { - return Err(syn::Error::new( - u.union_token.span(), - "expected enum, found union", - )) - } - }; - - let (not_found_route, ats) = parse_variants_attributes(&data.variants)?; - - Ok(Self { - ident, - variants: data.variants, - ats, - not_found_route, - }) - } -} - -fn parse_variants_attributes( - variants: &Punctuated, -) -> syn::Result<(Option, Vec)> { - let mut not_founds = vec![]; - let mut ats: Vec = vec![]; - - let mut not_found_attrs = vec![]; - - for variant in variants.iter() { - if let Fields::Unnamed(ref field) = variant.fields { - return Err(syn::Error::new( - field.span(), - "only named fields are supported", - )); - } - - let attrs = &variant.attrs; - let at_attrs = attrs - .iter() - .filter(|attr| attr.path.is_ident(AT_ATTR_IDENT)) - .collect::>(); - - let attr = match at_attrs.len() { - 1 => *at_attrs.first().unwrap(), - 0 => { - return Err(syn::Error::new( - variant.span(), - format!( - "{} attribute must be present on every variant", - AT_ATTR_IDENT - ), - )) - } - _ => { - return Err(syn::Error::new_spanned( - quote! { #(#at_attrs)* }, - format!("only one {} attribute must be present", AT_ATTR_IDENT), - )) - } - }; - - let lit = attr.parse_args::()?; - ats.push(lit); - - for attr in attrs.iter() { - if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) { - not_found_attrs.push(attr); - not_founds.push(variant.ident.clone()) - } - } - } - - if not_founds.len() > 1 { - return Err(syn::Error::new_spanned( - quote! { #(#not_found_attrs)* }, - format!("there can only be one {}", NOT_FOUND_ATTR_IDENT), - )); - } - - Ok((not_founds.into_iter().next(), ats)) -} - -impl Routable { - // fn build_from_path(&self) -> TokenStream { - // let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| { - // let ident = &variant.ident; - // let right = match &variant.fields { - // Fields::Unit => quote! { Self::#ident }, - // Fields::Named(field) => { - // let fields = field.named.iter().map(|it| { - // //named fields have idents - // it.ident.as_ref().unwrap() - // }); - // quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } } - // } - // Fields::Unnamed(_) => unreachable!(), // already checked - // }; - - // let left = self.ats.get(i).unwrap(); - // quote! { - // #left => ::std::option::Option::Some(#right) - // } - // }); - - // quote! { - // fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option { - // match path { - // #(#from_path_matches),*, - // _ => ::std::option::Option::None, - // } - // } - // } - // } - - // fn build_to_path(&self) -> TokenStream { - // let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| { - // let ident = &variant.ident; - // let mut right = self.ats.get(i).unwrap().value(); - - // match &variant.fields { - // Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) }, - // Fields::Named(field) => { - // let fields = field - // .named - // .iter() - // .map(|it| it.ident.as_ref().unwrap()) - // .collect::>(); - - // for field in fields.iter() { - // // :param -> {param} - // // so we can pass it to `format!("...", param)` - // right = right.replace(&format!(":{}", field), &format!("{{{}}}", field)) - // } - - // quote! { - // Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*) - // } - // } - // Fields::Unnamed(_) => unreachable!(), // already checked - // } - // }); - - // quote! { - // fn to_path(&self) -> ::std::string::String { - // match self { - // #(#to_path_matches),*, - // } - // } - // } - // } -} - -pub fn routable_derive_impl(input: Routable) -> TokenStream { - let Routable { - // ats, - // not_found_route, - // ident, - .. - } = &input; - - // let from_path = input.build_from_path(); - // let to_path = input.build_to_path(); - - quote! { - // #[automatically_derived] - // impl ::dioxus::router::Routable for #ident { - - // fn recognize(pathname: &str) -> ::std::option::Option { - // todo!() - // // ::std::thread_local! { - // // static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>(); - // // } - // // let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname)); - // // { - // // let route = ::std::clone::Clone::clone(&route); - // // #cache_thread_local_ident.with(move |val| { - // // *val.borrow_mut() = route; - // // }); - // // } - // // route - // } - // } - } -} diff --git a/packages/macro-inner/src/rsxtemplate.rs b/packages/macro-inner/src/rsxtemplate.rs deleted file mode 100644 index 9222d7117..000000000 --- a/packages/macro-inner/src/rsxtemplate.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::{rsx::RsxBody, util::is_valid_svg_tag}; - -use { - proc_macro::TokenStream, - proc_macro2::{Span, TokenStream as TokenStream2}, - quote::{quote, ToTokens, TokenStreamExt}, - syn::{ - ext::IdentExt, - parse::{Parse, ParseStream}, - token, Error, Expr, ExprClosure, Ident, LitBool, LitStr, Path, Result, Token, - }, -}; - -// ============================================== -// Parse any stream coming from the html! macro -// ============================================== -pub struct RsxTemplate { - inner: RsxBody, -} - -impl Parse for RsxTemplate { - fn parse(s: ParseStream) -> Result { - if s.peek(LitStr) { - use std::str::FromStr; - - let lit = s.parse::()?; - let g = lit.span(); - let mut value = lit.value(); - if value.ends_with('\n') { - value.pop(); - if value.ends_with('\r') { - value.pop(); - } - } - let lit = LitStr::new(&value, lit.span()); - - // panic!("{:#?}", lit); - match lit.parse::() { - Ok(r) => Ok(Self { inner: r }), - Err(e) => Err(e), - } - } else { - panic!("Not a str lit") - } - // let t = s.parse::()?; - - // let new_stream = TokenStream::from(t.to_s) - - // let cx: Ident = s.parse()?; - // s.parse::()?; - // if elements are in an array, return a bumpalo::collections::Vec rather than a Node. - // let kind = if s.peek(token::Bracket) { - // let nodes_toks; - // syn::bracketed!(nodes_toks in s); - // let mut nodes: Vec> = vec![nodes_toks.parse()?]; - // while nodes_toks.peek(Token![,]) { - // nodes_toks.parse::()?; - // nodes.push(nodes_toks.parse()?); - // } - // NodeOrList::List(NodeList(nodes)) - // } else { - // NodeOrList::Node(s.parse()?) - // }; - // Ok(HtmlRender { kind }) - } -} - -impl ToTokens for RsxTemplate { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - self.inner.to_tokens(out_tokens); - // let new_toks = ToToksCtx::new(&self.kind).to_token_stream(); - - // // create a lazy tree that accepts a bump allocator - // let final_tokens = quote! { - // dioxus::prelude::LazyNodes::new(move |cx| { - // let bump = &cx.bump(); - - // #new_toks - // }) - // }; - - // final_tokens.to_tokens(out_tokens); - } -} diff --git a/packages/macro-inner/Cargo.toml b/packages/rsx/Cargo.toml similarity index 91% rename from packages/macro-inner/Cargo.toml rename to packages/rsx/Cargo.toml index 63fcbbd9b..4cad9064c 100644 --- a/packages/macro-inner/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dioxus-macro-inner" +name = "dioxus-rsx" version = "0.0.0" edition = "2018" diff --git a/packages/macro-inner/src/htm.rs b/packages/rsx/src/htm.rs similarity index 100% rename from packages/macro-inner/src/htm.rs rename to packages/rsx/src/htm.rs diff --git a/packages/macro-inner/src/lib.rs b/packages/rsx/src/lib.rs similarity index 79% rename from packages/macro-inner/src/lib.rs rename to packages/rsx/src/lib.rs index 69e4b743f..b9911dedc 100644 --- a/packages/macro-inner/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -1,5 +1,4 @@ pub mod ifmt; pub mod inlineprops; pub mod props; -pub mod router; pub mod rsx; diff --git a/packages/macro-inner/src/rsx/component.rs b/packages/rsx/src/rsx/component.rs similarity index 100% rename from packages/macro-inner/src/rsx/component.rs rename to packages/rsx/src/rsx/component.rs diff --git a/packages/macro-inner/src/rsx/element.rs b/packages/rsx/src/rsx/element.rs similarity index 100% rename from packages/macro-inner/src/rsx/element.rs rename to packages/rsx/src/rsx/element.rs diff --git a/packages/macro-inner/src/rsx/mod.rs b/packages/rsx/src/rsx/mod.rs similarity index 100% rename from packages/macro-inner/src/rsx/mod.rs rename to packages/rsx/src/rsx/mod.rs diff --git a/packages/macro-inner/src/rsx/node.rs b/packages/rsx/src/rsx/node.rs similarity index 100% rename from packages/macro-inner/src/rsx/node.rs rename to packages/rsx/src/rsx/node.rs From f66d17ca84f80f199e5ce1735a11f087af7fe4ea Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 22 Jan 2022 14:53:59 -0500 Subject: [PATCH 009/256] wip: pass form data between web and desktop --- examples/form.rs | 23 +++++++++++++++++++++++ packages/html/src/events.rs | 3 +++ packages/jsinterpreter/interpreter.js | 18 ++++++++++++++++++ packages/jsinterpreter/interpreter.ts | 24 ++++++++++++++++++++---- packages/web/Cargo.toml | 1 + packages/web/src/dom.rs | 7 +++---- 6 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 examples/form.rs diff --git a/examples/form.rs b/examples/form.rs new file mode 100644 index 000000000..577658aeb --- /dev/null +++ b/examples/form.rs @@ -0,0 +1,23 @@ +//! Example: README.md showcase +//! +//! The example from the README.md. + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Form" } + form { + oninput: move |ev| println!("{:?}", ev), + input { r#type: "text", name: "username" } + input { r#type: "text", name: "full-name" } + input { r#type: "password", name: "password" } + } + } + }) +} diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index b9bcecd4f..8067e6314 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -3,6 +3,8 @@ use dioxus_core::exports::bumpalo; use dioxus_core::*; pub mod on { + use std::collections::HashMap; + use super::*; macro_rules! event_directory { ( $( @@ -484,6 +486,7 @@ pub mod on { #[derive(Debug)] pub struct FormData { pub value: String, + pub values: HashMap, /* DOMEvent: Send + SyncTarget relatedTarget */ } diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index ea50ec3d3..09cfa194b 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -231,6 +231,23 @@ export class Interpreter { } } } + if (target.tagName == "FORM") { + let formTarget = target; + for (let x = 0; x < formTarget.elements.length; x++) { + let element = formTarget.elements[x]; + let name = element.getAttribute("name"); + if (name != null) { + if (element.getAttribute("type") == "checkbox") { + // @ts-ignore + contents.values[name] = element.checked ? "true" : "false"; + } + else { + // @ts-ignore + contents.values[name] = element.value ?? element.textContent; + } + } + } + } if (realId == null) { return; } @@ -316,6 +333,7 @@ function serialize_event(event) { } return { value: value, + values: {} }; } case "click": diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index fdd8c671d..744f91081 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -244,7 +244,6 @@ export class Interpreter { break; case "NewEventListener": - // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event: Event) => { @@ -286,6 +285,23 @@ export class Interpreter { } } + if (target.tagName == "FORM") { + let formTarget = target as HTMLFormElement; + for (let x = 0; x < formTarget.elements.length; x++) { + let element = formTarget.elements[x]; + let name = element.getAttribute("name"); + if (name != null) { + if (element.getAttribute("type") == "checkbox") { + // @ts-ignore + contents.values[name] = element.checked ? "true" : "false"; + } else { + // @ts-ignore + contents.values[name] = element.value ?? element.textContent; + } + } + } + } + if (realId == null) { return; } @@ -385,6 +401,8 @@ function serialize_event(event: Event) { case "invalid": case "reset": case "submit": { + + let target = event.target as HTMLFormElement; let value = target.value ?? target.textContent; @@ -394,6 +412,7 @@ function serialize_event(event: Event) { return { value: value, + values: {} }; } @@ -643,8 +662,6 @@ const bool_attrs = { truespeed: true, }; - - type PushRoot = { type: "PushRoot", root: number }; type AppendChildren = { type: "AppendChildren", many: number }; type ReplaceWith = { type: "ReplaceWith", root: number, m: number }; @@ -661,7 +678,6 @@ type SetText = { type: "SetText", root: number, text: string }; type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined }; type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string }; - type DomEdit = PushRoot | AppendChildren | diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 52a78e631..6acdb5366 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -41,6 +41,7 @@ features = [ "HtmlInputElement", "HtmlSelectElement", "HtmlTextAreaElement", + "HtmlFormElement", "EventTarget", "HtmlCollection", "Node", diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index ffbef9f4b..b60f2485d 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -178,7 +178,8 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc anyhow::Result { priority: dioxus_core::EventPriority::Medium, }); } - Some(Err(e)) => { - return Err(e.into()); - } + Some(Err(e)) => return Err(e.into()), None => { // walk the tree upwards until we actually find an event target if let Some(parent) = target.parent_element() { From 1c6d6421dc4c285751fda8666c1e400021006fdb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 03:01:04 -0500 Subject: [PATCH 010/256] wip: add router --- docs/router/.gitignore | 1 + docs/router/book.toml | 6 ++++++ docs/router/src/SUMMARY.md | 3 +++ docs/router/src/chapter_1.md | 1 + 4 files changed, 11 insertions(+) create mode 100644 docs/router/.gitignore create mode 100644 docs/router/book.toml create mode 100644 docs/router/src/SUMMARY.md create mode 100644 docs/router/src/chapter_1.md diff --git a/docs/router/.gitignore b/docs/router/.gitignore new file mode 100644 index 000000000..7585238ef --- /dev/null +++ b/docs/router/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/router/book.toml b/docs/router/book.toml new file mode 100644 index 000000000..216bc555e --- /dev/null +++ b/docs/router/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Jonathan Kelley"] +language = "en" +multilingual = false +src = "src" +title = "Dioxus Router" diff --git a/docs/router/src/SUMMARY.md b/docs/router/src/SUMMARY.md new file mode 100644 index 000000000..7390c8289 --- /dev/null +++ b/docs/router/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/docs/router/src/chapter_1.md b/docs/router/src/chapter_1.md new file mode 100644 index 000000000..b743fda35 --- /dev/null +++ b/docs/router/src/chapter_1.md @@ -0,0 +1 @@ +# Chapter 1 From 29ed7ebece26e9d53925af55f2f34a8fd8241405 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 25 Jan 2022 15:06:37 -0500 Subject: [PATCH 011/256] feat: connect an onchange listener --- packages/router/README.md | 2 +- packages/router/src/components/router.rs | 15 +++++++-- packages/router/src/lib.rs | 15 +-------- packages/router/src/service.rs | 40 +++++++++++++++++------- 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/router/README.md b/packages/router/README.md index a6dc5027b..6fbc652eb 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -5,7 +5,7 @@ DioxusRouter adds React-Router style routing to your Dioxus apps. Works in brows ```rust fn app() { cx.render(rsx! { - Routes { + Router { Route { to: "/", Component {} }, Route { to: "/blog", Blog {} }, Route { to: "/blog/:id", BlogPost {} }, diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 3d9f0c83a..240c3806a 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -11,17 +11,26 @@ use crate::RouterService; pub struct RouterProps<'a> { children: Element<'a>, - #[props(default, strip_option)] - onchange: Option<&'a dyn Fn(&'a str)>, + #[props(default)] + onchange: EventHandler<'a, String>, } #[allow(non_snake_case)] pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { - cx.use_hook(|_| { + let svc = cx.use_hook(|_| { let update = cx.schedule_update_any(); cx.provide_context(RouterService::new(update, cx.scope_id())) }); + let any_pending = svc.pending_events.borrow().len() > 0; + svc.pending_events.borrow_mut().clear(); + + if any_pending { + let location = svc.current_location(); + let path = location.path(); + cx.props.onchange.call(path.to_string()); + } + cx.render(rsx!( div { &cx.props.children } )) diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index b6ffb5971..309cef905 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -8,22 +8,9 @@ //! //! ```rust //! fn app(cx: Scope) -> Element { +//! //! } -//! -//! -//! -//! -//! //! ``` -//! -//! -//! -//! -//! -//! -//! -//! -//! mod hooks { mod use_route; diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 00642414f..9da2bef7e 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -9,6 +9,7 @@ use dioxus_core::ScopeId; pub struct RouterService { pub(crate) regen_route: Rc, + pub(crate) pending_events: Rc>>, history: Rc>, slots: Rc>>, root_found: Rc>>, @@ -16,6 +17,12 @@ pub struct RouterService { listener: HistoryListener, } +pub enum RouteEvent { + Change, + Pop, + Push, +} + enum RouteSlot { Routes { // the partial route @@ -36,28 +43,37 @@ impl RouterService { let path = location.path(); let slots: Rc>> = Default::default(); - - let _slots = slots.clone(); - + let pending_events: Rc>> = Default::default(); let root_found = Rc::new(Cell::new(None)); - let regen = regen_route.clone(); - let _root_found = root_found.clone(); - let listener = history.listen(move || { - _root_found.set(None); - // checking if the route is valid is cheap, so we do it - for (slot, root) in _slots.borrow_mut().iter().rev() { - log::trace!("regenerating slot {:?} for root '{}'", slot, root); - regen(*slot); + + let listener = history.listen({ + let pending_events = pending_events.clone(); + let regen_route = regen_route.clone(); + let root_found = root_found.clone(); + let slots = slots.clone(); + move || { + root_found.set(None); + // checking if the route is valid is cheap, so we do it + for (slot, root) in slots.borrow_mut().iter().rev() { + log::trace!("regenerating slot {:?} for root '{}'", slot, root); + regen_route(*slot); + } + + // also regenerate the root + regen_route(root_scope); + + pending_events.borrow_mut().push(RouteEvent::Change) } }); Self { + listener, root_found, history: Rc::new(RefCell::new(history)), regen_route, slots, + pending_events, cur_path_params: Rc::new(RefCell::new(HashMap::new())), - listener, } } From a8952a9ee8d8831fa911cef4e7035293c3a9f88b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 25 Jan 2022 21:41:40 -0500 Subject: [PATCH 012/256] fix: exampels --- examples/calculator.rs | 36 +-- examples/crm.rs | 30 +-- examples/disabled.rs | 6 +- examples/dog_app.rs | 12 +- examples/framework_benchmark.rs | 4 +- examples/hydration.rs | 4 +- examples/pattern_reducer.rs | 6 +- examples/readme.rs | 6 +- examples/rsx_compile_fail.rs | 2 +- examples/tasks.rs | 8 +- examples/todomvc.rs | 48 ++-- examples/xss_safety.rs | 4 +- packages/hooks/src/lib.rs | 64 ++--- packages/hooks/src/usestate.rs | 455 ++++++++++++++++++++------------ 14 files changed, 400 insertions(+), 285 deletions(-) diff --git a/examples/calculator.rs b/examples/calculator.rs index 7f051052b..5e23a3846 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -18,17 +18,19 @@ fn main() { } fn app(cx: Scope) -> Element { - let display_value = use_state(&cx, || String::from("0")); + let (display_value, set_display_value) = use_state(&cx, || String::from("0")); let input_digit = move |num: u8| { - if display_value.get() == "0" { - display_value.set(String::new()); + if display_value == "0" { + set_display_value(String::new()); } - display_value.modify().push_str(num.to_string().as_str()); + set_display_value + .make_mut() + .push_str(num.to_string().as_str()); }; let input_operator = move |key: &str| { - display_value.modify().push_str(key); + set_display_value.make_mut().push_str(key); }; cx.render(rsx!( @@ -53,7 +55,7 @@ fn app(cx: Scope) -> Element { KeyCode::Num9 => input_digit(9), KeyCode::Backspace => { if !display_value.len() != 0 { - display_value.modify().pop(); + set_display_value.make_mut().pop(); } } _ => {} @@ -65,21 +67,21 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-clear", onclick: move |_| { - display_value.set(String::new()); - if *display_value != "" { - display_value.set("0".into()); + set_display_value(String::new()); + if !display_value.is_empty(){ + set_display_value("0".into()); } }, - [if *display_value == "" { "C" } else { "AC" }] + [if display_value.is_empty() { "C" } else { "AC" }] } button { class: "calculator-key key-sign", onclick: move |_| { - let temp = calc_val(display_value.get().clone()); + let temp = calc_val(display_value.clone()); if temp > 0.0 { - display_value.set(format!("-{}", temp)); + set_display_value(format!("-{}", temp)); } else { - display_value.set(format!("{}", temp.abs())); + set_display_value(format!("{}", temp.abs())); } }, "±" @@ -87,8 +89,8 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-percent", onclick: move |_| { - display_value.set( - format!("{}", calc_val(display_value.get().clone()) / 100.0) + set_display_value( + format!("{}", calc_val(display_value.clone()) / 100.0) ); }, "%" @@ -98,7 +100,7 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-0", onclick: move |_| input_digit(0), "0" } - button { class: "calculator-key key-dot", onclick: move |_| display_value.modify().push('.'), + button { class: "calculator-key key-dot", onclick: move |_| set_display_value.make_mut().push('.'), "●" } (1..10).map(|k| rsx!{ @@ -130,7 +132,7 @@ fn app(cx: Scope) -> Element { } button { class: "calculator-key key-equals", onclick: move |_| { - display_value.set(format!("{}", calc_val(display_value.get().clone()))); + set_display_value(format!("{}", calc_val(display_value.clone()))); }, "=" } diff --git a/examples/crm.rs b/examples/crm.rs index 241511385..f6e7e05af 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -20,11 +20,11 @@ pub struct Client { } fn app(cx: Scope) -> Element { - let scene = use_state(&cx, || Scene::ClientsList); let clients = use_ref(&cx, || vec![] as Vec); - let firstname = use_state(&cx, String::new); - let lastname = use_state(&cx, String::new); - let description = use_state(&cx, String::new); + let (scene, set_scene) = use_state(&cx, || Scene::ClientsList); + let (firstname, set_firstname) = use_state(&cx, String::new); + let (lastname, set_lastname) = use_state(&cx, String::new); + let (description, set_description) = use_state(&cx, String::new); cx.render(rsx!( body { @@ -38,7 +38,7 @@ fn app(cx: Scope) -> Element { h1 {"Dioxus CRM Example"} - match scene.get() { + match scene { Scene::ClientsList => rsx!( div { class: "crm", h2 { margin_bottom: "10px", "List of clients" } @@ -51,8 +51,8 @@ fn app(cx: Scope) -> Element { }) ) } - button { class: "pure-button pure-button-primary", onclick: move |_| scene.set(Scene::NewClientForm), "Add New" } - button { class: "pure-button", onclick: move |_| scene.set(Scene::Settings), "Settings" } + button { class: "pure-button pure-button-primary", onclick: move |_| set_scene(Scene::NewClientForm), "Add New" } + button { class: "pure-button", onclick: move |_| set_scene(Scene::Settings), "Settings" } } ), Scene::NewClientForm => rsx!( @@ -63,19 +63,19 @@ fn app(cx: Scope) -> Element { class: "new-client firstname", placeholder: "First name", value: "{firstname}", - oninput: move |e| firstname.set(e.value.clone()) + oninput: move |e| set_firstname(e.value.clone()) } input { class: "new-client lastname", placeholder: "Last name", value: "{lastname}", - oninput: move |e| lastname.set(e.value.clone()) + oninput: move |e| set_lastname(e.value.clone()) } textarea { class: "new-client description", placeholder: "Description", value: "{description}", - oninput: move |e| description.set(e.value.clone()) + oninput: move |e| set_description(e.value.clone()) } } button { @@ -86,13 +86,13 @@ fn app(cx: Scope) -> Element { first_name: (*firstname).clone(), last_name: (*lastname).clone(), }); - description.set(String::new()); - firstname.set(String::new()); - lastname.set(String::new()); + set_description(String::new()); + set_firstname(String::new()); + set_lastname(String::new()); }, "Add New" } - button { class: "pure-button", onclick: move |_| scene.set(Scene::ClientsList), + button { class: "pure-button", onclick: move |_| set_scene(Scene::ClientsList), "Go Back" } } @@ -108,7 +108,7 @@ fn app(cx: Scope) -> Element { } button { class: "pure-button pure-button-primary", - onclick: move |_| scene.set(Scene::ClientsList), + onclick: move |_| set_scene(Scene::ClientsList), "Go Back" } } diff --git a/examples/disabled.rs b/examples/disabled.rs index 5bfa5bbb8..f72a8ff77 100644 --- a/examples/disabled.rs +++ b/examples/disabled.rs @@ -5,13 +5,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let disabled = use_state(&cx, || false); + let (disabled, set_disabled) = use_state(&cx, || false); cx.render(rsx! { div { button { - onclick: move |_| disabled.set(!disabled.get()), - "click to " [if **disabled {"enable"} else {"disable"} ] " the lower button" + onclick: move |_| set_disabled(!disabled), + "click to " [if *disabled {"enable"} else {"disable"} ] " the lower button" } button { diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 54fb12baa..331574a75 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -24,7 +24,7 @@ fn app(cx: Scope) -> Element { .await }); - let selected_breed = use_state(&cx, || None); + let (breed, set_breed) = use_state(&cx, || None); match fut.value() { Some(Ok(breeds)) => cx.render(rsx! { @@ -36,14 +36,14 @@ fn app(cx: Scope) -> Element { breeds.message.keys().map(|breed| rsx!( li { button { - onclick: move |_| selected_breed.set(Some(breed.clone())), + onclick: move |_| set_breed(Some(breed.clone())), "{breed}" } } )) } div { flex: "50%", - match selected_breed.get() { + match breed { Some(breed) => rsx!( Breed { breed: breed.clone() } ), None => rsx!("No Breed selected"), } @@ -73,9 +73,9 @@ fn Breed(cx: Scope, breed: String) -> Element { reqwest::get(endpoint).await.unwrap().json::().await }); - let breed_name = use_state(&cx, || breed.clone()); - if breed_name.get() != breed { - breed_name.set(breed.clone()); + let (name, set_name) = use_state(&cx, || breed.clone()); + if name != breed { + set_name(breed.clone()); fut.restart(); } diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index 69e3b8faa..7cea4f5f7 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -33,7 +33,7 @@ impl Label { fn app(cx: Scope) -> Element { let items = use_ref(&cx, Vec::new); - let selected = use_state(&cx, || None); + let (selected, set_selected) = use_state(&cx, || None); cx.render(rsx! { div { class: "container", @@ -71,7 +71,7 @@ fn app(cx: Scope) -> Element { rsx!(tr { class: "{is_in_danger}", td { class:"col-md-1" } td { class:"col-md-1", "{item.key}" } - td { class:"col-md-1", onclick: move |_| selected.set(Some(id)), + td { class:"col-md-1", onclick: move |_| set_selected(Some(id)), a { class: "lbl", item.labels } } td { class: "col-md-1", diff --git a/examples/hydration.rs b/examples/hydration.rs index 6ec777352..83b7633df 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -20,13 +20,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let val = use_state(&cx, || 0); + let (val, set_val) = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "hello world. Count: {val}" } button { - onclick: move |_| *val.modify() += 1, + onclick: move |_| set_val(val + 1), "click to increment" } } diff --git a/examples/pattern_reducer.rs b/examples/pattern_reducer.rs index 82634d771..04c1811a1 100644 --- a/examples/pattern_reducer.rs +++ b/examples/pattern_reducer.rs @@ -15,16 +15,16 @@ fn main() { } fn app(cx: Scope) -> Element { - let state = use_state(&cx, PlayerState::new); + let (state, set_state) = use_state(&cx, PlayerState::new); cx.render(rsx!( div { h1 {"Select an option"} h3 { "The radio is... " [state.is_playing()] "!" } - button { onclick: move |_| state.modify().reduce(PlayerAction::Pause), + button { onclick: move |_| set_state.make_mut().reduce(PlayerAction::Pause), "Pause" } - button { onclick: move |_| state.modify().reduce(PlayerAction::Play), + button { onclick: move |_| set_state.make_mut().reduce(PlayerAction::Play), "Play" } } diff --git a/examples/readme.rs b/examples/readme.rs index 89282babd..b37a8e07a 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -9,13 +9,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "High-Five counter: {count}" } - button { onclick: move |_| *count.modify() += 1, "Up high!" } - button { onclick: move |_| *count.modify() -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } } }) } diff --git a/examples/rsx_compile_fail.rs b/examples/rsx_compile_fail.rs index 20760ef5b..8f490bf26 100644 --- a/examples/rsx_compile_fail.rs +++ b/examples/rsx_compile_fail.rs @@ -12,7 +12,7 @@ fn main() { } fn example(cx: Scope) -> Element { - let items = use_state(&cx, || { + let (items, _set_items) = use_state(&cx, || { vec![Thing { a: "asd".to_string(), b: 10, diff --git a/examples/tasks.rs b/examples/tasks.rs index f71abe38d..3c4d4cba3 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -10,14 +10,14 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); use_future(&cx, move || { - let mut count = count.for_async(); + let set_count = set_count.to_owned(); async move { loop { tokio::time::sleep(Duration::from_millis(1000)).await; - count += 1; + set_count.modify(|f| f + 1); } } }); @@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element { div { h1 { "Current count: {count}" } button { - onclick: move |_| count.set(0), + onclick: move |_| set_count(0), "Reset the count" } } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 30d1fbb10..e5f7d1dfa 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -19,15 +19,15 @@ pub struct TodoItem { } pub fn app(cx: Scope<()>) -> Element { - let todos = use_state(&cx, im_rc::HashMap::::default); - let filter = use_state(&cx, || FilterState::All); - let draft = use_state(&cx, || "".to_string()); - let (todo_id, set_todo_id) = use_state(&cx, || 0).split(); + let (todos, set_todos) = use_state(&cx, im_rc::HashMap::::default); + let (filter, set_filter) = use_state(&cx, || FilterState::All); + let (draft, set_draft) = use_state(&cx, || "".to_string()); + let (todo_id, set_todo_id) = use_state(&cx, || 0); // Filter the todos based on the filter state let mut filtered_todos = todos .iter() - .filter(|(_, item)| match **filter { + .filter(|(_, item)| match filter { FilterState::All => true, FilterState::Active => !item.checked, FilterState::Completed => item.checked, @@ -54,25 +54,25 @@ pub fn app(cx: Scope<()>) -> Element { placeholder: "What needs to be done?", value: "{draft}", autofocus: "true", - oninput: move |evt| draft.set(evt.value.clone()), + oninput: move |evt| set_draft(evt.value.clone()), onkeydown: move |evt| { if evt.key == "Enter" && !draft.is_empty() { - todos.modify().insert( + set_todos.make_mut().insert( *todo_id, TodoItem { id: *todo_id, checked: false, - contents: draft.get().clone(), + contents: draft.clone(), }, ); set_todo_id(todo_id + 1); - draft.set("".to_string()); + set_draft("".to_string()); } } } } ul { class: "todo-list", - filtered_todos.iter().map(|id| rsx!(todo_entry( key: "{id}", id: *id, todos: todos ))) + filtered_todos.iter().map(|id| rsx!(todo_entry( key: "{id}", id: *id, set_todos: set_todos ))) } (!todos.is_empty()).then(|| rsx!( footer { class: "footer", @@ -81,14 +81,14 @@ pub fn app(cx: Scope<()>) -> Element { span {"{item_text} left"} } ul { class: "filters", - li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }} - li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }} - li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }} + li { class: "All", a { onclick: move |_| set_filter(FilterState::All), "All" }} + li { class: "Active", a { onclick: move |_| set_filter(FilterState::Active), "Active" }} + li { class: "Completed", a { onclick: move |_| set_filter(FilterState::Completed), "Completed" }} } (show_clear_completed).then(|| rsx!( button { class: "clear-completed", - onclick: move |_| todos.modify().retain(|_, todo| !todo.checked), + onclick: move |_| set_todos.make_mut().retain(|_, todo| !todo.checked), "Clear completed" } )) @@ -106,24 +106,26 @@ pub fn app(cx: Scope<()>) -> Element { #[derive(Props)] pub struct TodoEntryProps<'a> { - todos: &'a UseState>, + set_todos: &'a UseState>, id: u32, } pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { - let todo = &cx.props.todos[&cx.props.id]; - let is_editing = use_state(&cx, || false); + let (is_editing, set_is_editing) = use_state(&cx, || false); + + let todos = cx.props.set_todos.get(); + let todo = &todos[&cx.props.id]; let completed = if todo.checked { "completed" } else { "" }; - let editing = if *is_editing.get() { "editing" } else { "" }; + let editing = if *is_editing { "editing" } else { "" }; rsx!(cx, li { class: "{completed} {editing}", - onclick: move |_| is_editing.set(true), - onfocusout: move |_| is_editing.set(false), + onclick: move |_| set_is_editing(true), + onfocusout: move |_| set_is_editing(false), div { class: "view", input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{todo.checked}", onchange: move |evt| { - cx.props.todos.modify()[&cx.props.id].checked = evt.value.parse().unwrap(); + cx.props.set_todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap(); } } label { r#for: "cbg-{todo.id}", pointer_events: "none", "{todo.contents}" } @@ -132,11 +134,11 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { input { class: "edit", value: "{todo.contents}", - oninput: move |evt| cx.props.todos.modify()[&cx.props.id].contents = evt.value.clone(), + oninput: move |evt| cx.props.set_todos.make_mut()[&cx.props.id].contents = evt.value.clone(), autofocus: "true", onkeydown: move |evt| { match evt.key.as_str() { - "Enter" | "Escape" | "Tab" => is_editing.set(false), + "Enter" | "Escape" | "Tab" => set_is_editing(false), _ => {} } }, diff --git a/examples/xss_safety.rs b/examples/xss_safety.rs index 36fc81a95..2790a8a89 100644 --- a/examples/xss_safety.rs +++ b/examples/xss_safety.rs @@ -9,7 +9,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let contents = use_state(&cx, || { + let (contents, set_contents) = use_state(&cx, || { String::from("") }); @@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element { input { value: "{contents}", r#type: "text", - oninput: move |e| contents.set(e.value.clone()), + oninput: move |e| set_contents(e.value.clone()), } } }) diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 139e05245..cb4afd297 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -16,45 +16,25 @@ pub use usefuture::*; mod usesuspense; pub use usesuspense::*; -// #[macro_export] -// macro_rules! to_owned { -// ($($es:ident),+) => {$( -// #[allow(unused_mut)] -// let mut $es = $es.to_owned(); -// )*} -// } - -// /// Calls `for_async` on the series of paramters. -// /// -// /// If the type is Clone, then it will be cloned. However, if the type is not `clone` -// /// then it must have a `for_async` method for Rust to lower down into. -// /// -// /// See: how use_state implements `for_async` but *not* through the trait. -// #[macro_export] -// macro_rules! for_async { -// ($($es:ident),+) => {$( -// #[allow(unused_mut)] -// let mut $es = $es.for_async(); -// )*} -// } - -// /// This is a marker trait that uses decoherence. -// /// -// /// It is *not* meant for hooks to actually implement, but rather defer to their -// /// underlying implementation if they *don't* implement the trait. -// /// -// /// -// pub trait AsyncHook { -// type Output; -// fn for_async(self) -> Self::Output; -// } - -// impl AsyncHook for T -// where -// T: ToOwned, -// { -// type Output = T; -// fn for_async(self) -> Self::Output { -// self -// } -// } +#[macro_export] +/// A helper macro for using hooks in async environements. +/// +/// # Usage +/// +/// +/// ``` +/// let (data) = use_ref(&cx, || {}); +/// +/// let handle_thing = move |_| { +/// to_owned![data] +/// cx.spawn(async move { +/// // do stuff +/// }); +/// } +/// ``` +macro_rules! to_owned { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.to_owned(); + )*} +} diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 9cfcedf4f..eb3900981 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -1,46 +1,29 @@ +#![warn(clippy::pedantic)] + use dioxus_core::prelude::*; use std::{ - cell::{Cell, Ref, RefCell, RefMut}, + cell::{RefCell, RefMut}, fmt::{Debug, Display}, rc::Rc, }; -/// Store state between component renders! +/// Store state between component renders. /// /// ## Dioxus equivalent of useState, designed for Rust /// /// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and /// modify state between component renders. When the state is updated, the component will re-render. /// -/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. -/// -/// [`use_state`] exposes a few helper methods to modify the underlying state: -/// - `.set(new)` allows you to override the "work in progress" value with a new value -/// - `.get_mut()` allows you to modify the WIP value -/// - `.get_wip()` allows you to access the WIP value -/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) -/// -/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations -/// will automatically be called on the WIP value. -/// -/// ## Combinators -/// -/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: -/// - `.classic()` and `.split()` convert the hook into the classic React-style hook -/// ```rust -/// let (state, set_state) = use_state(&cx, || 10).split() -/// ``` -/// Usage: /// /// ```ignore /// const Example: Component = |cx| { -/// let counter = use_state(&cx, || 0); +/// let (count, set_count) = use_state(&cx, || 0); /// /// cx.render(rsx! { /// div { -/// h1 { "Counter: {counter}" } -/// button { onclick: move |_| counter.set(**counter + 1), "Increment" } -/// button { onclick: move |_| counter.set(**counter - 1), "Decrement" } +/// h1 { "Count: {count}" } +/// button { onclick: move |_| set_count(a - 1), "Increment" } +/// button { onclick: move |_| set_count(a + 1), "Decrement" } /// } /// )) /// } @@ -48,34 +31,269 @@ use std::{ pub fn use_state<'a, T: 'static>( cx: &'a ScopeState, initial_state_fn: impl FnOnce() -> T, -) -> &'a UseState { - let hook = cx.use_hook(move |_| UseState { - current_val: Rc::new(initial_state_fn()), - update_callback: cx.schedule_update(), - wip: Rc::new(RefCell::new(None)), - update_scheuled: Cell::new(false), +) -> (&'a T, &'a UseState) { + let hook = cx.use_hook(move |_| { + let current_val = Rc::new(initial_state_fn()); + let update_callback = cx.schedule_update(); + let slot = Rc::new(RefCell::new(current_val.clone())); + let setter = Rc::new({ + crate::to_owned![update_callback, slot]; + move |new| { + let mut slot = slot.borrow_mut(); + + // if there's only one reference (weak or otherwise), we can just swap the values + // Typically happens when the state is set multiple times - we don't want to create a new Rc for each new value + if let Some(val) = Rc::get_mut(&mut slot) { + *val = new; + } else { + *slot = Rc::new(new); + } + + update_callback(); + } + }); + + UseState { + current_val, + update_callback, + setter, + slot, + } }); - hook.update_scheuled.set(false); - let mut new_val = hook.wip.borrow_mut(); - - if new_val.is_some() { - // if there's only one reference (weak or otherwise), we can just swap the values - if let Some(val) = Rc::get_mut(&mut hook.current_val) { - *val = new_val.take().unwrap(); - } else { - hook.current_val = Rc::new(new_val.take().unwrap()); - } - } - - hook + (hook.current_val.as_ref(), hook) } pub struct UseState { pub(crate) current_val: Rc, - pub(crate) wip: Rc>>, pub(crate) update_callback: Rc, - pub(crate) update_scheuled: Cell, + pub(crate) setter: Rc, + pub(crate) slot: Rc>>, +} + +impl UseState { + /// Get the current value of the state by cloning its container Rc. + /// + /// This is useful when you are dealing with state in async contexts but need + /// to know the current value. You are not given a reference to the state. + /// + /// # Examples + /// An async context might need to know the current value: + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (count, set_count) = use_state(&cx, || 0); + /// cx.spawn({ + /// let set_count = set_count.to_owned(); + /// async move { + /// let current = set_count.current(); + /// } + /// }) + /// } + /// ``` + #[must_use] + pub fn current(&self) -> Rc { + self.slot.borrow().clone() + } + + /// Get the `setter` function directly without the `UseState` wrapper. + /// + /// This is useful for passing the setter function to other components. + /// + /// However, for most cases, calling `to_owned` o`UseState`te is the + /// preferred way to get "anoth`set_state`tate handle. + /// + /// + /// # Examples + /// A component might require an `Rc` as an input to set a value. + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// rsx!{ + /// Component { + /// handler: set_val.setter() + /// } + /// } + /// } + /// ``` + #[must_use] + pub fn setter(&self) -> Rc { + self.setter.clone() + } + + /// Set the state to a new value, using the current state value as a reference. + /// + /// This is similar to passing a closure to React's `set_value` function. + /// + /// # Examples + /// + /// Basic usage: + /// ```rust + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// // to increment the value + /// set_value.modify(|v| v + 1); + /// + /// // usage in async + /// cx.spawn({ + /// let set_value = set_value.to_owned(); + /// async move { + /// set_value.modify(|v| v + 1); + /// } + /// }); + /// + /// # todo!() + /// } + /// ``` + pub fn modify(&self, f: impl FnOnce(&T) -> T) { + let curernt = self.slot.borrow(); + let new_val = f(curernt.as_ref()); + (self.setter)(new_val); + } + + /// Get the value of the state when this handle was created. + /// + /// This method is useful when you want an `Rc` around the data to cheaply + /// pass it around your app. + /// + /// ## Warning + /// + /// This will return a stale value if used within async contexts. + /// + /// Try `current` to get the real current value of the state. + /// + /// ## Example + /// + /// ```rust, ignore + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// let as_rc = set_value.get(); + /// assert_eq!(as_rc.as_ref(), &0); + /// + /// # todo!() + /// } + /// ``` + #[must_use] + pub fn get(&self) -> &Rc { + &self.current_val + } + + /// Mark the component that create this [`UseState`] as dirty, forcing it to re-render. + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (count, set_count) = use_state(&cx, || 0); + /// cx.spawn({ + /// let set_count = set_count.to_owned(); + /// async move { + /// // for the component to re-render + /// set_count.needs_update(); + /// } + /// }) + /// } + /// ``` + pub fn needs_update(&self) { + (self.update_callback)(); + } +} + +impl UseState { + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// If you are comfortable dealing with `RefMut`, then you can use `make_mut` to get + /// the underlying slot. However, be careful with `RefMut` since you might panic + /// if the `RefCell` is left open. + /// + /// # Examples + /// + /// ``` + /// let (val, set_val) = use_state(&cx, || 0); + /// + /// set_val.with_mut(|v| *v = 1); + /// ``` + pub fn with_mut(&self, apply: impl FnOnce(&mut T)) { + let mut slot = self.slot.borrow_mut(); + let mut inner = slot.as_ref().to_owned(); + + apply(&mut inner); + + if let Some(new) = Rc::get_mut(&mut slot) { + *new = inner; + } else { + *slot = Rc::new(inner); + } + + self.needs_update(); + } + + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// # Warning + /// Be careful with `RefMut` since you might panic if the `RefCell` is left open! + /// + /// # Examples + /// + /// ``` + /// let (val, set_val) = use_state(&cx, || 0); + /// + /// set_val.with_mut(|v| *v = 1); + /// ``` + #[must_use] + pub fn make_mut(&self) -> RefMut { + let mut slot = self.slot.borrow_mut(); + + self.needs_update(); + + if Rc::strong_count(&*slot) > 0 { + *slot = Rc::new(slot.as_ref().to_owned()); + } + + RefMut::map(slot, |rc| Rc::get_mut(rc).expect("the hard count to be 0")) + } +} + +impl ToOwned for UseState { + type Owned = UseState; + + fn to_owned(&self) -> Self::Owned { + UseState { + current_val: self.current_val.clone(), + update_callback: self.update_callback.clone(), + setter: self.setter.clone(), + slot: self.slot.clone(), + } + } +} + +impl<'a, T: 'static + Display> std::fmt::Display for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.current_val) + } +} + +impl PartialEq> for UseState { + fn eq(&self, other: &UseState) -> bool { + // some level of memoization for UseState + Rc::ptr_eq(&self.slot, &other.slot) + } } impl Debug for UseState { @@ -84,129 +302,42 @@ impl Debug for UseState { } } -impl UseState { - /// Tell the Dioxus Scheduler that we need to be processed - pub fn needs_update(&self) { - if !self.update_scheuled.get() { - self.update_scheuled.set(true); - (self.update_callback)(); - } - } - - pub fn set(&self, new_val: T) { - *self.wip.borrow_mut() = Some(new_val); - self.needs_update(); - } - - pub fn get(&self) -> &T { - &self.current_val - } - - pub fn get_rc(&self) -> &Rc { - &self.current_val - } - - /// Get the current status of the work-in-progress data - pub fn get_wip(&self) -> Ref> { - self.wip.borrow() - } - - /// Get the current status of the work-in-progress data - pub fn get_wip_mut(&self) -> RefMut> { - self.wip.borrow_mut() - } - - pub fn split(&self) -> (&T, Rc) { - (&self.current_val, self.setter()) - } - - pub fn setter(&self) -> Rc { - let slot = self.wip.clone(); - let callback = self.update_callback.clone(); - Rc::new(move |new| { - callback(); - *slot.borrow_mut() = Some(new) - }) - } - - pub fn wtih(&self, f: impl FnOnce(&mut T)) { - let mut val = self.wip.borrow_mut(); - - if let Some(inner) = val.as_mut() { - f(inner); - } - } - - pub fn for_async(&self) -> UseState { - let UseState { - current_val, - wip, - update_callback, - update_scheuled, - } = self; - - UseState { - current_val: current_val.clone(), - wip: wip.clone(), - update_callback: update_callback.clone(), - update_scheuled: update_scheuled.clone(), - } - } -} - -impl> UseState { - /// Gain mutable access to the new value via [`RefMut`]. - /// - /// If `modify` is called, then the component will re-render. - /// - /// This method is only available when the value is a `ToOwned` type. - /// - /// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value. - /// - /// To get a reference to the current value, use `.get()` - pub fn modify(&self) -> RefMut { - // make sure we get processed - self.needs_update(); - - // Bring out the new value, cloning if it we need to - // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this - RefMut::map(self.wip.borrow_mut(), |slot| { - if slot.is_none() { - *slot = Some(self.current_val.as_ref().to_owned()); - } - slot.as_mut().unwrap() - }) - } - - pub fn inner(self) -> T { - self.current_val.as_ref().to_owned() - } -} - impl<'a, T> std::ops::Deref for UseState { - type Target = T; + type Target = Rc; fn deref(&self) -> &Self::Target { - self.get() + &self.setter } } -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.current_val) - } -} +#[test] +fn api_makes_sense() { + #[allow(unused)] + fn app(cx: Scope) -> Element { + let (val, set_val) = use_state(&cx, || 0); -impl<'a, V, T: PartialEq> PartialEq for UseState { - fn eq(&self, other: &V) -> bool { - self.get() == other - } -} -impl<'a, O, T: std::ops::Not + Copy> std::ops::Not for UseState { - type Output = O; + set_val(0); + set_val.modify(|v| v + 1); + let real_current = set_val.current(); - fn not(self) -> Self::Output { - !*self.get() + match val { + 10 => { + set_val(20); + set_val.modify(|v| v + 1); + } + 20 => {} + _ => { + println!("{real_current}"); + } + } + + cx.spawn({ + crate::to_owned![set_val]; + async move { + set_val.modify(|f| f + 1); + } + }); + + cx.render(LazyNodes::new(|f| f.static_text("asd"))) } } From 25286a6711e2505601f34c39bb8166d793dc927e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 14:42:07 -0500 Subject: [PATCH 013/256] Create FUNDING.yml --- .github/FUNDING.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..332e03eb2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: jkelleyrtp # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +open_collective: dioxus-labs # Replace with a single Open Collective username From 6073bddf308278661d3f22bc626e6ad676d9e15b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 27 Jan 2022 15:53:06 -0500 Subject: [PATCH 014/256] docs: fix readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 658a6e48d..52493b119 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,12 @@ Dioxus is a portable, performant, and ergonomic framework for building cross-pla ```rust fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } )) } ``` From 430cde7068d308f2783e33d278fd0c0efa659c1b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 27 Jan 2022 16:36:17 -0500 Subject: [PATCH 015/256] feat: default asset server --- examples/assets/logo.png | Bin 0 -> 59292 bytes examples/custom_assets.rs | 18 ++++++++++++++++++ packages/desktop/Cargo.toml | 7 ++++--- packages/desktop/src/lib.rs | 35 ++++++++++++++++++++++++++++++----- 4 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 examples/assets/logo.png create mode 100644 examples/custom_assets.rs diff --git a/examples/assets/logo.png b/examples/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7d08f2af8f057780eddae4b0f8b8fa753b69e7f8 GIT binary patch literal 59292 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 Element { + let (disabled, set_disabled) = use_state(&cx, || false); + + cx.render(rsx! { + div { + "hi" + img { + src: "examples/../../../assets/logo.png", + } + } + }) +} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index eb9425c1f..123013e5e 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7", features = ["serialize"] } +dioxus-core = { path = "../core", version = "^0.1.7", features = ["serialize"] } argh = "0.1.4" serde = "1.0.120" serde_json = "1.0.61" @@ -27,9 +27,10 @@ tokio = { version = "1.12.0", features = [ "rt", "time", ], optional = true, default-features = false } -dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"} -dioxus-html = { path = "../html", features = ["serialize"], version ="^0.1.4"} +dioxus-core-macro = { path = "../core-macro", version = "^0.1.6" } +dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.4" } webbrowser = "0.5.5" +mime_guess = "2.0.3" [features] default = ["tokio_runtime"] diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 93c175cc6..36b017077 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -210,18 +210,43 @@ pub fn launch_with_props( // For now, we only serve two pieces of content which get included as bytes into the final binary. let path = request.uri().replace("dioxus://", ""); - if path.trim_end_matches('/') == "index.html" { + // all assets shouldbe called from index.html + let trimmed = path.trim_start_matches("index.html/"); + + if trimmed.is_empty() { wry::http::ResponseBuilder::new() .mimetype("text/html") .body(include_bytes!("./index.html").to_vec()) - } else if path.trim_end_matches('/') == "index.html/index.js" { + } else if trimmed == "index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") .body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec()) } else { - wry::http::ResponseBuilder::new() - .status(wry::http::status::StatusCode::NOT_FOUND) - .body(format!("Not found: {}", path).as_bytes().to_vec()) + // Read the file content from file path + use std::fs::read; + + let path_buf = std::path::Path::new(trimmed).canonicalize()?; + let cur_path = std::path::Path::new(".").canonicalize()?; + + if !path_buf.starts_with(cur_path) { + return wry::http::ResponseBuilder::new() + .status(wry::http::status::StatusCode::FORBIDDEN) + .body(String::from("Forbidden").into_bytes()); + } + + if !path_buf.exists() { + return wry::http::ResponseBuilder::new() + .status(wry::http::status::StatusCode::NOT_FOUND) + .body(String::from("Not Found").into_bytes()); + } + + let mime = mime_guess::from_path(&path_buf).first_or_octet_stream(); + + // do not let path searching to go two layers beyond the caller level + let data = read(path_buf)?; + let meta = format!("{mime}"); + + wry::http::ResponseBuilder::new().mimetype(&meta).body(data) } }) .with_file_drop_handler(move |window, evet| { From 6e4ed9e3510aa7383652adbf786677a86409ca6a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 27 Jan 2022 16:37:09 -0500 Subject: [PATCH 016/256] examples: fixup asset example --- examples/custom_assets.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/custom_assets.rs b/examples/custom_assets.rs index 931caa839..e623d5b50 100644 --- a/examples/custom_assets.rs +++ b/examples/custom_assets.rs @@ -5,14 +5,10 @@ fn main() { } fn app(cx: Scope) -> Element { - let (disabled, set_disabled) = use_state(&cx, || false); - cx.render(rsx! { div { - "hi" - img { - src: "examples/../../../assets/logo.png", - } + "This should show an image:" + img { src: "examples/assets/logo.png", } } }) } From 22308eb26a9ea48b14f5f5abb833aa90a4e3fc40 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 27 Jan 2022 17:00:40 -0500 Subject: [PATCH 017/256] fix: custom protocol receiver type --- packages/desktop/src/cfg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index ad85cfc0f..5baa46f5d 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -71,7 +71,7 @@ impl DesktopConfig { self } - pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self + pub fn with_custom_protocol(&mut self, name: String, handler: F) -> &mut Self where F: Fn(&HttpRequest) -> WryResult + 'static, { From faf87ce316700eff9a81d6b35000c9da76e9d2be Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 28 Jan 2022 09:56:56 +0800 Subject: [PATCH 018/256] feat: add `issue` template --- .github/ISSUE_TEMPLATE/desktop.md | 28 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 16 +++++++++++++ .github/ISSUE_TEMPLATE/web.md | 27 ++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/desktop.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/web.md diff --git a/.github/ISSUE_TEMPLATE/desktop.md b/.github/ISSUE_TEMPLATE/desktop.md new file mode 100644 index 000000000..cbc26136e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/desktop.md @@ -0,0 +1,28 @@ +--- +name: Desktop Platform Issue +about: The Problem with desktop platform +--- + +## Enviroment + +> development enviroment info. + +- system version: +- dioxus version: +- rust version: + +## Error Info + +> verbose error information. + +``` + +``` + +## Bad Code + +> the code where error occurred. + +```rust + +``` \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..f8735b9a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,16 @@ +--- +name: Feature Request +about: If you have any interesting advice, you can tell us. +--- + +## Specific Demand + + + +## Implement Suggestion + + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/web.md b/.github/ISSUE_TEMPLATE/web.md new file mode 100644 index 000000000..f92ec0b99 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/web.md @@ -0,0 +1,27 @@ +--- +name: Web(WASM) Platform Issue +about: The Problem with Web | WASM platform +--- + +## Enviroment + +> development enviroment info. + +- dioxus version: +- rust version: + +## Error Info + +> verbose error information. + +``` + +``` + +## Bad Code + +> the code where error occurred. + +```rust + +``` \ No newline at end of file From 6df36feb70c32d653438e8ed363924c6918cb2bf Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 28 Jan 2022 10:02:45 +0800 Subject: [PATCH 019/256] feat: add `issue` template --- .github/ISSUE_TEMPLATE/core_problem.md | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/core_problem.md diff --git a/.github/ISSUE_TEMPLATE/core_problem.md b/.github/ISSUE_TEMPLATE/core_problem.md new file mode 100644 index 000000000..0e7cb3390 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/core_problem.md @@ -0,0 +1,28 @@ +--- +name: Core Development Issue +about: Something that doesn't directly relate to the platform +--- + +## Enviroment + +> development enviroment info. + +- dioxus version: +- dioxus feature: ["web", "desktop", ..."] +- rust version: + +## Error Info + +> verbose error information. + +``` + +``` + +## Bad Code + +> the code where error occurred. + +```rust + +``` \ No newline at end of file From 4fb2221ee18d85c1f4b7bbcbf06c1b962d42bbbe Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 02:44:13 -0500 Subject: [PATCH 020/256] docs: buff up the theory of react --- .vscode/spellright.dict | 1 + docs/guide/src/SUMMARY.md | 2 +- docs/guide/src/elements/component_children.md | 4 +- docs/guide/src/elements/components.md | 2 +- docs/guide/src/elements/composing.md | 244 ++++++++++++++++-- .../src/elements/exporting_components.md | 11 +- docs/guide/src/elements/lists.md | 20 +- docs/guide/src/elements/special_attributes.md | 8 +- docs/guide/src/elements/vnodes.md | 2 +- docs/guide/src/images/oldnew.png | Bin 0 -> 124296 bytes docs/guide/src/setup.md | 6 +- packages/desktop/examples/async.rs | 8 +- 12 files changed, 263 insertions(+), 45 deletions(-) create mode 100644 docs/guide/src/images/oldnew.png diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 1333ce02e..3b67f08b6 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -71,3 +71,4 @@ reborrow VirtualDoms bootstrapper WebkitGtk +laymans diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 32d3eb88b..9772f6b54 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -13,7 +13,7 @@ - [Properties](elements/propsmacro.md) - [Reusing, Importing, and Exporting](elements/exporting_components.md) - [Children and Attributes](elements/component_children.md) - - [Composing Components](elements/composing.md) + - [Theory of React](elements/composing.md) - [Adding Interactivity](interactivity/index.md) - [Hooks and Internal State](interactivity/hooks.md) - [Event handlers](interactivity/event_handlers.md) diff --git a/docs/guide/src/elements/component_children.md b/docs/guide/src/elements/component_children.md index d99273394..52f116881 100644 --- a/docs/guide/src/elements/component_children.md +++ b/docs/guide/src/elements/component_children.md @@ -137,7 +137,7 @@ fn clickable(cx: Scope) -> Element { } ``` -## Passing attributes + ## Passing handlers diff --git a/docs/guide/src/elements/components.md b/docs/guide/src/elements/components.md index ea43034d9..8b284b793 100644 --- a/docs/guide/src/elements/components.md +++ b/docs/guide/src/elements/components.md @@ -106,7 +106,7 @@ fn Post(cx: Scope) -> Element { } ``` -When declaring a component in `rsx!`, we can pass in properties using the traditional Rust struct syntax. Dioxus will automatically call "into" on the property fields, cloning when necessary. Our `Post` component is simply a collection of smaller components wrapped together in a single container. +When declaring a component in `rsx!`, we can pass in properties using the traditional Rust struct syntax. Our `Post` component is simply a collection of smaller components wrapped together in a single container. 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. diff --git a/docs/guide/src/elements/composing.md b/docs/guide/src/elements/composing.md index e4b00de2e..e6ccf189e 100644 --- a/docs/guide/src/elements/composing.md +++ b/docs/guide/src/elements/composing.md @@ -1,43 +1,243 @@ -# Composing Components +# Thinking in React -So far, we've talked about declaring new components and setting up their properties. However, we haven't really talked about how components work together and how your app is updated. +We've finally reached the point in our tutorial where we can talk about the "Theory of React." We've talked about defining a declarative view, but not about the aspects that make our code *reactive*. + +Understanding the theory of reactive program is essential to making sense of Dioxus and writing effective, performant UIs. In this section, we'll talk about: -- Sharing data between components -- How the UI is updated from input and state changes +- One-way data flow +- Modifying data - Forcing renders - How renders propagate -- +This section is a bit long, but worth the read. We recommend coffee, tea, and/or snacks. -### Rendering our posts with a PostList component +## Reactive Programming -Let's start by modeling this problem with a component and some properties. +Dioxus is one the very few Rust libraries that provide a "Reactive Programming Model". The term "Reactive programming" is a classification of programming paradigm - much like functional or imperative programming. This is a very important distinction since it affects how we *think* about our code. -For this example, we're going to use the borrowed component syntax since we probably have a large list of posts that we don't want to clone every time we render the Post List. +Reactive programming is programming model concerned with deriving computations from asynchronous data flow. Most reactive programs are comprised of a handful of datasources, intermediate computations, and a final result. + +We can consider the our rendered GUI to be the final result of reactive app and our datasources to include shared contexts and component properties. + +For example, the model presented in the figure below is comprised of two data sources: time and a constant. These values are passed through our computation graph to achieve a final result: `g`. + +![Reactive Model](https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Reactive_programming_glitches.svg/440px-Reactive_programming_glitches.svg.png) + +Whenever our `seconds` variable changes, then we will reevaluate the computation for `t`. Because `g` relies on `t`, then we will also reevaluate its computation too. Notice that we would've reevaluated the computation for `g` even if `t` didn't change because `seconds` is used to calculate `g`. + +However, if we somehow changed our constant from `1` to `2`, then we need to reevaluate `t`. If, for whatever reason, this change did not affect the result of `t`, then we wouldn't try to reevaluate `g`. + +In Reactive Programming, we don't think about whether or not we should reevaluate `t` or `g`; instead, we simply provide functions of computation and let the framework figure out the rest for us. + +In Rust, our reactive app would look something like: ```rust -#[derive(Props, PartialEq)] -struct PostListProps<'a> { - posts: &'a [PostData] +fn compute_g(t: i32, seconds: i32) -> bool { + t > seconds +} + +fn compute_t(constant: i32, seconds: i32) -> i32 { + constant + seconds +} + +fn compute_graph(constant: i32, seconds: i32) -> bool { + let t = compute_t(constant, seconds); + let g = compute_g(t, seconds); + g } ``` -Next, we're going to define our component: + +## How is Dioxus Reactive? + +The Dioxus VirtualDom provides us a framework for reactive programming. When we build apps with dioxus, we need to feed it our own custom datasources. This can be either initial props or some values fetched from the network. We then pass this data through our app into components through properties. + +If we represented the reactive graph presented above in Dioxus, it would look very similar: ```rust -fn App(cx: Scope) -> Element { +// Declare a component that holds our datasources and calculates `g` +fn RenderGraph(cx: Scope) -> Element { + let seconds = use_datasource(SECONDS); + let constant = use_state(&cx, || 1); + + cx.render(rsx!( + RenderG { seconds: seconds } + RenderT { seconds: seconds, constant: constant } + )) +} + +// "calculate" g by rendering `t` and `seconds` +#[inline_props] +fn RenderG(cx: Scope, seconds: i32) -> Element { + cx.render(rsx!{ "There are {seconds} seconds remaining..." }) +} + +// calculate and render `t` in its own component +#[inline_props] +fn RenderT(cx: Scope, seconds: i32, constant: i32) -> Element { + let res = seconds + constant; + cx.render(rsx!{ "{res}" }) +} +``` + +With this app, we've defined three components. Our top-level component provides our datasources (the hooks), computation nodes (child components), and a final value (what's "rendered"). + +Now, whenever the `constant` changes, our `RenderT` component will be re-rendered. However, if `seconds` doesn't change, then we don't need to re-render `RenderG` because the input is the same. If `seconds` *does* change, then both RenderG and RenderT will be reevaluated. + +Dioxus is "Reactive" because it provides this framework for us. All we need to do is write our own tiny units of computations and Dioxus figures out which components need to be reevaluated automatically. + +These extra checks and algorithms add some overhead, which is why you see projects like [Sycamore](http://sycamore-rs.netlify.app) and [SolidJS](http://solidjs.com) eliminating them altogether. Dioxus is *really* fast, so we're willing to exchange the added overhead for improved developer experience. + +## How do we update values in our dataflow graph? + +Dioxus will automatically figure out how to regenerate parts of our app when datasources change. But how exactly can we update our data sources? + +In Dioxus there are two datasources: + +1. Local state in `use_hook` and all other hooks +2. Global state through `provide_context`. + +Technically, the root props of the VirtualDom are a third datasource, but since we cannot modify them, they are not worth talking about. + +### Local State + +For local state in hooks, Dioxus gives us the `use_hook` method which returns an `&mut T` without any requirements. This means raw hook values are not tracked by Dioxus. In fact, we could write a component that modifies a hook value directly: + +```rust +fn app(cx: Scope) -> Element { + let mut count = cx.use_hook(|_| 0); cx.render(rsx!{ - ul { class: "post-list", - // we can drop an iterator directly into our elements - cx.props.posts.iter().map(|post| rsx!{ - Post { - title: post.title, - age: post.age, - original_poster: post.original_poster - } - }) + button { + onclick: move |_| *count += 1, + "Count: {count}" } }) } ``` + +However, when this value is written to, the component does not know to be reevaluated. We must explicitly tell Dioxus that this component is "dirty" and needs to be re-rendered. This is done through the `cx.needs_update` method: + +```rust +button { + onclick: move |_| { + *count += 1; + cx.needs_update(); + }, + "Count: {count}" +} +``` + +Now, whenever we click the button, the value will change and the component will be re-rendered. + +> Re-rendering is when Dioxus calls your function component *again*. Component functions will be called over and over throughout their lifetime, so they should be mostly side-effect free. + +### Understand this! + +Your component functions will be called ("rendered" in our lingo) for as long as the component is present in the tree. + +A single component will be called multiple times, modifying its own internal state or rendering new nodes with new values from its properties. + +### App-Global State + +With the `provide_context` and `consume_context` methods on `Scope`, we can share values to descendants without having to pass values through component props. This has the side-effect of making our datasources less obvious from a high-level perspective, but it makes our components more modular within the same codebase. + +To make app-global state easier to reason about, Dioxus makes all values provided through `provide_context` immutable. This means any library built on top of `provide_context` needs to use interior mutability to modify share global state. + +In these cases, App-Global state needs to manually track which components need to be re-generated. + +To regenerate *any* component in your app, you can get a handle the Dioxus' internal scheduler through `schedule_update_any`: + +```rust +let force_render = cx.schedule_update_any(); + +// force a render of the root component +force_render(ScopeId(0)); +``` + +## What does it mean for a component to "re-render" + +In our guides, we frequently use the phrase "re-render" to describe updates to our app. You'll often hear this paired with "preventing unnecessary re-renders." But what exactly does this mean? + +When we call `dioxus::desktop::launch`, Dioxus will create a new `Scope` object and call the component we gave it. Our `rsx!` calls will create new nodes which we return back to the VirtualDom. Dioxus will then look through these nodes for child components, call their functions, and so on until every component has been "rendered." We consider these nodes "rendered" because they were created because of our explicit actions. + +The tree of UI that dioxus creates will roughly look like the tree of components presented earlier: + +![Tree of UI](../images/component_tree.png) + +But what happens when we call `needs_update` after modifying some important state? Well, if Dioxus called our component's function again, then we would produce new, different nodes. In fact, this is exactly what Dioxus does! + +At this point, we have some old nodes and some new nodes. Again, we call this "rendering" because Dioxus had to create new nodes because of our explicit actions. Any time new nodes get created, our VirtualDom is being "rendered." + +These nodes are stored in an extremely efficient memory allocator called a "bump arena." For example, a div with a handler and attribute would be stored in memory in two locations: the "old" tree and the "new" tree. + +![Bump Arenas](../images/oldnew.png) + +From here, Dioxus computes the difference between these trees and updates the Real DOM to make it look like the new version of what we've declared. + +![Diffing](../images/diffing.png) + +## Suppressing Renders + +So, we know how to make Dioxus render, but how do we *stop* it? What if we *know* that our state didn't change and we shouldn't render and diff new nodes because they'll be exactly the same as the last time? + +In these cases, you want to reach for *memoization*. In Dioxus, memoization involves preventing a component from rendering again if its props didn't change since the last time it attempted to render. + +Visually, you can tell that a component will only re-render if the new value is sufficiently different than the old one. + +| props.val | re-render | +| --------- | --------- | +| 10 | true | +| 20 | true | +| 20 | false | +| 20 | false | +| 10 | true | +| 30 | false | + +This is why when you `derive(Props)`, you must also implement the `PartialEq` trait. To override the memoization strategy for a component, you can simply implement your own PartialEq. + +```rust +struct CustomProps { + val: i32, +} + +impl PartialEq for CustomProps { + fn partial_eq(&self, other: &Self) -> bool { + // we don't render components that have a val less than 5 + if other.val > 5 && self.val > 5{ + self.val == other.val + } + } +} +``` + +However, for components that borrow data, it doesn't make sense to implement PartialEq since the actual references in memory might be different. + +You can technically override this behavior by implementing the `Props` trait manually, though it's unsafe and easy to mess up: + +```rust +impl Properties for CustomProps { + fn memoize(&self, other &Self) -> bool { + self != other + } +} +``` + +TLDR: +- Dioxus checks if props changed between renders +- If props changed according to PartialEq, Dioxus re-renders the component +- Props that have a lifetime (ie `<'a>`) will always be re-rendered + +## Wrapping Up + +Wow, that was a lot of material! + +Let's see if we can recap what was presented: + +- Reactive programming calculates a final value from datasources and computation +- Dioxus is "reactive" since it figures out which computations to check +- `schedule_update` must be called to mark a component as dirty +- dirty components will be re-rendered (called multiple times) to produce a new UI +- Renders can be suppressed with memoization + +This theory is crucial to understand how to compose components and how to control renders in your app. diff --git a/docs/guide/src/elements/exporting_components.md b/docs/guide/src/elements/exporting_components.md index 117249db8..350484c95 100644 --- a/docs/guide/src/elements/exporting_components.md +++ b/docs/guide/src/elements/exporting_components.md @@ -66,6 +66,14 @@ fn ActionCard(Scope) -> Element {} We should also create a `mod.rs` file in the `post` folder so we can use it from our `main.rs`. Our `Post` component and its props will go into this file. +```rust +use dioxus::prelude::*; + +#[derive(PartialEq, Props)] +struct PostProps {} +fn Post(Scope) -> Element {} +``` + ```shell ├── Cargo.toml └── src @@ -78,8 +86,6 @@ We should also create a `mod.rs` file in the `post` folder so we can use it from └── mod.rs ``` - - In our `main.rs`, we'll want to declare the `post` module so we can access our `Post` component. ```rust @@ -164,7 +170,6 @@ pub fn Post(Scope) -> Element { } ``` - Ultimately, including and exporting components is governed by Rust's module system. [The Rust book is a great resource to learn about these concepts in greater detail.](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html) ## Final structure: diff --git a/docs/guide/src/elements/lists.md b/docs/guide/src/elements/lists.md index 4c56c3d88..41c6a892d 100644 --- a/docs/guide/src/elements/lists.md +++ b/docs/guide/src/elements/lists.md @@ -10,14 +10,24 @@ In this chapter, you will learn: ## Rendering data from lists -Thinking back to our analysis of the `r/reddit` page, we notice a list of data that needs to be rendered: the list of posts. This list of posts is always changing, so we cannot just hardcode the lists into our app like: +If we wanted to build the Reddit app, then we need to implement a list of data that needs to be rendered: the list of posts. This list of posts is always changing, so we cannot just hardcode the lists into our app directly, like so: ```rust +// we shouldn't ship our app with posts that don't update! rsx!( div { - Post {/* some properties */} - Post {/* some properties */} - Post {/* some properties */} + Post { + title: "Post A", + votes: 120, + } + Post { + title: "Post B", + votes: 14, + } + Post { + title: "Post C", + votes: 999, + } } ) ``` @@ -168,7 +178,7 @@ File names in a folder and Element keys in an array serve a similar purpose. The ### Gotcha You might be tempted to use an item’s index in the array as its key. In fact, that’s what Dioxus will use if you don’t specify a key at all. But the order in which you render items will change over time if an item is inserted, deleted, or if the array gets reordered. Index as a key often leads to subtle and confusing bugs. -Similarly, do not generate keys on the fly, `gen_random`. This will cause keys to never match up between renders, leading to all your components and DOM being recreated every time. Not only is this slow, but it will also lose any user input inside the list items. Instead, use a stable ID based on the data. +Similarly, do not generate keys on the fly, like `gen_random`. This will cause keys to never match up between renders, leading to all your components and DOM being recreated every time. Not only is this slow, but it will also lose any user input inside the list items. Instead, use a stable ID based on the data. Note that your components won’t receive key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop: ```rust diff --git a/docs/guide/src/elements/special_attributes.md b/docs/guide/src/elements/special_attributes.md index 9a1122a3e..460d1cd69 100644 --- a/docs/guide/src/elements/special_attributes.md +++ b/docs/guide/src/elements/special_attributes.md @@ -7,7 +7,7 @@ In this section, we'll cover special attributes built into Dioxus: - `dangerous_inner_html` - Boolean attributes - `prevent_default` -- `..Attributes` + - event handlers as string attributes - `value`, `checked`, and `selected` @@ -104,9 +104,11 @@ rsx!{ } } ``` - + ## Controlled inputs and `value`, `checked`, and `selected` diff --git a/docs/guide/src/elements/vnodes.md b/docs/guide/src/elements/vnodes.md index f8959c123..3c136cfc2 100644 --- a/docs/guide/src/elements/vnodes.md +++ b/docs/guide/src/elements/vnodes.md @@ -170,4 +170,4 @@ We learned: - Elements can either be a named container or text - Some Elements have properties that the renderer can use to draw the UI to the screen -Next, we'll compose Elements together to form components. +Next, we'll compose Elements together using Rust-based logic. diff --git a/docs/guide/src/images/oldnew.png b/docs/guide/src/images/oldnew.png new file mode 100644 index 0000000000000000000000000000000000000000..5aca37e4079cf9984fe7341823406dd7d36b4ebb GIT binary patch literal 124296 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 Element { - let count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); use_future(&cx, || { - let count = count.for_async(); + to_owned![set_count]; async move { loop { tokio::time::sleep(Duration::from_millis(1000)).await; - *count.modify() += 1; + set_count.modify(|v| v + 1) } } }); @@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element { div { h1 { "High-Five counter: {count}" } button { - onclick: move |_| count.set(0), + onclick: move |_| set_count(0), "Click me!" } } From 564284f4be9b2931cd49f7e5435e0e8368fddd53 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 03:04:21 -0500 Subject: [PATCH 021/256] docs: add rules of hooks --- .vscode/spellright.dict | 1 + docs/guide/src/SUMMARY.md | 5 +- docs/guide/src/interactivity/hooks.md | 73 +++++++++++++++++++ .../guide/src/interactivity/importanthooks.md | 23 ++++++ docs/guide/src/interactivity/index.md | 20 +++-- 5 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 docs/guide/src/interactivity/importanthooks.md diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 3b67f08b6..fb7eef771 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -72,3 +72,4 @@ VirtualDoms bootstrapper WebkitGtk laymans +iter diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 9772f6b54..5a8fadb96 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -13,10 +13,11 @@ - [Properties](elements/propsmacro.md) - [Reusing, Importing, and Exporting](elements/exporting_components.md) - [Children and Attributes](elements/component_children.md) - - [Theory of React](elements/composing.md) + - [Theory of Reactive Programming](elements/composing.md) - [Adding Interactivity](interactivity/index.md) - [Hooks and Internal State](interactivity/hooks.md) - - [Event handlers](interactivity/event_handlers.md) + - [Event Listeners](interactivity/event_handlers.md) + - [UseState and UseRef](interactivity/importanthooks.md) - [User Input and Controlled Components](interactivity/user_input.md) - [Lifecycle, updates, and effects](interactivity/lifecycles.md) - [Managing State](state/index.md) diff --git a/docs/guide/src/interactivity/hooks.md b/docs/guide/src/interactivity/hooks.md index adf43c6ef..b9ec32e58 100644 --- a/docs/guide/src/interactivity/hooks.md +++ b/docs/guide/src/interactivity/hooks.md @@ -76,6 +76,78 @@ This is why hooks called out of order will fail - if we try to downcast a `Hook< 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). + +## Rules of hooks + +Hooks are sensitive to how they are used. To use hooks, you must abide by the +"rules of hooks" (borrowed from react)](https://reactjs.org/docs/hooks-rules.html): + +- Functions with "use_" should not be called in callbacks +- Functions with "use_" should not be called out of order +- Functions with "use_" should not be called in loops or conditionals + +Examples of "no-nos" include: + +### ❌ Nested uses + +```rust +// ❌ don't call use_hook or any `use_` function *inside* use_hook! +cx.use_hook(|_| { + let name = cx.use_hook(|_| "ads"); +}) + +// ✅ instead, move the first hook above +let name = cx.use_hook(|_| "ads"); +cx.use_hook(|_| { + // do something with name here +}) +``` + +### ❌ Uses in conditionals +```rust +// ❌ don't call use_ in conditionals! +if do_thing { + let name = use_state(&cx, || 0); +} + +// ✅ instead, *always* call use_state but leave your logic +let name = use_state(&cx, || 0); +if do_thing { + // do thing with name here +} +``` + +### ❌ Uses in loops + + +```rust +// ❌ Do not use hooks in loops! +let mut nodes = vec![]; + +for name in names { + let age = use_state(&cx, |_| 0); + nodes.push(cx.render(rsx!{ + div { "{age}" } + })) +} + +// ✅ Instead, consider refactoring your usecase into components +#[inline_props] +fn Child(cx: Scope, name: String) -> Element { + let age = use_state(&cx, |_| 0); + cx.render(rsx!{ div { "{age}" } }) +} + +// ✅ Or, use a hashmap with use_ref +```rust +let ages = use_ref(&cx, |_| HashMap::new()); + +names.iter().map(|name| { + let age = ages.get(name).unwrap(); + cx.render(rsx!{ div { "{age}" } }) +}) +``` + ## 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. @@ -168,6 +240,7 @@ fn example(cx: Scope) -> Element { ``` + ## 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. diff --git a/docs/guide/src/interactivity/importanthooks.md b/docs/guide/src/interactivity/importanthooks.md new file mode 100644 index 000000000..91d50cab9 --- /dev/null +++ b/docs/guide/src/interactivity/importanthooks.md @@ -0,0 +1,23 @@ +# `use_state` and `use_ref` + +Most components you will write in Dioxus will need to store state somehow. For local state, we provide two very convenient hooks: + +- `use_state` +- `use_ref` + +Both of these hooks are extremely powerful and flexible, so we've dedicated this section to understanding them properly. + +## Note on Hooks + +If you're struggling with errors due to usage in hooks, make sure you're following the rules of hooks: + + +## `use_state` + +The `use_state` hook is very similar to its React counterpart. + + + + +## `use_ref` + diff --git a/docs/guide/src/interactivity/index.md b/docs/guide/src/interactivity/index.md index 0e1a61252..b71a3e73d 100644 --- a/docs/guide/src/interactivity/index.md +++ b/docs/guide/src/interactivity/index.md @@ -64,7 +64,7 @@ The most common hook you'll use for storing state is `use_state`. `use_state` pr ```rust fn App(cx: Scope)-> Element { - let post = use_state(&cx, || { + let (post, set_post) = use_state(&cx, || { PostData { id: Uuid::new_v4(), score: 10, @@ -84,10 +84,10 @@ fn App(cx: Scope)-> Element { } ``` -Whenever we have a new post that we want to render, we can call `set` on `post` and provide a new value: +Whenever we have a new post that we want to render, we can call `set_post` and provide a new value: ```rust -post.set(PostData { +set_post(PostData { id: Uuid::new_v4(), score: 20, comment_count: 0, @@ -128,7 +128,13 @@ We'll dive much deeper into event listeners later. ### Updating state asynchronously -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 also update our state outside of event listeners with `futures` and `coroutines`. + +- `Futures` are Rust's version of promises that can execute asynchronous work by an efficient polling system. We can submit new futures to Dioxus either through `push_future` which returns a `TaskId` or with `spawn`. +- +- `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 and Futures 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. @@ -139,7 +145,7 @@ fn App(cx: Scope)-> Element { let mut sec_elapsed = use_state(&cx, || 0); use_future(&cx, || { - let mut sec_elapsed = sec_elapsed.for_async(); + to_owned![sec_elapsed]; // explicitly capture this hook for use in async async move { loop { TimeoutFuture::from_ms(1000).await; @@ -152,7 +158,7 @@ fn App(cx: Scope)-> Element { } ``` -Using asynchronous code can be difficult! This is just scratching the surface of what's possible. We have an entire chapter on using async properly in your Dioxus Apps. +Using asynchronous code can be difficult! This is just scratching the surface of what's possible. We have an entire chapter on using async properly in your Dioxus Apps. We have an entire section dedicated to using `async` properly later in this book. ### How do I tell Dioxus that my state changed? @@ -170,7 +176,7 @@ With these building blocks, we can craft new hooks similar to `use_state` that l In general, Dioxus should be plenty fast for most use cases. However, there are some rules you should consider following to ensure your apps are quick. -- 1) **Don't call set—state _while rendering_**. This will cause Dioxus to unnecessarily re-check the component for updates. +- 1) **Don't call set—state _while rendering_**. This will cause Dioxus to unnecessarily re-check the component for updates or enter an infinite loop. - 2) **Break your state apart into smaller sections.** Hooks are explicitly designed to "unshackle" your state from the typical model-view-controller paradigm, making it easy to reuse useful bits of code with a single function. - 3) **Move local state down**. Dioxus will need to re-check child components of your app if the root component is constantly being updated. You'll get best results if rapidly-changing state does not cause major re-renders. From b0fd034ae70a0fb373e62a8f5c10e5eaf207183b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 03:20:42 -0500 Subject: [PATCH 022/256] docs: add important hooks --- .vscode/spellright.dict | 3 + .../guide/src/interactivity/importanthooks.md | 119 +++++++++++++++++- 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index fb7eef771..7a5a261d7 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -73,3 +73,6 @@ bootstrapper WebkitGtk laymans iter +cloneable +fudamental +clonable diff --git a/docs/guide/src/interactivity/importanthooks.md b/docs/guide/src/interactivity/importanthooks.md index 91d50cab9..eff27b199 100644 --- a/docs/guide/src/interactivity/importanthooks.md +++ b/docs/guide/src/interactivity/importanthooks.md @@ -11,13 +11,130 @@ Both of these hooks are extremely powerful and flexible, so we've dedicated this If you're struggling with errors due to usage in hooks, make sure you're following the rules of hooks: +- Functions with "use_" should not be called in callbacks +- Functions with "use_" should not be called out of order +- Functions with "use_" should not be called in loops or conditionals + +A large majority of issues that seem to be "wrong with Dioxus" are actually just a misuse of hooks. ## `use_state` -The `use_state` hook is very similar to its React counterpart. +The `use_state` hook is very similar to its React counterpart. When we use it, we get two values: +- The value itself as an `&T` +- A handle to set the value `&UseState` +```rust +let (count, set_count) = use_state(&cx, || 0); +// then to set the count +set_count(count + 1) +``` + +However, the `set_count` object is more powerful than it looks. You can use it as a closure, but you can also call methods on it to do more powerful operations. + +For instance, we can get a handle to the current value at any time: + +```rust +let current: Rc = set_count.current(); +``` + +Or, we can get the inner handle to set the value + +```rust +let setter: Rc = set_count.setter(); +``` + +Or, we can set a new value using the current value as a reference: + +```rust +set_count.modify(|i| i + 1); +``` + +If the value is cheaply cloneable, then we can call `with_mut` to get a mutable reference to the value: + +```rust +set_count.with_mut(|i| *i += 1); +``` + +Alternatively, we can call `make_mut` which gives us a `RefMut` to the underlying value: + +```rust +*set_count.make_mut() += 1; +``` + +Plus, the `set_count` handle is cloneable into async contexts, so we can use it in futures. + +```rust +// count up infinitely +cx.spawn({ + to_owned![set_count]; + async move { + loop { + wait_ms(100).await; + set_count.modify(|i| i + 1); + } + } +}) +``` ## `use_ref` +You might've noticed a fundamental limitation to `use_state`: to modify the value in-place, it must be cheaply cloneable. But what if your type is not cheap to clone? + +In these cases, you should reach for `use_ref` which is essentially just a glorified `Rc>` (typical Rust UI shenanigans). + +This provides us some runtime locks around our data, trading reliability for performance. For most cases though, you will find it hard to make `use_ref` crash. + +> Note: this is *not* exactly like its React counterpart since calling `write` will cause a re-render. For more React parity, use the `write_silent` method. + +To use the hook: + +```rust +let names = use_ref(&cx, || vec!["susie", "calvin"]); +``` + +To read the hook values, use the `read()` method: + +```rust +cx.render(rsx!{ + ul { + names.read().iter().map(|name| { + rsx!{ "{name}" } + }) + } +}) +``` + +And then to write into the hook value, use the `write` method: + +```rust +names.write().push("Tiger"); +``` + +If you don't want to re-render the component when names is updated, then we can use the `write_silent` method: + +```rust +names.write().push("Transmogrifier"); +``` + +Again, like `UseState`, the `UseRef` handle is clonable into async contexts: + + +```rust +// infinitely push calvin into the list +cx.spawn({ + to_owned![names]; + async move { + loop { + wait_ms(100).await; + names.write().push("Calvin"); + } + } +}) +``` + + +## Wrapping up + +These two hooks are extremely powerful at storing state. From a21e7d4dd168129da06f535f9dc4b1de724617cb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 12:00:13 -0500 Subject: [PATCH 023/256] fix: use_route should subscribe to changes to the route --- packages/router/src/hooks/use_route.rs | 32 ++++++++++++++++++++------ packages/router/src/service.rs | 14 ++++++++++- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 97dd6c048..ab6fad268 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,4 +1,4 @@ -use dioxus_core::ScopeState; +use dioxus_core::{ScopeId, ScopeState}; use gloo::history::{HistoryResult, Location}; use serde::de::DeserializeOwned; use std::{rc::Rc, str::FromStr}; @@ -74,10 +74,28 @@ impl UseRoute { /// This hook provides access to information about the current location in the /// context of a [`Router`]. If this function is called outside of a `Router` /// component it will panic. -pub fn use_route(cx: &ScopeState) -> UseRoute { - let router = cx - .consume_context::() - .expect("Cannot call use_route outside the scope of a Router component") - .clone(); - UseRoute { router } +pub fn use_route(cx: &ScopeState) -> &UseRoute { + &cx.use_hook(|_| { + let router = cx + .consume_context::() + .expect("Cannot call use_route outside the scope of a Router component"); + + router.subscribe_onchange(cx.scope_id()); + + UseRouteInner { + router: UseRoute { router }, + scope: cx.scope_id(), + } + }) + .router +} + +struct UseRouteInner { + router: UseRoute, + scope: ScopeId, +} +impl Drop for UseRouteInner { + fn drop(&mut self) { + self.router.router.unsubscribe_onchange(self.scope) + } } diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 9da2bef7e..f2682f1f0 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,7 +1,7 @@ use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ cell::{Cell, Ref, RefCell}, - collections::HashMap, + collections::{HashMap, HashSet}, rc::Rc, }; @@ -12,6 +12,7 @@ pub struct RouterService { pub(crate) pending_events: Rc>>, history: Rc>, slots: Rc>>, + onchange_listeners: Rc>>, root_found: Rc>>, cur_path_params: Rc>>, listener: HistoryListener, @@ -73,6 +74,7 @@ impl RouterService { regen_route, slots, pending_events, + onchange_listeners: Rc::new(RefCell::new(HashSet::new())), cur_path_params: Rc::new(RefCell::new(HashMap::new())), } } @@ -143,6 +145,16 @@ impl RouterService { pub fn current_path_params(&self) -> Ref> { self.cur_path_params.borrow() } + + pub fn subscribe_onchange(&self, id: ScopeId) { + log::trace!("Subscribing onchange for scope id {:?}", id); + self.onchange_listeners.borrow_mut().insert(id); + } + + pub fn unsubscribe_onchange(&self, id: ScopeId) { + log::trace!("Subscribing onchange for scope id {:?}", id); + self.onchange_listeners.borrow_mut().remove(&id); + } } fn clean_route(route: String) -> String { From 5ee9d6c4348a2f51adac827f715fb138918d1dc6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 12:04:16 -0500 Subject: [PATCH 024/256] fix: attach router listener to subscriber list --- packages/router/src/service.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index f2682f1f0..2378c5aee 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -43,6 +43,7 @@ impl RouterService { let location = history.location(); let path = location.path(); + let onchange_listeners = Rc::new(RefCell::new(HashSet::new())); let slots: Rc>> = Default::default(); let pending_events: Rc>> = Default::default(); let root_found = Rc::new(Cell::new(None)); @@ -52,6 +53,7 @@ impl RouterService { let regen_route = regen_route.clone(); let root_found = root_found.clone(); let slots = slots.clone(); + let onchange_listeners = onchange_listeners.clone(); move || { root_found.set(None); // checking if the route is valid is cheap, so we do it @@ -60,6 +62,13 @@ impl RouterService { regen_route(*slot); } + for listener in onchange_listeners.borrow_mut().iter() { + log::trace!("regenerating listener {:?}", listener); + regen_route(*listener); + } + + + // also regenerate the root regen_route(root_scope); @@ -74,7 +83,7 @@ impl RouterService { regen_route, slots, pending_events, - onchange_listeners: Rc::new(RefCell::new(HashSet::new())), + onchange_listeners, cur_path_params: Rc::new(RefCell::new(HashMap::new())), } } From 9da46eb7bc207997ca7779c58fcb2a9645dfa9d0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 12:07:10 -0500 Subject: [PATCH 025/256] chore: rustfmt --- packages/router/src/service.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 2378c5aee..b62bc78d5 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -67,8 +67,6 @@ impl RouterService { regen_route(*listener); } - - // also regenerate the root regen_route(root_scope); From 79e09934aa685d03d6e0b323723bc1cd537d74d9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 12:14:40 -0500 Subject: [PATCH 026/256] chore: add docs to router UseRouteListener --- packages/router/src/hooks/use_route.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index ab6fad268..3b609f5e2 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -8,6 +8,7 @@ use crate::RouterService; /// This struct provides is a wrapper around the internal router /// implementation, with methods for getting information about the current /// route. +#[derive(Clone)] pub struct UseRoute { router: Rc, } @@ -82,7 +83,7 @@ pub fn use_route(cx: &ScopeState) -> &UseRoute { router.subscribe_onchange(cx.scope_id()); - UseRouteInner { + UseRouteListener { router: UseRoute { router }, scope: cx.scope_id(), } @@ -90,11 +91,15 @@ pub fn use_route(cx: &ScopeState) -> &UseRoute { .router } -struct UseRouteInner { +// The entire purpose of this struct is to unubscribe this component when it is unmounted. +// The UseRoute can be cloned into async contexts, so we can't rely on its drop to unubscribe. +// Instead, we hide the drop implementation on this private type exclusive to the hook, +// and reveal our cached version of UseRoute to the component. +struct UseRouteListener { router: UseRoute, scope: ScopeId, } -impl Drop for UseRouteInner { +impl Drop for UseRouteListener { fn drop(&mut self) { self.router.router.unsubscribe_onchange(self.scope) } From f26accd685457ae880b97873f86c90e62640655a Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Sat, 29 Jan 2022 01:46:00 +0800 Subject: [PATCH 027/256] Delete core_problem.md --- .github/ISSUE_TEMPLATE/core_problem.md | 28 -------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/core_problem.md diff --git a/.github/ISSUE_TEMPLATE/core_problem.md b/.github/ISSUE_TEMPLATE/core_problem.md deleted file mode 100644 index 0e7cb3390..000000000 --- a/.github/ISSUE_TEMPLATE/core_problem.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Core Development Issue -about: Something that doesn't directly relate to the platform ---- - -## Enviroment - -> development enviroment info. - -- dioxus version: -- dioxus feature: ["web", "desktop", ..."] -- rust version: - -## Error Info - -> verbose error information. - -``` - -``` - -## Bad Code - -> the code where error occurred. - -```rust - -``` \ No newline at end of file From d00a371e1022bdf2cf8057a5fc112090dc2307d5 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Sat, 29 Jan 2022 01:47:01 +0800 Subject: [PATCH 028/256] Delete desktop.md --- .github/ISSUE_TEMPLATE/desktop.md | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/desktop.md diff --git a/.github/ISSUE_TEMPLATE/desktop.md b/.github/ISSUE_TEMPLATE/desktop.md deleted file mode 100644 index cbc26136e..000000000 --- a/.github/ISSUE_TEMPLATE/desktop.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Desktop Platform Issue -about: The Problem with desktop platform ---- - -## Enviroment - -> development enviroment info. - -- system version: -- dioxus version: -- rust version: - -## Error Info - -> verbose error information. - -``` - -``` - -## Bad Code - -> the code where error occurred. - -```rust - -``` \ No newline at end of file From 265ecd5c8ee1eac5241f13c0e4231c4c4a662f4b Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Sat, 29 Jan 2022 01:47:07 +0800 Subject: [PATCH 029/256] Delete feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index f8735b9a2..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Feature Request -about: If you have any interesting advice, you can tell us. ---- - -## Specific Demand - - - -## Implement Suggestion - - \ No newline at end of file From 003f8be7a25e1f21c30eb5375915e2dc68e935ad Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Sat, 29 Jan 2022 02:14:22 +0800 Subject: [PATCH 030/256] feat: add `issue` template --- .github/ISSUE_TEMPLATE/bug_report.md | 36 ++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_requst.md | 16 +++++++++++ .github/ISSUE_TEMPLATE/web.md | 27 ------------------ 3 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_requst.md delete mode 100644 .github/ISSUE_TEMPLATE/web.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..e31219bb6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve Dioxus +--- + +**Problem** + + + +**Steps To Reproduce** + +Steps to reproduce the behavior: + +- +- +- + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots to help explain your problem. + +**Environment:** + - Dioxus version: [e.g. v0.17, `master`] + - Rust version: [e.g. 1.43.0, `nightly`] + - OS info: [e.g. MacOS] + - App platform: [e.g. `web`, `desktop`] + +**Questionnaire** + +- [ ] I'm interested in fixing this myself but don't know where to start +- [ ] I would like to fix and I have a solution +- [ ] I don't have time to fix this right now, but maybe later \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_requst.md b/.github/ISSUE_TEMPLATE/feature_requst.md new file mode 100644 index 000000000..f8735b9a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_requst.md @@ -0,0 +1,16 @@ +--- +name: Feature Request +about: If you have any interesting advice, you can tell us. +--- + +## Specific Demand + + + +## Implement Suggestion + + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/web.md b/.github/ISSUE_TEMPLATE/web.md deleted file mode 100644 index f92ec0b99..000000000 --- a/.github/ISSUE_TEMPLATE/web.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Web(WASM) Platform Issue -about: The Problem with Web | WASM platform ---- - -## Enviroment - -> development enviroment info. - -- dioxus version: -- rust version: - -## Error Info - -> verbose error information. - -``` - -``` - -## Bad Code - -> the code where error occurred. - -```rust - -``` \ No newline at end of file From e24957fc191476184816fe5dee249f691170d4ae Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 14:38:17 -0500 Subject: [PATCH 031/256] feat: enable use_router --- packages/router/src/hooks/use_route.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 3b609f5e2..31a157a8f 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -104,3 +104,11 @@ impl Drop for UseRouteListener { self.router.router.unsubscribe_onchange(self.scope) } } + +/// This hook provides access to the `RouterService` for the app. +pub fn use_router(cx: &ScopeState) -> &RouterService { + cx.use_hook(|_| { + cx.consume_context::() + .expect("Cannot call use_route outside the scope of a Router component") + }) +} From 3d75f1cb054ec051846fb5b72dd7b2e55ef3e242 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 16:11:04 -0500 Subject: [PATCH 032/256] docs: fix some typos --- .vscode/spellright.dict | 3 +++ docs/guide/.vscode/spellright.dict | 3 --- docs/guide/src/SUMMARY.md | 5 +++-- docs/guide/src/elements/composing.md | 18 +++++++++--------- docs/guide/src/interactivity/index.md | 13 +++++-------- 5 files changed, 20 insertions(+), 22 deletions(-) delete mode 100644 docs/guide/.vscode/spellright.dict diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 7a5a261d7..06845ebf9 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -76,3 +76,6 @@ iter cloneable fudamental clonable +oninput +Webview +idanarye diff --git a/docs/guide/.vscode/spellright.dict b/docs/guide/.vscode/spellright.dict deleted file mode 100644 index 572d1f65c..000000000 --- a/docs/guide/.vscode/spellright.dict +++ /dev/null @@ -1,3 +0,0 @@ -oninput -Webview -idanarye diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 5a8fadb96..d4990fc99 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -13,11 +13,11 @@ - [Properties](elements/propsmacro.md) - [Reusing, Importing, and Exporting](elements/exporting_components.md) - [Children and Attributes](elements/component_children.md) - - [Theory of Reactive Programming](elements/composing.md) + - [How Data Flows](elements/composing.md) - [Adding Interactivity](interactivity/index.md) - [Hooks and Internal State](interactivity/hooks.md) + - [UseState and UseRef](interactivity/importanthooks.md) - [Event Listeners](interactivity/event_handlers.md) - - [UseState and UseRef](interactivity/importanthooks.md) - [User Input and Controlled Components](interactivity/user_input.md) - [Lifecycle, updates, and effects](interactivity/lifecycles.md) - [Managing State](state/index.md) @@ -45,3 +45,4 @@ + diff --git a/docs/guide/src/elements/composing.md b/docs/guide/src/elements/composing.md index e6ccf189e..dcd54b18f 100644 --- a/docs/guide/src/elements/composing.md +++ b/docs/guide/src/elements/composing.md @@ -2,7 +2,7 @@ We've finally reached the point in our tutorial where we can talk about the "Theory of React." We've talked about defining a declarative view, but not about the aspects that make our code *reactive*. -Understanding the theory of reactive program is essential to making sense of Dioxus and writing effective, performant UIs. +Understanding the theory of reactive programming is essential to making sense of Dioxus and writing effective, performant UIs. In this section, we'll talk about: @@ -17,15 +17,15 @@ This section is a bit long, but worth the read. We recommend coffee, tea, and/or Dioxus is one the very few Rust libraries that provide a "Reactive Programming Model". The term "Reactive programming" is a classification of programming paradigm - much like functional or imperative programming. This is a very important distinction since it affects how we *think* about our code. -Reactive programming is programming model concerned with deriving computations from asynchronous data flow. Most reactive programs are comprised of a handful of datasources, intermediate computations, and a final result. +Reactive programming is a programming model concerned with deriving computations from asynchronous data flow. Most reactive programs are comprised of datasources, intermediate computations, and a final result. -We can consider the our rendered GUI to be the final result of reactive app and our datasources to include shared contexts and component properties. +We consider the rendered GUI to be the final result of our Dioxus apps. The datasources for our apps include local and global state. For example, the model presented in the figure below is comprised of two data sources: time and a constant. These values are passed through our computation graph to achieve a final result: `g`. ![Reactive Model](https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Reactive_programming_glitches.svg/440px-Reactive_programming_glitches.svg.png) -Whenever our `seconds` variable changes, then we will reevaluate the computation for `t`. Because `g` relies on `t`, then we will also reevaluate its computation too. Notice that we would've reevaluated the computation for `g` even if `t` didn't change because `seconds` is used to calculate `g`. +Whenever our `seconds` variable changes, we will then reevaluate the computation for `t`. Because `g` relies on `t`, we will also reevaluate its computation too. Notice that we would've reevaluated the computation for `g` even if `t` didn't change because `seconds` is used to calculate `g`. However, if we somehow changed our constant from `1` to `2`, then we need to reevaluate `t`. If, for whatever reason, this change did not affect the result of `t`, then we wouldn't try to reevaluate `g`. @@ -51,7 +51,7 @@ fn compute_graph(constant: i32, seconds: i32) -> bool { ## How is Dioxus Reactive? -The Dioxus VirtualDom provides us a framework for reactive programming. When we build apps with dioxus, we need to feed it our own custom datasources. This can be either initial props or some values fetched from the network. We then pass this data through our app into components through properties. +The Dioxus VirtualDom provides us a framework for reactive programming. When we build apps with dioxus, we need to provide our own datasources. This can be either initial props or some values fetched from the network. We then pass this data through our app into components through properties. If we represented the reactive graph presented above in Dioxus, it would look very similar: @@ -85,7 +85,7 @@ With this app, we've defined three components. Our top-level component provides Now, whenever the `constant` changes, our `RenderT` component will be re-rendered. However, if `seconds` doesn't change, then we don't need to re-render `RenderG` because the input is the same. If `seconds` *does* change, then both RenderG and RenderT will be reevaluated. -Dioxus is "Reactive" because it provides this framework for us. All we need to do is write our own tiny units of computations and Dioxus figures out which components need to be reevaluated automatically. +Dioxus is "Reactive" because it provides this framework for us. All we need to do is write our own tiny units of computation and Dioxus figures out which components need to be reevaluated automatically. These extra checks and algorithms add some overhead, which is why you see projects like [Sycamore](http://sycamore-rs.netlify.app) and [SolidJS](http://solidjs.com) eliminating them altogether. Dioxus is *really* fast, so we're willing to exchange the added overhead for improved developer experience. @@ -142,11 +142,11 @@ A single component will be called multiple times, modifying its own internal sta With the `provide_context` and `consume_context` methods on `Scope`, we can share values to descendants without having to pass values through component props. This has the side-effect of making our datasources less obvious from a high-level perspective, but it makes our components more modular within the same codebase. -To make app-global state easier to reason about, Dioxus makes all values provided through `provide_context` immutable. This means any library built on top of `provide_context` needs to use interior mutability to modify share global state. +To make app-global state easier to reason about, Dioxus makes all values provided through `provide_context` immutable. This means any library built on top of `provide_context` needs to use interior mutability to modify shared global state. In these cases, App-Global state needs to manually track which components need to be re-generated. -To regenerate *any* component in your app, you can get a handle the Dioxus' internal scheduler through `schedule_update_any`: +To regenerate *any* component in your app, you can get a handle to the Dioxus' internal scheduler through `schedule_update_any`: ```rust let force_render = cx.schedule_update_any(); @@ -155,7 +155,7 @@ let force_render = cx.schedule_update_any(); force_render(ScopeId(0)); ``` -## What does it mean for a component to "re-render" +## What does it mean for a component to "re-render"? In our guides, we frequently use the phrase "re-render" to describe updates to our app. You'll often hear this paired with "preventing unnecessary re-renders." But what exactly does this mean? diff --git a/docs/guide/src/interactivity/index.md b/docs/guide/src/interactivity/index.md index b71a3e73d..a1f963bd3 100644 --- a/docs/guide/src/interactivity/index.md +++ b/docs/guide/src/interactivity/index.md @@ -112,11 +112,11 @@ For example, let's say we provide a button to generate a new post. Whenever the ```rust fn App(cx: Scope)-> Element { - let post = use_state(&cx, || PostData::new()); + let (post, set_post) = use_state(&cx, || PostData::new()); cx.render(rsx!{ button { - on_click: move |_| post.set(PostData::random()) + on_click: move |_| set_post(PostData::random()) "Generate a random post" } Post { props: &post } @@ -131,7 +131,6 @@ We'll dive much deeper into event listeners later. We can also update our state outside of event listeners with `futures` and `coroutines`. - `Futures` are Rust's version of promises that can execute asynchronous work by an efficient polling system. We can submit new futures to Dioxus either through `push_future` which returns a `TaskId` or with `spawn`. -- - `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 and Futures 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. @@ -142,14 +141,14 @@ We can use tasks in our components to build a tiny stopwatch that ticks every se ```rust fn App(cx: Scope)-> Element { - let mut sec_elapsed = use_state(&cx, || 0); + let (elapsed, set_elapsed) = use_state(&cx, || 0); use_future(&cx, || { - to_owned![sec_elapsed]; // explicitly capture this hook for use in async + to_owned![set_elapsed]; // explicitly capture this hook for use in async async move { loop { TimeoutFuture::from_ms(1000).await; - sec_elapsed += 1; + set_elapsed.modify(|i| i + 1) } } }); @@ -162,8 +161,6 @@ Using asynchronous code can be difficult! This is just scratching the surface of ### How do I tell Dioxus that my state changed? -So far, we've only updated our state with `.set`. However, you might've noticed that we used `AddAssign` to increment the `sec_elapsed` value in our stopwatch example *without* calling set. This is because the `AddAssign` trait is implemented for `UseState` (the wrapper around our value returned from `use_state`). Under the hood, whenever you try to mutate our value through `UseState`, you're actually calling `.set` which informs Dioxus that _this_ component needs to be updated on the screen. - Whenever you inform Dioxus that the component needs to be updated, it will "render" your component again, storing the previous and current Elements in memory. Dioxus will automatically figure out the differences between the old and the new and generate a list of edits that the renderer needs to apply to change what's on the screen. This process is called "diffing": ![Diffing](../images/diffing.png) From c092bd43edf1891c427722bc9ca04e11c9359069 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 16:12:06 -0500 Subject: [PATCH 033/256] fix: use_state --- packages/hooks/src/usestate.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index eb3900981..914587c13 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -61,6 +61,8 @@ pub fn use_state<'a, T: 'static>( } }); + hook.current_val = hook.slot.borrow().clone(); + (hook.current_val.as_ref(), hook) } From 72c6bb3d0b7253f084f7e3bcf55d458cb4adeedb Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 28 Jan 2022 15:31:43 -0600 Subject: [PATCH 034/256] Make log message in Link component trace level, not debug All the other routing-related messages are at the trace level. Leaving this at debug was an oversight on my part. --- packages/router/src/components/link.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 4c0ecb996..fd8eea66d 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -41,7 +41,7 @@ pub struct LinkProps<'a> { } pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - log::debug!("render Link to {}", cx.props.to); + log::trace!("render Link to {}", cx.props.to); if let Some(service) = cx.consume_context::() { return cx.render(rsx! { a { From e9792e9b95521fcf0b0b4e240d7cf0397cd79b8f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 21:43:01 -0500 Subject: [PATCH 035/256] tests: add miri stress test --- packages/core/tests/miri_stress.rs | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index bb4963b71..6b7805261 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -313,3 +313,42 @@ fn leak_thru_children() { dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); dom.work_with_deadline(|| false); } + +#[test] +fn test_pass_thru() { + #[inline_props] + fn Router<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { + &cx.props.children + }) + } + + fn MemoizedThing(cx: Scope) -> Element { + rsx!(cx, div { "memoized" }) + } + + fn app(cx: Scope) -> Element { + let thing = cx.use_hook(|_| "asd"); + rsx!(cx, + Router { + MemoizedThing { + } + } + ) + } + + let mut dom = new_dom(app, ()); + let _ = dom.rebuild(); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); +} From 05d68b1107677ad49b4e9aba1ef094b64c8d6262 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 21:45:40 -0500 Subject: [PATCH 036/256] ci: fewer windows targets this commit decreases the number of windows targets we check to speed up CI --- .github/workflows/windows.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 183fb282d..b776be433 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -19,13 +19,7 @@ jobs: max-parallel: 2 fail-fast: false matrix: - target: - [ - i686-pc-windows-gnu, - i686-pc-windows-msvc, - x86_64-pc-windows-gnu, - x86_64-pc-windows-msvc, - ] + target: [x86_64-pc-windows-gnu, x86_64-pc-windows-msvc] cfg_release_channel: [stable] steps: From 66bc09cf2837d9efe794f13d5ba6118b91b2d8d6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 28 Jan 2022 21:46:15 -0500 Subject: [PATCH 037/256] ci: remove i686 windows target --- .github/workflows/windows.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b776be433..2b6ea5c23 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -41,12 +41,6 @@ jobs: rustup target add ${{ matrix.target }} shell: powershell - - name: Add mingw32 to path for i686-gnu - run: | - echo "C:\msys64\mingw32\bin" >> $GITHUB_PATH - if: matrix.target == 'i686-pc-windows-gnu' && matrix.channel == 'nightly' - shell: bash - - name: Add mingw64 to path for x86_64-gnu run: echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly' From 8b402f946afa163f1a1bdc17e38959a0e7131b8e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:15:23 -0500 Subject: [PATCH 038/256] docs: updaet hooks --- docs/guide/src/SUMMARY.md | 5 +++++ docs/guide/src/interactivity/importanthooks.md | 2 ++ 2 files changed, 7 insertions(+) diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index d4990fc99..5023eab8e 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -14,6 +14,11 @@ - [Reusing, Importing, and Exporting](elements/exporting_components.md) - [Children and Attributes](elements/component_children.md) - [How Data Flows](elements/composing.md) +- [Thinking Reactively]() + - [Thinking Reactively]() + - [Thinking Reactively]() + - [Thinking Reactively]() + - [Thinking Reactively]() - [Adding Interactivity](interactivity/index.md) - [Hooks and Internal State](interactivity/hooks.md) - [UseState and UseRef](interactivity/importanthooks.md) diff --git a/docs/guide/src/interactivity/importanthooks.md b/docs/guide/src/interactivity/importanthooks.md index eff27b199..fb985ce90 100644 --- a/docs/guide/src/interactivity/importanthooks.md +++ b/docs/guide/src/interactivity/importanthooks.md @@ -7,6 +7,8 @@ Most components you will write in Dioxus will need to store state somehow. For l Both of these hooks are extremely powerful and flexible, so we've dedicated this section to understanding them properly. +> These two hooks are not the only way to store state. You can always build your own hooks! + ## Note on Hooks If you're struggling with errors due to usage in hooks, make sure you're following the rules of hooks: From 56f3002aede54807c8f385a8e1eba88386946897 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:17:14 -0500 Subject: [PATCH 039/256] feat: add changelogs --- CHANGELOG.md | 20 +++++++ packages/core-macro/CHANGELOG.md | 40 +++++++++++++- packages/core/CHANGELOG.md | 57 ++++++++++++++++++- packages/desktop/CHANGELOG.md | 94 ++++++++++++++++++++++++++++++-- packages/hooks/CHANGELOG.md | 36 +++++++++++- packages/html/CHANGELOG.md | 35 +++++++++++- packages/mobile/CHANGELOG.md | 23 +++++++- packages/router/CHANGELOG.md | 58 +++++++++++++++++++- packages/ssr/CHANGELOG.md | 31 ++++++++++- packages/web/CHANGELOG.md | 60 +++++++++++++++++++- 10 files changed, 437 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b624794..b056a9590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Commit Statistics + + + + - 1 commit contributed to the release over the course of 7 calendar days. + - 0 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +

view details + + * **Uncategorized** + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) +
+ ## v0.1.7 (2022-01-08) ### Bug Fixes diff --git a/packages/core-macro/CHANGELOG.md b/packages/core-macro/CHANGELOG.md index 79cbe766f..cf1143e1e 100644 --- a/packages/core-macro/CHANGELOG.md +++ b/packages/core-macro/CHANGELOG.md @@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### New Features + + - flatten props attrs + - add "optional" flag for props + +### Bug Fixes + + - detection of f-string formatting in components + +### Commit Statistics + + + + - 9 commits contributed to the release over the course of 12 calendar days. + - 3 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #95 from DioxusLabs/jk/filedragindrop ([`ca0dd4a`](https://github.comgit//DioxusLabs/dioxus/commit/ca0dd4aa7192d483a195d420363f39d771f3e471)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - flatten props attrs ([`d237271`](https://github.comgit//DioxusLabs/dioxus/commit/d2372717bd01fcff50af0572360e3f763d4c869d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - detection of f-string formatting in components ([`90abd9c`](https://github.comgit//DioxusLabs/dioxus/commit/90abd9c9a08c165384fae9c1f7c3fd098d512c48)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - Merge pull request #107 from autarch/autarch/half-assed-router ([`8d3ac3f`](https://github.comgit//DioxusLabs/dioxus/commit/8d3ac3ff148aef9d10a393eda453a11c1e882f58)) + - Merge pull request #127 from DioxusLabs/jk/handler-tweak ([`5bce294`](https://github.comgit//DioxusLabs/dioxus/commit/5bce294a86941090660b1cfd87809a2b0b76d2ce)) + - add "optional" flag for props ([`47bc4e4`](https://github.comgit//DioxusLabs/dioxus/commit/47bc4e4a44a7d08d4f42102d13f0766d9d6bf358)) +
+ ## v0.1.6 (2022-01-08) ### Documentation @@ -64,7 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 138 commits contributed to the release over the course of 352 calendar days. + - 139 commits contributed to the release over the course of 352 calendar days. - 123 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -75,6 +112,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-core-macro v0.1.6 ([`db0a5bd`](https://github.comgit//DioxusLabs/dioxus/commit/db0a5bd6ec93803cddb3c6fda4172257b9c301c6)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index a0aa9f28d..563106db3 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -5,6 +5,58 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Documentation + + - add comments for the Handler + +### New Features + + - add "spawn" method + - allow providing context to the root component + +### Bug Fixes + + - provide_root_context on root scopes + - proprogation of root context + - allow eventhandler to derive default + +### Commit Statistics + + + + - 18 commits contributed to the release over the course of 13 calendar days. + - 8 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a)) + - Merge branch 'master' of github.com:DioxusLabs/dioxus ([`8899701`](https://github.comgit//DioxusLabs/dioxus/commit/88997019c5c74658bfd76344d6bb6f3565943b41)) + - add miri stress test ([`e9792e9`](https://github.comgit//DioxusLabs/dioxus/commit/e9792e9b95521fcf0b0b4e240d7cf0397cd79b8f)) + - add "spawn" method ([`bad4b77`](https://github.comgit//DioxusLabs/dioxus/commit/bad4b773b7bbab6a38d4f48d88d8c5a8787927ac)) + - drop hooks before resetting bump arena ([`2e4f765`](https://github.comgit//DioxusLabs/dioxus/commit/2e4f7659327b69b74e20af59b6d5e65c699b6adb)) + - provide_root_context on root scopes ([`d9a07dd`](https://github.comgit//DioxusLabs/dioxus/commit/d9a07ddddb45ba00e35645ac0e0ad1a4d2dd996d)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #121 from DioxusLabs/jk/unify ([`b287a4c`](https://github.comgit//DioxusLabs/dioxus/commit/b287a4cab3eec9e6961a3f010846291f4f105747)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - proprogation of root context ([`c8d528b`](https://github.comgit//DioxusLabs/dioxus/commit/c8d528b3b18e320ca0cee1542ae5fc659c1cca81)) + - allow providing context to the root component ([`d2bd175`](https://github.comgit//DioxusLabs/dioxus/commit/d2bd1751436ef4bec554bb361ba87aea1357036a)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - Merge pull request #107 from autarch/autarch/half-assed-router ([`8d3ac3f`](https://github.comgit//DioxusLabs/dioxus/commit/8d3ac3ff148aef9d10a393eda453a11c1e882f58)) + - Merge pull request #138 from mrxiaozhuox/master ([`8c7473d`](https://github.comgit//DioxusLabs/dioxus/commit/8c7473d1943dd133f388ec36116c9d8295861b97)) + - Merge pull request #133 from mrxiaozhuox/master ([`887f69d`](https://github.comgit//DioxusLabs/dioxus/commit/887f69d5b47bdcde4fe0eab094c0cd0de23e4f3f)) + - Don't expect all components to have a scope in ScopeArena.ensure_drop_safety ([`9b282d8`](https://github.comgit//DioxusLabs/dioxus/commit/9b282d877ba0fe2463687cde7ea899b7f8510425)) + - add comments for the Handler ([`036a0ff`](https://github.comgit//DioxusLabs/dioxus/commit/036a0ff49a7dade0e04c9c07071a1ff49133ee24)) + - allow eventhandler to derive default ([`e47ead5`](https://github.comgit//DioxusLabs/dioxus/commit/e47ead5347ce778935f8f2127fcb14f520eda0ce)) +
+ ## v0.1.7 (2022-01-08) ### Documentation @@ -138,7 +190,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 437 commits contributed to the release over the course of 358 calendar days. + - 438 commits contributed to the release over the course of 358 calendar days. - 416 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -149,8 +201,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** - - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - Release dioxus-core v0.1.7 ([`16d73b2`](https://github.comgit//DioxusLabs/dioxus/commit/16d73b240fc446c6f6996c19ccf52bbcda2eca79)) - add title to doc comment ([`d11f322`](https://github.comgit//DioxusLabs/dioxus/commit/d11f322f554e7dbf43b988c9cfda56498cc49872)) + - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - better document the `EventHandler` type ([`be9f1a5`](https://github.comgit//DioxusLabs/dioxus/commit/be9f1a52ad2b04f101397ae34482ea7394df653b)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index 245d05623..65657ab37 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -5,6 +5,76 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Documentation + + - fix web doc example and use &mut for builders everywhere + +### New Features + + - default asset server + - remove dioxus id on non-event elements + - also hide placeholder node + - wire up both desktop and web + - bool attr white list + - setup a typescript build + +### Bug Fixes + + - custom protocol receiver type + - clippy + - wry pathing + - cursor jumping and use set instead of lsit + - format code + - check `href` null + - add exclusion list + - check `href` null + - prevent `submit` default + +### Commit Statistics + + + + - 26 commits contributed to the release over the course of 11 calendar days. + - 17 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - custom protocol receiver type ([`22308eb`](https://github.comgit//DioxusLabs/dioxus/commit/22308eb26a9ea48b14f5f5abb833aa90a4e3fc40)) + - default asset server ([`430cde7`](https://github.comgit//DioxusLabs/dioxus/commit/430cde7068d308f2783e33d278fd0c0efa659c1b)) + - fix web doc example and use &mut for builders everywhere ([`a239d2b`](https://github.comgit//DioxusLabs/dioxus/commit/a239d2ba6ac7f1f3d09de16c022ce8ca52cf0f63)) + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - clippy ([`6bc45b1`](https://github.comgit//DioxusLabs/dioxus/commit/6bc45b1c5064a4e2b04d452c52a8167ad179691e)) + - Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401)) + - wry pathing ([`bad3616`](https://github.comgit//DioxusLabs/dioxus/commit/bad36162af764291f5a031b6233d151f61d745a4)) + - remove dioxus id on non-event elements ([`95e93ed`](https://github.comgit//DioxusLabs/dioxus/commit/95e93ed0bcf6c69990f4cf3c6448b2bf5da96c36)) + - also hide placeholder node ([`eb13884`](https://github.comgit//DioxusLabs/dioxus/commit/eb138848ec7a8978f0ed7c717374684d2315dc03)) + - drag and drop support ([`9ae981a`](https://github.comgit//DioxusLabs/dioxus/commit/9ae981a1af4b5474ce16e27e070794d59128c12a)) + - feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9)) + - cursor jumping and use set instead of lsit ([`be614e6`](https://github.comgit//DioxusLabs/dioxus/commit/be614e6535e6e13e6ff93e9c6a171c1c002e6b01)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3)) + - Merge pull request #101 from alexkirsz/ci ([`29bf424`](https://github.comgit//DioxusLabs/dioxus/commit/29bf424b0976b95ff645bb128d0e758cf0186614)) + - Merge pull request #139 from DioxusLabs/jk/provide-context-any ([`70f2ef4`](https://github.comgit//DioxusLabs/dioxus/commit/70f2ef43db5b6737bd9bcbfc1aa21c834ce4b395)) + - Merge branch 'master' into jk/unify ([`824defa`](https://github.comgit//DioxusLabs/dioxus/commit/824defa2dbcc16d66588b3976699d89b65a8a068)) + - wire up both desktop and web ([`05331dd`](https://github.comgit//DioxusLabs/dioxus/commit/05331ddd8033f6997d4916179b62f4d62f832988)) + - format code ([`9256161`](https://github.comgit//DioxusLabs/dioxus/commit/92561612c727e73356d7d36e16af39aacf02a56d)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - bool attr white list ([`8f4aa84`](https://github.comgit//DioxusLabs/dioxus/commit/8f4aa84f1a4f2443b34d81ee42490564e168de53)) + - setup a typescript build ([`5bf6c96`](https://github.comgit//DioxusLabs/dioxus/commit/5bf6c96f9fed04de949403202bafcbeadb5d2030)) + - check `href` null ([`2073b40`](https://github.comgit//DioxusLabs/dioxus/commit/2073b400df55f0c6d8bed7371b2313be6c064e6e)) + - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) + - check `href` null ([`327f901`](https://github.comgit//DioxusLabs/dioxus/commit/327f9015481809d8e5b9e69f26202e8d66dd198e)) + - prevent `submit` default ([`8089023`](https://github.comgit//DioxusLabs/dioxus/commit/8089023a6c3a54957af9c9c05c9dee6088b059ef)) +
+ ## v0.1.5 (2022-01-08) ### Documentation @@ -37,6 +107,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - omg what a dumb mistake - bless up, no more segfaults - wire up event delegator for webview + - link open in browser + - move `rpc` to handler + - `open_browser` bool attribute + - link open in browser + - link open in browser ### Bug Fixes @@ -48,13 +123,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - tags - desktop and mobile - messed up how lifetimes worked, need to render once per component + - error pattern + - format code ### Commit Statistics - - 91 commits contributed to the release over the course of 151 calendar days. - - 78 commits where understood as [conventional](https://www.conventionalcommits.org). + - 98 commits contributed to the release over the course of 151 calendar days. + - 84 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages ### Commit Details @@ -64,11 +141,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** - - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) + - Release dioxus-desktop v0.1.5 ([`cd0dcac`](https://github.comgit//DioxusLabs/dioxus/commit/cd0dcacaf2862f26d29acb21d98f75d41b940e3f)) - handle bool attrs properly ([`8d685f4`](https://github.comgit//DioxusLabs/dioxus/commit/8d685f40b7e0ef6521c60310d8687291e9b9c48a)) + - link open in browser ([`46fd6ac`](https://github.comgit//DioxusLabs/dioxus/commit/46fd6ac3450ca5ebf9aecb2d59a5a92b2a68bdd0)) + - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) + - error pattern ([`62b637f`](https://github.comgit//DioxusLabs/dioxus/commit/62b637f8b0eaf616c49461fa23b9251a79abc147)) + - move `rpc` to handler ([`f006f50`](https://github.comgit//DioxusLabs/dioxus/commit/f006f50317f4b75fac353bc988db057a281ba7f8)) + - `open_browser` bool attribute ([`9e04ce5`](https://github.comgit//DioxusLabs/dioxus/commit/9e04ce5342850d2e0a01dde169807d6f6eb16566)) + - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - format code ([`5233ee9`](https://github.comgit//DioxusLabs/dioxus/commit/5233ee97d9314f7f0e0bdf05c56d2a9e4201a596)) + - link open in browser ([`c737c42`](https://github.comgit//DioxusLabs/dioxus/commit/c737c424b05ad8453e8770a14a0d210fb0c7c2fe)) - Merge pull request #89 from DioxusLabs/jk/simplify-example-run ([`8b6aa8b`](https://github.comgit//DioxusLabs/dioxus/commit/8b6aa8b880b6cb5c95e0c0743aad4e4e74388e05)) + - link open in browser ([`a0f6015`](https://github.comgit//DioxusLabs/dioxus/commit/a0f60152bc7e5866f114ed469809ce8be70d17d4)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) - Merge pull request #80 from DioxusLabs/jk/router2dotoh ([`cdc2d8e`](https://github.comgit//DioxusLabs/dioxus/commit/cdc2d8ec6d123245c2ea5f6d10af02b6a6833994)) - clear warnigns ([`175a6a1`](https://github.comgit//DioxusLabs/dioxus/commit/175a6a199c6738d8d0c7646ba0ec3fc4406c6535)) diff --git a/packages/hooks/CHANGELOG.md b/packages/hooks/CHANGELOG.md index 07e15ed64..253768068 100644 --- a/packages/hooks/CHANGELOG.md +++ b/packages/hooks/CHANGELOG.md @@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Bug Fixes + + - use_state + - exampels + +### Commit Statistics + + + + - 9 commits contributed to the release over the course of 21 calendar days. + - 3 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a)) + - Merge pull request #158 from DioxusLabs/jk/router-onchange ([`08988e1`](https://github.comgit//DioxusLabs/dioxus/commit/08988e1bfec35eadb30e50b5d677dbb36af91b9b)) + - use_state ([`c092bd4`](https://github.comgit//DioxusLabs/dioxus/commit/c092bd43edf1891c427722bc9ca04e11c9359069)) + - exampels ([`a8952a9`](https://github.comgit//DioxusLabs/dioxus/commit/a8952a9ee8d8831fa911cef4e7035293c3a9f88b)) + - Merge branch 'master' into jk/update-hooks ([`5c4bd08`](https://github.comgit//DioxusLabs/dioxus/commit/5c4bd0881bc10572440d9272ceb8ca774431c406)) + - modify usestate to be borrowed ([`58839f4`](https://github.comgit//DioxusLabs/dioxus/commit/58839f47bae3c49cadd4b41da9a5debebb1def99)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) +
+ ## v0.1.6 (2022-01-08) ### Documentation @@ -52,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 93 commits contributed to the release over the course of 358 calendar days. + - 94 commits contributed to the release over the course of 358 calendar days. - 82 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -63,6 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) diff --git a/packages/html/CHANGELOG.md b/packages/html/CHANGELOG.md index 9648c0c8f..48c7f622c 100644 --- a/packages/html/CHANGELOG.md +++ b/packages/html/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Bug Fixes + + - rustfmt + +### Commit Statistics + + + + - 7 commits contributed to the release over the course of 19 calendar days. + - 1 commit where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - rustfmt ([`43e78d5`](https://github.comgit//DioxusLabs/dioxus/commit/43e78d56f72e23ada939f5688a6bd2fdd9b8292d)) + - Add gap and row_gap to style_trait_methods ([`5f4a724`](https://github.comgit//DioxusLabs/dioxus/commit/5f4a72446e63441628e18bb19fd9b2d9f65f9d52)) + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401)) + - feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9)) + - feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3)) +
+ ## v0.1.4 (2022-01-08) ### Documentation @@ -42,7 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 60 commits contributed to the release over the course of 184 calendar days. + - 61 commits contributed to the release over the course of 184 calendar days. - 52 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -53,8 +83,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** - - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - enable prevent_default everywhere ([`9dff700`](https://github.comgit//DioxusLabs/dioxus/commit/9dff700c220dd9e0da2ee028900e82fd24f9d0dd)) + - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) - memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c)) - new versions of everything ([`4ea5c99`](https://github.comgit//DioxusLabs/dioxus/commit/4ea5c990d72b1645724ab0a88ffea2baf28e2835)) diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index 284068810..4486eb279 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Commit Statistics + + + + - 1 commit contributed to the release over the course of 7 calendar days. + - 0 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) +
+ ## v0.0.3 (2022-01-08) ### Documentation @@ -28,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 32 commits contributed to the release over the course of 151 calendar days. + - 33 commits contributed to the release over the course of 151 calendar days. - 21 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -39,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/router/CHANGELOG.md b/packages/router/CHANGELOG.md index 7e257f854..86b157737 100644 --- a/packages/router/CHANGELOG.md +++ b/packages/router/CHANGELOG.md @@ -5,6 +5,54 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### New Features + + - enable use_router + - connect an onchange listener + - flatten props attrs + +### Bug Fixes + + - attach router listener to subscriber list + - use_route should subscribe to changes to the route + +### Commit Statistics + + + + - 18 commits contributed to the release over the course of 14 calendar days. + - 7 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a)) + - enable use_router ([`e24957f`](https://github.comgit//DioxusLabs/dioxus/commit/e24957fc191476184816fe5dee249f691170d4ae)) + - add docs to router UseRouteListener ([`79e0993`](https://github.comgit//DioxusLabs/dioxus/commit/79e09934aa685d03d6e0b323723bc1cd537d74d9)) + - Merge pull request #166 from DioxusLabs/jk/default-assets-desktop ([`ccbb955`](https://github.comgit//DioxusLabs/dioxus/commit/ccbb955b7b24bd4e1c5aa40e813a2872ae474a69)) + - rustfmt ([`9da46eb`](https://github.comgit//DioxusLabs/dioxus/commit/9da46eb7bc207997ca7779c58fcb2a9645dfa9d0)) + - Make log message in Link component trace level, not debug ([`72c6bb3`](https://github.comgit//DioxusLabs/dioxus/commit/72c6bb3d0b7253f084f7e3bcf55d458cb4adeedb)) + - attach router listener to subscriber list ([`5ee9d6c`](https://github.comgit//DioxusLabs/dioxus/commit/5ee9d6c4348a2f51adac827f715fb138918d1dc6)) + - connect an onchange listener ([`29ed7eb`](https://github.comgit//DioxusLabs/dioxus/commit/29ed7ebece26e9d53925af55f2f34a8fd8241405)) + - use_route should subscribe to changes to the route ([`a21e7d4`](https://github.comgit//DioxusLabs/dioxus/commit/a21e7d4dd168129da06f535f9dc4b1de724617cb)) + - Merge pull request #95 from DioxusLabs/jk/filedragindrop ([`ca0dd4a`](https://github.comgit//DioxusLabs/dioxus/commit/ca0dd4aa7192d483a195d420363f39d771f3e471)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - flatten props attrs ([`d237271`](https://github.comgit//DioxusLabs/dioxus/commit/d2372717bd01fcff50af0572360e3f763d4c869d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - Merge pull request #138 from mrxiaozhuox/master ([`8c7473d`](https://github.comgit//DioxusLabs/dioxus/commit/8c7473d1943dd133f388ec36116c9d8295861b97)) + - Add a warning when Link it called outside of a Router context ([`6408058`](https://github.comgit//DioxusLabs/dioxus/commit/64080588d02c54a1d380116cbecdd17de16d2392)) + - Merge pull request #133 from mrxiaozhuox/master ([`887f69d`](https://github.comgit//DioxusLabs/dioxus/commit/887f69d5b47bdcde4fe0eab094c0cd0de23e4f3f)) + - Fix handling of re-renders in the Router ([`81c094e`](https://github.comgit//DioxusLabs/dioxus/commit/81c094ed29fcdc5c6099492dd6ab09a59b79252c)) +
+ ## v0.1.0 (2022-01-08) ### Documentation @@ -33,7 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 42 commits contributed to the release over the course of 352 calendar days. + - 50 commits contributed to the release over the course of 352 calendar days. - 35 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -44,8 +92,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) + - More WIP router implementation ([`e06eac1`](https://github.comgit//DioxusLabs/dioxus/commit/e06eac1ce5c6bb0a6680482574d82b16a141a626)) + - Implement UseRoute segment method ([`c9408da`](https://github.comgit//DioxusLabs/dioxus/commit/c9408da7310423b1676fab6d41635f9a8000d89e)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) + - Implement router matching for path parameters ([`f8a7e1c`](https://github.comgit//DioxusLabs/dioxus/commit/f8a7e1cd8255fd7d0116384247e0e305bb73bf3d)) + - Commit WIP on router ([`3c6142f`](https://github.comgit//DioxusLabs/dioxus/commit/3c6142fb9d8b5f715adf0fb30c2f3534b9ecd923)) + - Add more trace messages to the RouterService code ([`3a5b417`](https://github.comgit//DioxusLabs/dioxus/commit/3a5b417ad1639a31e9aac9f41b05d8b3f074b128)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - Fix typo in RouterService struct's "registered_routes" field name ([`d367e0f`](https://github.comgit//DioxusLabs/dioxus/commit/d367e0f89f1b31484bde42efaf10c3531aed1d64)) + - Add title prop to Link ([`e22ba5b`](https://github.comgit//DioxusLabs/dioxus/commit/e22ba5b1e52fe00e08e0b6ac0abae29b6068b8f0)) - add prevent default attribute and upgrade router ([`427b126`](https://github.comgit//DioxusLabs/dioxus/commit/427b126bc17336d5d14d56eb7fddb8e07752495f)) - memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c)) - bump all versions ([`4f92ba4`](https://github.comgit//DioxusLabs/dioxus/commit/4f92ba41602d706449c1bddabd49829873ee72eb)) diff --git a/packages/ssr/CHANGELOG.md b/packages/ssr/CHANGELOG.md index 73cee67b0..43e1960c1 100644 --- a/packages/ssr/CHANGELOG.md +++ b/packages/ssr/CHANGELOG.md @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Bug Fixes + + - ssr respects bool attrs + +### Commit Statistics + + + + - 5 commits contributed to the release over the course of 21 calendar days. + - 1 commit where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - ssr respects bool attrs ([`255f58a`](https://github.comgit//DioxusLabs/dioxus/commit/255f58af63e002339b02bf91c5b85cc5ec917428)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) +
+ ## v0.1.2 (2022-01-08) ### Documentation @@ -47,7 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 84 commits contributed to the release over the course of 357 calendar days. + - 85 commits contributed to the release over the course of 357 calendar days. - 75 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -58,6 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index b81747e95..3c27f3f94 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -5,6 +5,60 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Documentation + + - fix web doc example and use &mut for builders everywhere + +### New Features + + - add panic hook by default + - remove dioxus id on non-event elements + - wire up both desktop and web + - bool attr white list + +### Bug Fixes + + - webconfig should take &mut self + - format code + - ssr + hydration event listeners + - add exclusion list + +### Commit Statistics + + + + - 17 commits contributed to the release over the course of 21 calendar days. + - 10 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - webconfig should take &mut self ([`ae676d9`](https://github.comgit//DioxusLabs/dioxus/commit/ae676d9d816fcbf88d110157124443a5b3aef9eb)) + - fix web doc example and use &mut for builders everywhere ([`a239d2b`](https://github.comgit//DioxusLabs/dioxus/commit/a239d2ba6ac7f1f3d09de16c022ce8ca52cf0f63)) + - add panic hook by default ([`1406c90`](https://github.comgit//DioxusLabs/dioxus/commit/1406c9020bb3102635737dc2d13e5b3171499a18)) + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401)) + - remove dioxus id on non-event elements ([`95e93ed`](https://github.comgit//DioxusLabs/dioxus/commit/95e93ed0bcf6c69990f4cf3c6448b2bf5da96c36)) + - feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9)) + - feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3)) + - Merge pull request #101 from alexkirsz/ci ([`29bf424`](https://github.comgit//DioxusLabs/dioxus/commit/29bf424b0976b95ff645bb128d0e758cf0186614)) + - Merge pull request #139 from DioxusLabs/jk/provide-context-any ([`70f2ef4`](https://github.comgit//DioxusLabs/dioxus/commit/70f2ef43db5b6737bd9bcbfc1aa21c834ce4b395)) + - Merge branch 'master' into jk/unify ([`824defa`](https://github.comgit//DioxusLabs/dioxus/commit/824defa2dbcc16d66588b3976699d89b65a8a068)) + - wire up both desktop and web ([`05331dd`](https://github.comgit//DioxusLabs/dioxus/commit/05331ddd8033f6997d4916179b62f4d62f832988)) + - format code ([`9256161`](https://github.comgit//DioxusLabs/dioxus/commit/92561612c727e73356d7d36e16af39aacf02a56d)) + - ssr + hydration event listeners ([`c1a5d4e`](https://github.comgit//DioxusLabs/dioxus/commit/c1a5d4e11f5277e6ee644f670dc6fdd68f2202f1)) + - web now links against the js interprter code ([`10db6ad`](https://github.comgit//DioxusLabs/dioxus/commit/10db6ad65bd23dabf60d0afbe6ff82cda8220a8b)) + - bool attr white list ([`8f4aa84`](https://github.comgit//DioxusLabs/dioxus/commit/8f4aa84f1a4f2443b34d81ee42490564e168de53)) + - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) +
+ ## v0.0.4 (2022-01-08) ### Documentation @@ -84,7 +138,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 203 commits contributed to the release over the course of 358 calendar days. - - 190 commits where understood as [conventional](https://www.conventionalcommits.org). + - 189 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages ### Commit Details @@ -94,10 +148,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) + - handle bool attrs properly ([`8d685f4`](https://github.comgit//DioxusLabs/dioxus/commit/8d685f40b7e0ef6521c60310d8687291e9b9c48a)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) - - handle bool attrs properly ([`8d685f4`](https://github.comgit//DioxusLabs/dioxus/commit/8d685f40b7e0ef6521c60310d8687291e9b9c48a)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) - Merge pull request #80 from DioxusLabs/jk/router2dotoh ([`cdc2d8e`](https://github.comgit//DioxusLabs/dioxus/commit/cdc2d8ec6d123245c2ea5f6d10af02b6a6833994)) - memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c)) From 06723ad63fd21c5e50d666ff0809cd9ca07a7911 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:23:41 -0500 Subject: [PATCH 040/256] fix: add interpreter to desktop package so it publishes --- packages/desktop/src/interpreter.js | 548 ++++++++++++++++++++++++++++ packages/desktop/src/lib.rs | 2 +- 2 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 packages/desktop/src/interpreter.js diff --git a/packages/desktop/src/interpreter.js b/packages/desktop/src/interpreter.js new file mode 100644 index 000000000..61ba649c7 --- /dev/null +++ b/packages/desktop/src/interpreter.js @@ -0,0 +1,548 @@ +export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +export class Interpreter { + root; + stack; + listeners; + handlers; + lastNodeWasText; + nodes; + constructor(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + top() { + return this.stack[this.stack.length - 1]; + } + pop() { + return this.stack.pop(); + } + PushRoot(root) { + const node = this.nodes[root]; + this.stack.push(node); + } + AppendChildren(many) { + let root = this.stack[this.stack.length - (1 + many)]; + let to_add = this.stack.splice(this.stack.length - many); + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + } + ReplaceWith(root_id, m) { + let root = this.nodes[root_id]; + let els = this.stack.splice(this.stack.length - m); + root.replaceWith(...els); + } + InsertAfter(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + InsertBefore(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + Remove(root) { + let node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + } + CreateTextNode(text, root) { + // todo: make it so the types are okay + const node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + } + CreateElement(tag, root) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + this.nodes[root] = el; + this.stack.push(el); + } + CreateElementNs(tag, root, ns) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + CreatePlaceholder(root) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + NewEventListener(event_name, root, handler) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } else { + this.listeners[event_name]++; + } + } + RemoveEventListener(root, event_name) { + const element = this.nodes[root]; + element.removeAttribute(`data-dioxus-id`); + this.listeners[event_name]--; + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; + } + } + SetText(root, text) { + this.nodes[root].textContent = text; + } + SetAttribute(root, field, value, ns) { + const name = field; + const node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root, name) { + const node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + } + handleEdits(edits) { + this.stack.push(this.root); + for (let edit of edits) { + this.handleEdit(edit); + } + } + handleEdit(edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event) => { + let target = event.target; + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + const shouldPreventDefault = target.getAttribute( + `dioxus-prevent-default` + ); + let contents = serialize_event(event); + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + if (event.type == "submit") { + event.preventDefault(); + } + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if ( + href !== "" && + href !== null && + href !== undefined && + realId != null + ) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href, + }); + } + } + } + } + if (realId == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} +function serialize_event(event) { + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; + } + case "compositionend": + case "compositionstart": + case "compositionupdate": { + let { data } = event; + return { + data, + }; + } + case "keydown": + case "keypress": + case "keyup": { + let { + charCode, + key, + altKey, + ctrlKey, + metaKey, + keyCode, + shiftKey, + location, + repeat, + which, + } = event; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location, + repeat: repeat, + which: which, + locale: "locale", + }; + } + case "focus": + case "blur": { + return {}; + } + case "change": { + let target = event.target; + let value; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } else { + value = target.value ?? target.textContent; + } + return { + value: value, + }; + } + case "input": + case "invalid": + case "reset": + case "submit": { + let target = event.target; + let value = target.value ?? target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + const { + altKey, + button, + buttons, + clientX, + clientY, + ctrlKey, + metaKey, + pageX, + pageY, + screenX, + screenY, + shiftKey, + } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + const { + altKey, + button, + buttons, + clientX, + clientY, + ctrlKey, + metaKey, + pageX, + pageY, + screenX, + screenY, + shiftKey, + pointerId, + width, + height, + pressure, + tangentialPressure, + tiltX, + tiltY, + twist, + pointerType, + isPrimary, + } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary, + }; + } + case "select": { + return {}; + } + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + const { altKey, ctrlKey, metaKey, shiftKey } = event; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, + }; + } + case "scroll": { + return {}; + } + case "wheel": { + const { deltaX, deltaY, deltaZ, deltaMode } = event; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + const { animationName, elapsedTime, pseudoElement } = event; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + const { propertyName, elapsedTime, pseudoElement } = event; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": { + return {}; + } + case "toggle": { + return {}; + } + default: { + return {}; + } + } +} +const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, +}; diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 36b017077..f3e370230 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -220,7 +220,7 @@ pub fn launch_with_props( } else if trimmed == "index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") - .body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec()) + .body(include_bytes!("./interpreter.js").to_vec()) } else { // Read the file content from file path use std::fs::read; From 8056138f860d3f5a88f7ead580e1efb026263da1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:27:40 -0500 Subject: [PATCH 041/256] fix: explicit default for event handler rust derives default of T: Default, even when that's not a necessary requirement This fixes that --- packages/core/src/nodes.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index e30a07001..0efd64177 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -351,11 +351,18 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// } /// /// ``` -#[derive(Default)] pub struct EventHandler<'bump, T = ()> { pub callback: RefCell>>, } +impl<'a> Default for EventHandler<'a> { + fn default() -> Self { + Self { + callback: RefCell::new(None), + } + } +} + impl EventHandler<'_, T> { /// Call this event handler with the appropriate event type pub fn call(&self, event: T) { From 1b2a0053ef8aedf71cc4adad8c99f4db00bd8743 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:35:54 -0500 Subject: [PATCH 042/256] Release dioxus-core-macro v0.1.7 --- Cargo.toml | 2 +- packages/core-macro/Cargo.toml | 2 +- packages/desktop/Cargo.toml | 2 +- packages/router/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 884513a5b..105ce5050 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "./packages/core", version = "^0.1.7" } dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } -dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.6", optional = true } +dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index ee3dc2883..eb8210923 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core-macro" -version = "0.1.6" +version = "0.1.7" authors = ["Jonathan Kelley"] edition = "2021" description = "Core macro for Dioxus Virtual DOM" diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 123013e5e..cb880d51b 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -27,7 +27,7 @@ tokio = { version = "1.12.0", features = [ "rt", "time", ], optional = true, default-features = false } -dioxus-core-macro = { path = "../core-macro", version = "^0.1.6" } +dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.4" } webbrowser = "0.5.5" mime_guess = "2.0.3" diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 032dbe3bf..4dfb03a6b 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version ="^0.1.7", default-features = false } dioxus-html = { path = "../html", version ="^0.1.4", default-features = false } -dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"} +dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } serde = "1" url = "2.2.2" From f965030b80491f47ecdbff4698ec4d891a5d4cbe Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:36:10 -0500 Subject: [PATCH 043/256] Release dioxus-core v0.1.8 --- Cargo.toml | 2 +- packages/core/Cargo.toml | 2 +- packages/desktop/Cargo.toml | 2 +- packages/hooks/Cargo.toml | 2 +- packages/html/Cargo.toml | 2 +- packages/router/Cargo.toml | 2 +- packages/ssr/Cargo.toml | 2 +- packages/web/Cargo.toml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 105ce5050..4839ed024 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "./packages/core", version = "^0.1.7" } +dioxus-core = { path = "./packages/core", version = "^0.1.8" } dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index f54c6d5d9..c6cf62115 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core" -version = "0.1.7" +version = "0.1.8" authors = ["Jonathan Kelley"] edition = "2018" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index cb880d51b..a382b7166 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version = "^0.1.7", features = ["serialize"] } +dioxus-core = { path = "../core", version = "^0.1.8", features = ["serialize"] } argh = "0.1.4" serde = "1.0.120" serde_json = "1.0.61" diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 375fef087..05368f171 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -12,4 +12,4 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../../packages/core", version ="^0.1.7"} +dioxus-core = { path = "../../packages/core", version = "^0.1.8" } diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 3c0f07b9b..b46f73c75 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://docs.rs/dioxus" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7"} +dioxus-core = { path = "../core", version = "^0.1.8" } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 4dfb03a6b..3a6912f18 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7", default-features = false } +dioxus-core = { path = "../core", version = "^0.1.8", default-features = false } dioxus-html = { path = "../html", version ="^0.1.4", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 754fbbb78..77cf055a5 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7", features = ["serialize"] } +dioxus-core = { path = "../core", version = "^0.1.8", features = ["serialize"] } [dev-dependencies] diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index a5befe7b3..502164e39 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version = "^0.1.7" } +dioxus-core = { path = "../core", version = "^0.1.8" } dioxus-html = { path = "../html", version = "^0.1.4" } js-sys = "0.3" wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } From 11ba189eab817507f71a3dfd66a403d0fae33b98 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:36:24 -0500 Subject: [PATCH 044/256] Release dioxus-html v0.1.5 --- Cargo.toml | 2 +- packages/desktop/Cargo.toml | 2 +- packages/html/Cargo.toml | 2 +- packages/router/Cargo.toml | 2 +- packages/web/Cargo.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4839ed024..d31c9f555 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "./packages/core", version = "^0.1.8" } -dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } +dioxus-html = { path = "./packages/html", version = "^0.1.5", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index a382b7166..0a8a1e5d9 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -28,7 +28,7 @@ tokio = { version = "1.12.0", features = [ "time", ], optional = true, default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } -dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.4" } +dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.5" } webbrowser = "0.5.5" mime_guess = "2.0.3" diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index b46f73c75..73e6b4a8a 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-html" -version = "0.1.4" +version = "0.1.5" authors = ["Jonathan Kelley"] edition = "2018" description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 3a6912f18..c60a6b105 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.8", default-features = false } -dioxus-html = { path = "../html", version ="^0.1.4", default-features = false } +dioxus-html = { path = "../html", version = "^0.1.5", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } serde = "1" diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 502164e39..48b871282 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.8" } -dioxus-html = { path = "../html", version = "^0.1.4" } +dioxus-html = { path = "../html", version = "^0.1.5" } js-sys = "0.3" wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } lazy_static = "1.4.0" From 539ca2d655c0e4eba7d4bfcb359ec2e12d915800 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:37:03 -0500 Subject: [PATCH 045/256] Release dioxus-hooks v0.1.7 --- Cargo.toml | 2 +- packages/hooks/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d31c9f555..3c954df9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "./packages/core", version = "^0.1.8" } dioxus-html = { path = "./packages/html", version = "^0.1.5", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } -dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } +dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } dioxus-desktop = { path = "./packages/desktop", version = "^0.1.5", optional = true } diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 05368f171..aad2952bf 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-hooks" -version = "0.1.6" +version = "0.1.7" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for a remote webview instance" From 3c2d5985da9c0bb61eb7bd0eec519baa23df4c59 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:37:34 -0500 Subject: [PATCH 046/256] Release dioxus-ssr v0.1.3 --- Cargo.toml | 2 +- packages/ssr/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3c954df9c..5977f0508 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } dioxus-desktop = { path = "./packages/desktop", version = "^0.1.5", optional = true } -dioxus-ssr = { path = "./packages/ssr", version = "^0.1.2", optional = true } +dioxus-ssr = { path = "./packages/ssr", version = "^0.1.3", optional = true } dioxus-router = { path = "./packages/router", version = "^0.1.0", optional = true } dioxus-mobile = { path = "./packages/mobile", version = "^0.0.3", optional = true } diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 77cf055a5..039c2bae4 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-ssr" -version = "0.1.2" +version = "0.1.3" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus render-to-string" From 2c51a090e3d9d30c61c6d19a317f50d9df5cd27b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:37:50 -0500 Subject: [PATCH 047/256] Release dioxus-web v0.0.5 --- Cargo.toml | 2 +- packages/web/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5977f0508..62d25783a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ dioxus-html = { path = "./packages/html", version = "^0.1.5", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } -dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } +dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } dioxus-desktop = { path = "./packages/desktop", version = "^0.1.5", optional = true } dioxus-ssr = { path = "./packages/ssr", version = "^0.1.3", optional = true } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 48b871282..926b52429 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-web" -version = "0.0.4" +version = "0.0.5" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for the web browser using websys" From 189ddd8c51a5cacef440580962c9166236ba3471 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:39:21 -0500 Subject: [PATCH 048/256] fix: move bindings into package --- packages/web/src/bindings.rs | 2 +- packages/web/src/interpreter.js | 499 ++++++++++++++++++++++++++++++++ 2 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 packages/web/src/interpreter.js diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index 9b0c2ebfb..fa96e3230 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -2,7 +2,7 @@ use js_sys::Function; use wasm_bindgen::prelude::*; use web_sys::{Element, Node}; -#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")] +#[wasm_bindgen(module = "/src/interpreter.js")] extern "C" { pub type Interpreter; diff --git a/packages/web/src/interpreter.js b/packages/web/src/interpreter.js new file mode 100644 index 000000000..ea50ec3d3 --- /dev/null +++ b/packages/web/src/interpreter.js @@ -0,0 +1,499 @@ +export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +export class Interpreter { + root; + stack; + listeners; + handlers; + lastNodeWasText; + nodes; + constructor(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + top() { + return this.stack[this.stack.length - 1]; + } + pop() { + return this.stack.pop(); + } + PushRoot(root) { + const node = this.nodes[root]; + this.stack.push(node); + } + AppendChildren(many) { + let root = this.stack[this.stack.length - (1 + many)]; + let to_add = this.stack.splice(this.stack.length - many); + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + } + ReplaceWith(root_id, m) { + let root = this.nodes[root_id]; + let els = this.stack.splice(this.stack.length - m); + root.replaceWith(...els); + } + InsertAfter(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + InsertBefore(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + Remove(root) { + let node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + } + CreateTextNode(text, root) { + // todo: make it so the types are okay + const node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + } + CreateElement(tag, root) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + this.nodes[root] = el; + this.stack.push(el); + } + CreateElementNs(tag, root, ns) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + CreatePlaceholder(root) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + NewEventListener(event_name, root, handler) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + else { + this.listeners[event_name]++; + } + } + RemoveEventListener(root, event_name) { + const element = this.nodes[root]; + element.removeAttribute(`data-dioxus-id`); + this.listeners[event_name]--; + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; + } + } + SetText(root, text) { + this.nodes[root].textContent = text; + } + SetAttribute(root, field, value, ns) { + const name = field; + const node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } + else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } + else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } + else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root, name) { + const node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + } + handleEdits(edits) { + this.stack.push(this.root); + for (let edit of edits) { + this.handleEdit(edit); + } + } + handleEdit(edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event) => { + let target = event.target; + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + let contents = serialize_event(event); + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + if (event.type == "submit") { + event.preventDefault(); + } + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + if (realId == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} +function serialize_event(event) { + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; + } + case "compositionend": + case "compositionstart": + case "compositionupdate": { + let { data } = event; + return { + data, + }; + } + case "keydown": + case "keypress": + case "keyup": { + let { charCode, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location, + repeat: repeat, + which: which, + locale: "locale", + }; + } + case "focus": + case "blur": { + return {}; + } + case "change": { + let target = event.target; + let value; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } + else { + value = target.value ?? target.textContent; + } + return { + value: value, + }; + } + case "input": + case "invalid": + case "reset": + case "submit": { + let target = event.target; + let value = target.value ?? target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, pointerId, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, pointerType, isPrimary, } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary, + }; + } + case "select": { + return {}; + } + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + const { altKey, ctrlKey, metaKey, shiftKey, } = event; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, + }; + } + case "scroll": { + return {}; + } + case "wheel": { + const { deltaX, deltaY, deltaZ, deltaMode, } = event; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + const { animationName, elapsedTime, pseudoElement, } = event; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + const { propertyName, elapsedTime, pseudoElement, } = event; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": { + return {}; + } + case "toggle": { + return {}; + } + default: { + return {}; + } + } +} +const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, +}; From d570bb6c42c59ef76bee7f55be123bdbee688794 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:40:03 -0500 Subject: [PATCH 050/256] Release dioxus-router v0.1.1 --- Cargo.toml | 2 +- packages/router/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 62d25783a..e1184c2de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } dioxus-desktop = { path = "./packages/desktop", version = "^0.1.5", optional = true } dioxus-ssr = { path = "./packages/ssr", version = "^0.1.3", optional = true } -dioxus-router = { path = "./packages/router", version = "^0.1.0", optional = true } +dioxus-router = { path = "./packages/router", version = "^0.1.1", optional = true } dioxus-mobile = { path = "./packages/mobile", version = "^0.0.3", optional = true } # dioxus-liveview = { path = "./packages/liveview", optional = true } diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index c60a6b105..a2cd4a559 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-router" -version = "0.1.0" +version = "0.1.1" edition = "2018" description = "Dioxus VirtualDOM renderer for the web browser using websys" license = "MIT/Apache-2.0" From 578c1dc069be48818a2f36e3b86927fd001b336e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:42:06 -0500 Subject: [PATCH 051/256] fix: enable default for any eventhandler --- packages/core/src/nodes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 0efd64177..b18aeaf2d 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -355,7 +355,7 @@ pub struct EventHandler<'bump, T = ()> { pub callback: RefCell>>, } -impl<'a> Default for EventHandler<'a> { +impl<'a, T> Default for EventHandler<'a, T> { fn default() -> Self { Self { callback: RefCell::new(None), From f6c6134d676edc05278d21817ddf06f7bbe1fbf8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:42:38 -0500 Subject: [PATCH 052/256] Release dioxus-core v0.1.9 --- Cargo.toml | 2 +- packages/core/Cargo.toml | 2 +- packages/desktop/Cargo.toml | 2 +- packages/hooks/Cargo.toml | 2 +- packages/html/Cargo.toml | 2 +- packages/router/Cargo.toml | 2 +- packages/ssr/Cargo.toml | 2 +- packages/web/Cargo.toml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e1184c2de..76630ca58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "./packages/core", version = "^0.1.8" } +dioxus-core = { path = "./packages/core", version = "^0.1.9" } dioxus-html = { path = "./packages/html", version = "^0.1.5", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index c6cf62115..03b378e88 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core" -version = "0.1.8" +version = "0.1.9" authors = ["Jonathan Kelley"] edition = "2018" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 0a8a1e5d9..e7700348c 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version = "^0.1.8", features = ["serialize"] } +dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } argh = "0.1.4" serde = "1.0.120" serde_json = "1.0.61" diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index aad2952bf..7d8e4d26a 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -12,4 +12,4 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../../packages/core", version = "^0.1.8" } +dioxus-core = { path = "../../packages/core", version = "^0.1.9" } diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 73e6b4a8a..339f47f3a 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://docs.rs/dioxus" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version = "^0.1.8" } +dioxus-core = { path = "../core", version = "^0.1.9" } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index a2cd4a559..77345dd3c 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version = "^0.1.8", default-features = false } +dioxus-core = { path = "../core", version = "^0.1.9", default-features = false } dioxus-html = { path = "../html", version = "^0.1.5", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 039c2bae4..02f51f453 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version = "^0.1.8", features = ["serialize"] } +dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } [dev-dependencies] diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 926b52429..c2232775c 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version = "^0.1.8" } +dioxus-core = { path = "../core", version = "^0.1.9" } dioxus-html = { path = "../html", version = "^0.1.5" } js-sys = "0.3" wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } From 6339fa8188bac0cdc62318286776c54d57744805 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:42:52 -0500 Subject: [PATCH 053/256] Release dioxus-html v0.1.6, dioxus-router v0.1.1 --- Cargo.toml | 2 +- packages/desktop/Cargo.toml | 2 +- packages/html/Cargo.toml | 2 +- packages/router/Cargo.toml | 2 +- packages/web/Cargo.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 76630ca58..0cbecbc17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "./packages/core", version = "^0.1.9" } -dioxus-html = { path = "./packages/html", version = "^0.1.5", optional = true } +dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index e7700348c..18bd2b082 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -28,7 +28,7 @@ tokio = { version = "1.12.0", features = [ "time", ], optional = true, default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } -dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.5" } +dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 339f47f3a..293d4c465 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-html" -version = "0.1.5" +version = "0.1.6" authors = ["Jonathan Kelley"] edition = "2018" description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 77345dd3c..1c6ed24d2 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.9", default-features = false } -dioxus-html = { path = "../html", version = "^0.1.5", default-features = false } +dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } serde = "1" diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index c2232775c..a14d6cb0a 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.9" } -dioxus-html = { path = "../html", version = "^0.1.5" } +dioxus-html = { path = "../html", version = "^0.1.6" } js-sys = "0.3" wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } lazy_static = "1.4.0" From 9b3192323e4cd43cbe4b4432ac413a1904a43a14 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:43:43 -0500 Subject: [PATCH 054/256] Release dioxus-desktop v0.1.6 --- Cargo.toml | 2 +- packages/desktop/Cargo.toml | 2 +- packages/mobile/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0cbecbc17..6c4272863 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", option dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } -dioxus-desktop = { path = "./packages/desktop", version = "^0.1.5", optional = true } +dioxus-desktop = { path = "./packages/desktop", version = "^0.1.6", optional = true } dioxus-ssr = { path = "./packages/ssr", version = "^0.1.3", optional = true } dioxus-router = { path = "./packages/router", version = "^0.1.1", optional = true } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 18bd2b082..0d393e1ef 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-desktop" -version = "0.1.5" +version = "0.1.6" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for a remote webview instance" diff --git a/packages/mobile/Cargo.toml b/packages/mobile/Cargo.toml index 373814260..9da440343 100644 --- a/packages/mobile/Cargo.toml +++ b/packages/mobile/Cargo.toml @@ -12,4 +12,4 @@ license = "MIT/Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-desktop = { path = "../desktop", version ="^0.1.5"} +dioxus-desktop = { path = "../desktop", version = "^0.1.6" } From 875977f5a666804b100f089200ca3db1e87ac93f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:48:41 -0500 Subject: [PATCH 055/256] publish: update 0.1.8 for dioxus --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6c4272863..e0fbc94b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus" -version = "0.1.7" +version = "0.1.8" authors = ["Jonathan Kelley"] edition = "2018" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" From 3d3d18554da562faf68b8231787ecc59fde79d62 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 29 Jan 2022 10:36:44 -0600 Subject: [PATCH 056/256] Fix misspelled variable name in usestate.rs --- packages/hooks/src/usestate.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 914587c13..bce5b2b88 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -153,8 +153,8 @@ impl UseState { /// } /// ``` pub fn modify(&self, f: impl FnOnce(&T) -> T) { - let curernt = self.slot.borrow(); - let new_val = f(curernt.as_ref()); + let current = self.slot.borrow(); + let new_val = f(current.as_ref()); (self.setter)(new_val); } From 00ff54bb3bbb1c6cf98d4050f7a1b66c30b0c673 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 29 Jan 2022 10:43:10 -0600 Subject: [PATCH 057/256] Fix typo in comment in use_shared_state.rs --- packages/hooks/src/use_shared_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 9b59601b5..a05e99d7d 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -33,7 +33,7 @@ impl ProvidedStateInner { /// This hook provides some relatively light ergonomics around shared state. /// /// It is not a substitute for a proper state management system, but it is capable enough to provide use_state - type -/// ergonimics in a pinch, with zero cost. +/// ergonomics in a pinch, with zero cost. /// /// # Example /// From 169028705dc5b64f33a880d3b7bb845d44cf882f Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 31 Jan 2022 00:33:14 +0800 Subject: [PATCH 058/256] feat: add `DesktopContext` --- examples/borderless.rs | 21 +++++ packages/desktop/src/desktop_context.rs | 110 ++++-------------------- packages/desktop/src/lib.rs | 31 +++++++ 3 files changed, 71 insertions(+), 91 deletions(-) create mode 100644 examples/borderless.rs diff --git a/examples/borderless.rs b/examples/borderless.rs new file mode 100644 index 000000000..18bbbb80b --- /dev/null +++ b/examples/borderless.rs @@ -0,0 +1,21 @@ +use dioxus::prelude::*; +use dioxus_desktop::desktop_context::DesktopContext; + +fn main() { + dioxus::desktop::launch_cfg(app, |cfg| { + cfg.with_window(|w| { + w.with_title("BorderLess Demo") + .with_decorations(false) + }) + }); +} + +fn app (cx: Scope) -> Element { + let desktop = cx.consume_context::().unwrap(); + cx.render(rsx!( + div { + style: "background-color: black; height: 20px; width: 100%", + onmousedown: move |_| desktop.drag_window(), + } + )) +} \ No newline at end of file diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 48c82f5a8..3a48879c0 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,100 +1,28 @@ -use std::cell::RefCell; +use wry::application::event_loop::EventLoopProxy; -use dioxus::prelude::Scope; -use dioxus_core as dioxus; -use dioxus_core::{Context, Element, LazyNodes, NodeFactory, Properties}; -use dioxus_core_macro::Props; +use crate::UserWindowEvent; -/* -This module provides a set of Dioxus components to easily manage windows, tabs, etc. +type ProxyType = EventLoopProxy; -Windows can be created anywhere in the tree, making them very flexible for things like modals, etc. - -*/ -pub struct DesktopContext {} +#[derive(Clone)] +pub struct DesktopContext { + proxy: ProxyType, +} impl DesktopContext { - fn add_window(&mut self) { - // + pub fn new(proxy: ProxyType) -> Self { + Self { proxy } } - fn close_window(&mut self) { - // + + pub fn drag_window(&self) { + let _ = self.proxy.send_event(UserWindowEvent::DragWindow); } -} -enum WindowHandlers { - Resized(Box), - Moved(Box), - CloseRequested(Box), - Destroyed(Box), - DroppedFile(Box), - HoveredFile(Box), - HoverFileCancelled(Box), - ReceivedTimeText(Box), - Focused(Box), -} + pub fn minimized(&self, minimized: bool) { + let _ = self.proxy.send_event(UserWindowEvent::Minimized(minimized)); + } -#[derive(Props)] -pub struct WebviewWindowProps<'a> { - onclose: &'a dyn FnMut(()), - - onopen: &'a dyn FnMut(()), - - /// focuse me - onfocused: &'a dyn FnMut(()), - - children: Element, -} - -/// A handle to a -/// -/// -/// -/// -/// -/// -/// -/// -/// -pub fn WebviewWindow(cx: Scope) -> Element { - let dtcx = cx.consume_state::>()?; - - cx.use_hook(|_| {}); - - // render the children directly - todo!() - // cx.render(LazyNodes::new(move |f: NodeFactory| { - // f.fragment_from_iter(cx.children()) - // })) -} - -pub struct WindowHandle {} - -/// Get a handle to the current window from inside a component -pub fn use_current_window(cx: Scope) -> Option { - todo!() -} - -#[test] -fn syntax_works() { - use dioxus_core as dioxus; - use dioxus_core::prelude::*; - use dioxus_core_macro::*; - use dioxus_hooks::*; - use dioxus_html as dioxus_elements; - - static App: Component = |cx| { - cx.render(rsx! { - // left window - WebviewWindow { - onclose: move |evt| {} - onopen: move |evt| {} - onfocused: move |evt| {} - - div { - - } - } - }) - }; -} + pub fn maximized(&self, maximized: bool) { + let _ = self.proxy.send_event(UserWindowEvent::Maximized(maximized)); + } +} \ No newline at end of file diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 36b017077..b27ef7742 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -51,10 +51,12 @@ //! Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! pub mod cfg; +pub mod desktop_context; pub mod escape; pub mod events; use cfg::DesktopConfig; +use desktop_context::DesktopContext; use dioxus_core::*; use std::{ collections::{HashMap, VecDeque}, @@ -282,6 +284,27 @@ pub fn launch_with_props( // match _evt { UserWindowEvent::Update => desktop.try_load_ready_webviews(), + UserWindowEvent::DragWindow => { + // this loop just run once, because dioxus-desktop is unsupport multi-window. + for webview in desktop.webviews.values() { + let window = webview.window(); + window.drag_window().unwrap(); + } + } + UserWindowEvent::Minimized(state) => { + // this loop just run once, because dioxus-desktop is unsupport multi-window. + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_minimized(state); + } + } + UserWindowEvent::Maximized(state) => { + // this loop just run once, because dioxus-desktop is unsupport multi-window. + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_maximized(state); + } + } } } Event::MainEventsCleared => {} @@ -296,6 +319,9 @@ pub fn launch_with_props( pub enum UserWindowEvent { Update, + DragWindow, + Minimized(bool), + Maximized(bool), } pub struct DesktopController { @@ -322,6 +348,7 @@ impl DesktopController { let return_sender = sender.clone(); let proxy = evt.clone(); + let desktop_context_proxy = proxy.clone(); std::thread::spawn(move || { // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads let runtime = tokio::runtime::Builder::new_multi_thread() @@ -333,6 +360,10 @@ impl DesktopController { let mut dom = VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver)); + let window_context = DesktopContext::new(desktop_context_proxy); + + dom.base_scope().provide_context(window_context); + let edits = dom.rebuild(); edit_queue From 31a2f8f63abbbcc39c1ce23fb0d47d8e2e010f6f Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 31 Jan 2022 01:06:43 +0800 Subject: [PATCH 059/256] feat: add `DesktopContext` --- packages/desktop/src/desktop_context.rs | 31 +++++++++++++++++++++---- packages/desktop/src/lib.rs | 19 +++++++++++---- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 3a48879c0..577e33c54 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -4,25 +4,48 @@ use crate::UserWindowEvent; type ProxyType = EventLoopProxy; +/// Desktop-Window handle api context +/// +/// you can use this context control some window event +/// +/// you can use `cx.consume_context::` to get this context +/// +/// ```rust +/// let desktop = cx.consume_context::().unwrap(); +/// ``` #[derive(Clone)] pub struct DesktopContext { proxy: ProxyType, } impl DesktopContext { + pub fn new(proxy: ProxyType) -> Self { Self { proxy } } + /// trigger the drag-window event + /// + /// Moves the window with the left mouse button until the button is released. + /// + /// you need use it in `onmousedown` event: + /// ```rust + /// onmousedown: move |_| { desktop.drag_window(); } + /// ``` pub fn drag_window(&self) { let _ = self.proxy.send_event(UserWindowEvent::DragWindow); } - pub fn minimized(&self, minimized: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Minimized(minimized)); + pub fn minimize(&self, minimized: bool) { + let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized)); } - pub fn maximized(&self, maximized: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Maximized(maximized)); + pub fn maximize(&self, maximized: bool) { + let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); } + + pub fn close(&self) { + let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); + } + } \ No newline at end of file diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index b27ef7742..45314df34 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -288,20 +288,28 @@ pub fn launch_with_props( // this loop just run once, because dioxus-desktop is unsupport multi-window. for webview in desktop.webviews.values() { let window = webview.window(); - window.drag_window().unwrap(); + // start to drag the window. + // if the drag_window have any err. we don't do anything. + let _ = window.drag_window(); } } - UserWindowEvent::Minimized(state) => { + UserWindowEvent::CloseWindow => { + // close window + *control_flow = ControlFlow::Exit; + } + UserWindowEvent::Minimize(state) => { // this loop just run once, because dioxus-desktop is unsupport multi-window. for webview in desktop.webviews.values() { let window = webview.window(); + // change window minimized state. window.set_minimized(state); } } - UserWindowEvent::Maximized(state) => { + UserWindowEvent::Maximize(state) => { // this loop just run once, because dioxus-desktop is unsupport multi-window. for webview in desktop.webviews.values() { let window = webview.window(); + // change window maximized state. window.set_maximized(state); } } @@ -320,8 +328,9 @@ pub fn launch_with_props( pub enum UserWindowEvent { Update, DragWindow, - Minimized(bool), - Maximized(bool), + CloseWindow, + Minimize(bool), + Maximize(bool), } pub struct DesktopController { From d8ee553eaccd72ab68ff4dfe650e654bbd99bdf1 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sun, 30 Jan 2022 11:44:56 -0600 Subject: [PATCH 060/256] Fix the license field in Cargo.toml to be valid SPDX --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e0fbc94b6..468518f5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.8" authors = ["Jonathan Kelley"] edition = "2018" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" From 6537ad37b867cbf8367a22294da8948feba735cb Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sun, 30 Jan 2022 11:50:57 -0600 Subject: [PATCH 061/256] Add actual copies of the relevant licenses --- LICENSE | 1 - LICENSE-APACHE | 176 +++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE-MIT | 23 +++++++ 3 files changed, 199 insertions(+), 1 deletion(-) delete mode 100644 LICENSE create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8dbc080b8..000000000 --- a/LICENSE +++ /dev/null @@ -1 +0,0 @@ -MIT/Apache-2 diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 000000000..1b5ec8b78 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 000000000..31aa79387 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. From 3edf3e367f8ac7fb813dc3826e780e2a1125b1d8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 30 Jan 2022 14:08:03 -0500 Subject: [PATCH 062/256] wip: debugging --- packages/core/src/diff.rs | 59 ++++++++++-- packages/core/src/nodes.rs | 18 +++- packages/core/src/scopes.rs | 113 +++++++++++----------- packages/core/src/virtual_dom.rs | 4 +- packages/core/tests/miri_stress.rs | 115 +++++++++++++++++++---- packages/core/tests/sharedstate.rs | 96 ++++++++++++++++++- packages/router/src/components/link.rs | 2 +- packages/router/src/components/route.rs | 4 +- packages/router/src/components/router.rs | 1 + packages/router/src/platform/mod.rs | 7 +- packages/router/src/service.rs | 76 ++++++++------- 11 files changed, 367 insertions(+), 128 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 499d9543e..c8e2b29a7 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -443,6 +443,13 @@ impl<'bump> DiffState<'bump> { new_idx }; + log::info!( + "created component {:?} with parent {:?} and originator {:?}", + new_idx, + parent_idx, + vcomponent.originator + ); + // Actually initialize the caller's slot with the right address vcomponent.scope.set(Some(new_idx)); @@ -476,6 +483,11 @@ impl<'bump> DiffState<'bump> { // Check the most common cases first // these are *actual* elements, not wrappers around lists (Text(old), Text(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - text are the sames"); + return; + } + if let Some(root) = old.id.get() { if old.text != new.text { self.mutations.set_text(new.text, root.as_u64()); @@ -487,24 +499,46 @@ impl<'bump> DiffState<'bump> { } (Placeholder(old), Placeholder(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - placeholder are the sames"); + return; + } + if let Some(root) = old.id.get() { self.scopes.update_node(new_node, root); new.id.set(Some(root)) } } - (Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node), + (Element(old), Element(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - element are the sames"); + return; + } + self.diff_element_nodes(old, new, old_node, new_node) + } // These two sets are pointers to nodes but are not actually nodes themselves (Component(old), Component(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - placeholder are the sames"); + return; + } self.diff_component_nodes(old_node, new_node, *old, *new) } - (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), + (Fragment(old), Fragment(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - fragment are the sames"); + return; + } + self.diff_fragment_nodes(old, new) + } // The normal pathway still works, but generates slightly weird instructions // This pathway ensures uses the ReplaceAll, not the InsertAfter and remove (Placeholder(_), Fragment(new)) => { + log::debug!("replacing placeholder with fragment {:?}", new_node); self.stack .create_children(new.children, MountType::Replace { old: old_node }); } @@ -686,7 +720,11 @@ impl<'bump> DiffState<'bump> { new: &'bump VComponent<'bump>, ) { let scope_addr = old.scope.get().unwrap(); - log::trace!("diff_component_nodes: {:?}", scope_addr); + log::trace!( + "diff_component_nodes. old: {:#?} new: {:#?}", + old_node, + new_node + ); if std::ptr::eq(old, new) { log::trace!("skipping component diff - component is the sames"); @@ -747,6 +785,8 @@ impl<'bump> DiffState<'bump> { self.stack.scope_stack.pop(); } else { + // + log::debug!("scope stack is {:#?}", self.stack.scope_stack); self.stack .create_node(new_node, MountType::Replace { old: old_node }); } @@ -790,7 +830,10 @@ impl<'bump> DiffState<'bump> { match (old, new) { ([], []) => {} ([], _) => self.stack.create_children(new, MountType::Append), - (_, []) => self.remove_nodes(old, true), + (_, []) => { + log::debug!("removing nodes {:?}", old); + self.remove_nodes(old, true) + } _ => { let new_is_keyed = new[0].key().is_some(); let old_is_keyed = old[0].key().is_some(); @@ -1216,6 +1259,7 @@ impl<'bump> DiffState<'bump> { } fn replace_node(&mut self, old: &'bump VNode<'bump>, nodes_created: usize) { + log::debug!("Replacing node {:?}", old); match old { VNode::Element(el) => { let id = old @@ -1262,11 +1306,12 @@ impl<'bump> DiffState<'bump> { ) { // or cache the vec on the diff machine for node in nodes { + log::debug!("removing {:?}", node); match node { VNode::Text(t) => { // this check exists because our null node will be removed but does not have an ID if let Some(id) = t.id.get() { - self.scopes.collect_garbage(id); + // self.scopes.collect_garbage(id); if gen_muts { self.mutations.remove(id.as_u64()); @@ -1275,7 +1320,7 @@ impl<'bump> DiffState<'bump> { } VNode::Placeholder(a) => { let id = a.id.get().unwrap(); - self.scopes.collect_garbage(id); + // self.scopes.collect_garbage(id); if gen_muts { self.mutations.remove(id.as_u64()); @@ -1289,6 +1334,8 @@ impl<'bump> DiffState<'bump> { } self.remove_nodes(e.children, false); + + // self.scopes.collect_garbage(id); } VNode::Fragment(f) => { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index b18aeaf2d..64da46279 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -4,7 +4,7 @@ //! cheap and *very* fast to construct - building a full tree should be quick. use crate::{ - innerlude::{Element, Properties, Scope, ScopeId, ScopeState}, + innerlude::{Element, FcSlot, Properties, Scope, ScopeId, ScopeState}, lazynodes::LazyNodes, AnyEvent, Component, }; @@ -177,11 +177,19 @@ impl Debug for VNode<'_> { .field("children", &el.children) .finish(), VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text), - VNode::Placeholder(_) => write!(s, "VNode::VPlaceholder"), + VNode::Placeholder(t) => write!(s, "VNode::VPlaceholder {{ id: {:?} }}", t.id), VNode::Fragment(frag) => { write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children) } - VNode::Component(comp) => write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc), + VNode::Component(comp) => { + s.debug_struct("VNode::VComponent") + .field("fnptr", &comp.user_fc) + .field("key", &comp.key) + .field("scope", &comp.scope) + .field("originator", &comp.originator) + .finish() + //write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc) + } } } } @@ -384,7 +392,7 @@ pub struct VComponent<'src> { pub originator: ScopeId, pub scope: Cell>, pub can_memoize: bool, - pub user_fc: *const (), + pub user_fc: FcSlot, pub props: RefCell>>, } @@ -557,7 +565,7 @@ impl<'a> NodeFactory<'a> { key: key.map(|f| self.raw_text(f).0), scope: Default::default(), can_memoize: P::IS_STATIC, - user_fc: component as *const (), + user_fc: component as *mut std::os::raw::c_void, originator: self.scope.scope_id(), props: RefCell::new(Some(Box::new(VComponentProps { // local_props: RefCell::new(Some(props)), diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 77c648d18..d8cfd6505 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -13,7 +13,9 @@ use std::{ rc::Rc, }; -pub(crate) type FcSlot = *const (); +/// for traceability, we use the raw fn pointer to identify the function +/// we can use this with the traceback crate to resolve funciton names +pub(crate) type FcSlot = *mut std::os::raw::c_void; pub(crate) struct Heuristic { hook_arena_size: usize, @@ -82,7 +84,7 @@ impl ScopeArena { pub(crate) fn new_with_key( &self, - fc_ptr: *const (), + fc_ptr: FcSlot, vcomp: Box, parent_scope: Option, container: ElementId, @@ -116,25 +118,47 @@ impl ScopeArena { scope.subtree.set(subtree); scope.our_arena_idx = new_scope_id; scope.container = container; + scope.fnptr = fc_ptr; let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope); debug_assert!(any_item.is_none()); } else { // else create a new scope + let (node_capacity, hook_capacity) = self + .heuristics + .borrow() + .get(&fc_ptr) + .map(|h| (h.node_arena_size, h.hook_arena_size)) + .unwrap_or_default(); + self.scopes.borrow_mut().insert( new_scope_id, - self.bump.alloc(ScopeState::new( - height, + self.bump.alloc(ScopeState { container, - new_scope_id, + our_arena_idx: new_scope_id, parent_scope, - vcomp, - self.tasks.clone(), - self.heuristics - .borrow() - .get(&fc_ptr) - .map(|h| (h.node_arena_size, h.hook_arena_size)) - .unwrap_or_default(), - )), + height, + fnptr: fc_ptr, + props: RefCell::new(Some(vcomp)), + frames: [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)], + + // todo: subtrees + subtree: Cell::new(0), + is_subtree_root: Cell::new(false), + + generation: 0.into(), + + tasks: self.tasks.clone(), + shared_contexts: Default::default(), + + items: RefCell::new(SelfReferentialItems { + listeners: Default::default(), + borrowed_props: Default::default(), + }), + + hook_arena: Bump::new(), + hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)), + hook_idx: Default::default(), + }), ); } @@ -169,10 +193,17 @@ impl ScopeArena { pub fn update_node<'a>(&self, node: &'a VNode<'a>, id: ElementId) { let node = unsafe { extend_vnode(node) }; - *self.nodes.borrow_mut().get_mut(id.0).unwrap() = node; + + let mut nodes = self.nodes.borrow_mut(); + let entry = nodes.get_mut(id.0); + match entry { + Some(_node) => *_node = node, + None => panic!("cannot update node {}", id), + } } pub fn collect_garbage(&self, id: ElementId) { + log::debug!("collecting garbage for {:?}", id); self.nodes.borrow_mut().remove(id.0); } @@ -189,7 +220,7 @@ impl ScopeArena { /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will /// be dropped. pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { - log::trace!("Ensuring drop safety for scope {:?}", scope_id); + // log::trace!("Ensuring drop safety for scope {:?}", scope_id); if let Some(scope) = self.get_scope(scope_id) { let mut items = scope.items.borrow_mut(); @@ -217,12 +248,23 @@ impl ScopeArena { // Cycle to the next frame and then reset it // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners - log::trace!("Running scope {:?}", id); self.ensure_drop_safety(id); // todo: we *know* that this is aliased by the contents of the scope itself let scope = unsafe { &mut *self.get_scope_raw(id).expect("could not find scope") }; + // if cfg!(debug_assertions) { + log::debug!("running scope {:?} symbol: {:?}", id, scope.fnptr); + + // todo: resolve frames properly + backtrace::resolve(scope.fnptr, |symbol| { + // backtrace::resolve(scope.fnptr as *mut std::os::raw::c_void, |symbol| { + // panic!("asd"); + // log::trace!("Running scope {:?}, ptr {:?}", id, scope.fnptr); + log::debug!("running scope {:?} symbol: {:?}", id, symbol.name()); + }); + // } + // Safety: // - We dropped the listeners, so no more &mut T can be used while these are held // - All children nodes that rely on &mut T are replaced with a new reference @@ -421,6 +463,7 @@ pub struct ScopeState { pub(crate) container: ElementId, pub(crate) our_arena_idx: ScopeId, pub(crate) height: u32, + pub(crate) fnptr: FcSlot, // todo: subtrees pub(crate) is_subtree_root: Cell, @@ -449,43 +492,6 @@ pub struct SelfReferentialItems<'a> { // Public methods exposed to libraries and components impl ScopeState { - fn new( - height: u32, - container: ElementId, - our_arena_idx: ScopeId, - parent_scope: Option<*mut ScopeState>, - vcomp: Box, - tasks: Rc, - (node_capacity, hook_capacity): (usize, usize), - ) -> Self { - ScopeState { - container, - our_arena_idx, - parent_scope, - height, - props: RefCell::new(Some(vcomp)), - frames: [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)], - - // todo: subtrees - subtree: Cell::new(0), - is_subtree_root: Cell::new(false), - - generation: 0.into(), - - tasks, - shared_contexts: Default::default(), - - items: RefCell::new(SelfReferentialItems { - listeners: Default::default(), - borrowed_props: Default::default(), - }), - - hook_arena: Bump::new(), - hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)), - hook_idx: Default::default(), - } - } - /// Get the subtree ID that this scope belongs to. /// /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route @@ -731,6 +737,7 @@ impl ScopeState { while let Some(parent_ptr) = search_parent { // safety: all parent pointers are valid thanks to the bump arena let parent = unsafe { &*parent_ptr }; + log::trace!("Searching parent scope {:?}", parent.scope_id()); if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::()) { return Some(shared.clone().downcast::().unwrap()); } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 4e426caad..1f443db86 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -212,7 +212,7 @@ impl VirtualDom { let scopes = ScopeArena::new(channel.0.clone()); scopes.new_with_key( - root as *const _, + root as *mut std::os::raw::c_void, Box::new(VComponentProps { props: root_props, memo: |_a, _b| unreachable!("memo on root will neve be run"), @@ -475,6 +475,8 @@ impl VirtualDom { let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); diff_state.stack.push(DiffInstruction::Diff { new, old }); + + log::debug!("pushing scope {:?} onto scope stack", scopeid); diff_state.stack.scope_stack.push(scopeid); let scope = scopes.get_scope(scopeid).unwrap(); diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 6b7805261..0f4f64546 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -319,36 +319,117 @@ fn test_pass_thru() { #[inline_props] fn Router<'a>(cx: Scope, children: Element<'a>) -> Element { cx.render(rsx! { - &cx.props.children + div { + &cx.props.children + } }) } - fn MemoizedThing(cx: Scope) -> Element { - rsx!(cx, div { "memoized" }) + #[inline_props] + fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { + header { + nav { + &cx.props.children + } + } + }) + } + + fn NavMenu(cx: Scope) -> Element { + rsx!(cx, + NavBrand {} + div { + NavStart {} + NavEnd {} + } + ) + } + + fn NavBrand(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + fn NavStart(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + fn NavEnd(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + #[inline_props] + fn MainContainer<'a>( + cx: Scope, + nav: Element<'a>, + body: Element<'a>, + footer: Element<'a>, + ) -> Element { + cx.render(rsx! { + div { + class: "columns is-mobile", + div { + class: "column is-full", + &cx.props.nav, + &cx.props.body, + &cx.props.footer, + } + } + }) } fn app(cx: Scope) -> Element { - let thing = cx.use_hook(|_| "asd"); - rsx!(cx, - Router { - MemoizedThing { - } + let nav = cx.render(rsx! { + NavContainer { + NavMenu {} } - ) + }); + let body = cx.render(rsx! { + div {} + }); + let footer = cx.render(rsx! { + div {} + }); + + cx.render(rsx! { + MainContainer { + nav: nav, + body: body, + footer: footer, + } + }) } let mut dom = new_dom(app, ()); let _ = dom.rebuild(); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); + for x in 0..40 { + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); + dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); + dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); + dom.work_with_deadline(|| false); + } } diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs index 3f04aa154..41b178793 100644 --- a/packages/core/tests/sharedstate.rs +++ b/packages/core/tests/sharedstate.rs @@ -1,6 +1,6 @@ #![allow(unused, non_upper_case_globals)] -use dioxus::{prelude::*, DomEdit, Mutations}; +use dioxus::{prelude::*, DomEdit, Mutations, SchedulerMsg, ScopeId}; use dioxus_core as dioxus; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; @@ -37,3 +37,97 @@ fn shared_state_test() { ] ); } + +#[test] +fn swap_test() { + struct MySharedState(&'static str); + + fn app(cx: Scope) -> Element { + let val = cx.use_hook(|_| 0); + *val += 1; + + cx.provide_context(MySharedState("world!")); + + let child = match *val % 2 { + 0 => rsx!( + Child1 { + Child1 { } + Child2 { } + } + ), + _ => rsx!( + Child2 { + Child2 { } + Child2 { } + } + ), + }; + + cx.render(rsx!( + Router { + div { child } + } + )) + } + + #[inline_props] + fn Router<'a>(cx: Scope, children: Element<'a>) -> Element<'a> { + cx.render(rsx!(div { children })) + } + + #[inline_props] + fn Child1<'a>(cx: Scope, children: Element<'a>) -> Element { + let shared = cx.consume_context::().unwrap(); + println!("Child1: {}", shared.0); + cx.render(rsx! { + div { + "{shared.0}", + children + } + }) + } + + #[inline_props] + fn Child2<'a>(cx: Scope, children: Element<'a>) -> Element { + let shared = cx.consume_context::().unwrap(); + println!("Child2: {}", shared.0); + cx.render(rsx! { + h1 { + "{shared.0}", + children + } + }) + } + + let mut dom = VirtualDom::new(app); + let Mutations { edits, .. } = dom.rebuild(); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); + // dom.work_with_deadline(|| false); + + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); + // dom.work_with_deadline(|| false); + + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); + // dom.work_with_deadline(|| false); + + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + // dom.work_with_deadline(|| false); + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + // dom.work_with_deadline(|| false); + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + // dom.work_with_deadline(|| false); +} diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index fd8eea66d..18e9fd92e 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -41,7 +41,7 @@ pub struct LinkProps<'a> { } pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - log::trace!("render Link to {}", cx.props.to); + // log::trace!("render Link to {}", cx.props.to); if let Some(service) = cx.consume_context::() { return cx.render(rsx! { a { diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index 129953eea..e4dd2f9c8 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -30,7 +30,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { Some(ctx) => ctx.total_route.to_string(), None => cx.props.to.to_string(), }; - log::trace!("total route for {} is {}", cx.props.to, total_route); + // log::trace!("total route for {} is {}", cx.props.to, total_route); // provide our route context let route_context = cx.provide_context(RouteContext { @@ -48,7 +48,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { Some(RouteInner {}) }); - log::trace!("Checking route {}", cx.props.to); + // log::trace!("Checking route {}", cx.props.to); if router_root.should_render(cx.scope_id()) { cx.render(rsx!(&cx.props.children)) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 240c3806a..bf23645f5 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -17,6 +17,7 @@ pub struct RouterProps<'a> { #[allow(non_snake_case)] pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { + log::debug!("running router {:?}", cx.scope_id()); let svc = cx.use_hook(|_| { let update = cx.schedule_update_any(); cx.provide_context(RouterService::new(update, cx.scope_id())) diff --git a/packages/router/src/platform/mod.rs b/packages/router/src/platform/mod.rs index 7afc1316a..ec59a4703 100644 --- a/packages/router/src/platform/mod.rs +++ b/packages/router/src/platform/mod.rs @@ -1,9 +1,4 @@ pub trait RouterProvider { fn get_current_route(&self) -> String; - fn subscribe_to_route_changes(&self, callback: Box); -} - -enum RouteChange { - LinkTo(String), - Back(String), + fn listen(&self, callback: Box); } diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index b62bc78d5..b6d8ba11b 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -7,14 +7,18 @@ use std::{ use dioxus_core::ScopeId; +use crate::platform::RouterProvider; + pub struct RouterService { pub(crate) regen_route: Rc, pub(crate) pending_events: Rc>>, - history: Rc>, slots: Rc>>, onchange_listeners: Rc>>, root_found: Rc>>, cur_path_params: Rc>>, + + // history: Rc, + history: Rc>, listener: HistoryListener, } @@ -58,12 +62,12 @@ impl RouterService { root_found.set(None); // checking if the route is valid is cheap, so we do it for (slot, root) in slots.borrow_mut().iter().rev() { - log::trace!("regenerating slot {:?} for root '{}'", slot, root); + // log::trace!("regenerating slot {:?} for root '{}'", slot, root); regen_route(*slot); } for listener in onchange_listeners.borrow_mut().iter() { - log::trace!("regenerating listener {:?}", listener); + // log::trace!("regenerating listener {:?}", listener); regen_route(*listener); } @@ -87,31 +91,31 @@ impl RouterService { } pub fn push_route(&self, route: &str) { - log::trace!("Pushing route: {}", route); + // log::trace!("Pushing route: {}", route); self.history.borrow_mut().push(route); } pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { let clean = clean_route(route); - log::trace!("Registered route '{}' with scope id {:?}", clean, scope); + // log::trace!("Registered route '{}' with scope id {:?}", clean, scope); self.slots.borrow_mut().push((scope, clean)); } pub fn should_render(&self, scope: ScopeId) -> bool { - log::trace!("Should render scope id {:?}?", scope); + // log::trace!("Should render scope id {:?}?", scope); if let Some(root_id) = self.root_found.get() { - log::trace!(" we already found a root with scope id {:?}", root_id); + // log::trace!(" we already found a root with scope id {:?}", root_id); if root_id == scope { - log::trace!(" yes - it's a match"); + // log::trace!(" yes - it's a match"); return true; } - log::trace!(" no - it's not a match"); + // log::trace!(" no - it's not a match"); return false; } let location = self.history.borrow().location(); let path = location.path(); - log::trace!(" current path is '{}'", path); + // log::trace!(" current path is '{}'", path); let roots = self.slots.borrow(); @@ -120,23 +124,23 @@ impl RouterService { // fallback logic match root { Some((id, route)) => { - log::trace!( - " matched given scope id {:?} with route root '{}'", - scope, - route, - ); + // log::trace!( + // " matched given scope id {:?} with route root '{}'", + // scope, + // route, + // ); if let Some(params) = route_matches_path(route, path) { - log::trace!(" and it matches the current path '{}'", path); + // log::trace!(" and it matches the current path '{}'", path); self.root_found.set(Some(*id)); *self.cur_path_params.borrow_mut() = params; true } else { if route == "" { - log::trace!(" and the route is the root, so we will use that without a better match"); + // log::trace!(" and the route is the root, so we will use that without a better match"); self.root_found.set(Some(*id)); true } else { - log::trace!(" and the route '{}' is not the root nor does it match the current path", route); + // log::trace!(" and the route '{}' is not the root nor does it match the current path", route); false } } @@ -154,12 +158,12 @@ impl RouterService { } pub fn subscribe_onchange(&self, id: ScopeId) { - log::trace!("Subscribing onchange for scope id {:?}", id); + // log::trace!("Subscribing onchange for scope id {:?}", id); self.onchange_listeners.borrow_mut().insert(id); } pub fn unsubscribe_onchange(&self, id: ScopeId) { - log::trace!("Subscribing onchange for scope id {:?}", id); + // log::trace!("Subscribing onchange for scope id {:?}", id); self.onchange_listeners.borrow_mut().remove(&id); } } @@ -182,36 +186,36 @@ fn route_matches_path(route: &str, path: &str) -> Option let route_pieces = route.split('/').collect::>(); let path_pieces = clean_path(path).split('/').collect::>(); - log::trace!( - " checking route pieces {:?} vs path pieces {:?}", - route_pieces, - path_pieces, - ); + // log::trace!( + // " checking route pieces {:?} vs path pieces {:?}", + // route_pieces, + // path_pieces, + // ); if route_pieces.len() != path_pieces.len() { - log::trace!(" the routes are different lengths"); + // log::trace!(" the routes are different lengths"); return None; } let mut matches = HashMap::new(); for (i, r) in route_pieces.iter().enumerate() { - log::trace!(" checking route piece '{}' vs path", r); + // log::trace!(" checking route piece '{}' vs path", r); // If this is a parameter then it matches as long as there's // _any_thing in that spot in the path. if r.starts_with(':') { - log::trace!( - " route piece '{}' starts with a colon so it matches anything", - r, - ); + // log::trace!( + // " route piece '{}' starts with a colon so it matches anything", + // r, + // ); let param = &r[1..]; matches.insert(param.to_string(), path_pieces[i].to_string()); continue; } - log::trace!( - " route piece '{}' must be an exact match for path piece '{}'", - r, - path_pieces[i], - ); + // log::trace!( + // " route piece '{}' must be an exact match for path piece '{}'", + // r, + // path_pieces[i], + // ); if path_pieces[i] != *r { return None; } From 3bb5c8142cf63489ffd0ff0c316cc168852d5c4f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 30 Jan 2022 17:47:58 -0500 Subject: [PATCH 063/256] fix: switch to future based diffing this commit removes the old manual fiber implementation in favor of a traditional recursion based approach. This should make the diffing algorithm easier to work on and eliminate various stack-based bugs in. --- packages/core/.rustfmt.toml | 1 + packages/core/src/diff.rs | 21 +- packages/core/src/diff_async.rs | 1102 ++++++++++++++++++++++++++++++ packages/core/src/lib.rs | 6 +- packages/core/src/mutations.rs | 8 + packages/core/src/nodes.rs | 59 +- packages/core/src/scopes.rs | 32 +- packages/core/src/virtual_dom.rs | 104 +-- packages/core/tests/diffing.rs | 257 ++----- 9 files changed, 1268 insertions(+), 322 deletions(-) create mode 100644 packages/core/.rustfmt.toml create mode 100644 packages/core/src/diff_async.rs diff --git a/packages/core/.rustfmt.toml b/packages/core/.rustfmt.toml new file mode 100644 index 000000000..70a9483a8 --- /dev/null +++ b/packages/core/.rustfmt.toml @@ -0,0 +1 @@ +struct_lit_width = 80 diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index c8e2b29a7..3b9c7692c 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -222,7 +222,7 @@ impl<'bump> DiffStack<'bump> { self.nodes_created_stack.pop().unwrap() } - fn current_scope(&self) -> Option { + pub fn current_scope(&self) -> Option { self.scope_stack.last().copied() } @@ -314,9 +314,10 @@ impl<'bump> DiffState<'bump> { } MountType::Append => { - self.mutations.edits.push(AppendChildren { - many: nodes_created as u32, - }); + self.mutations.append_children(nodes_created as u32); + // self.mutations.edits.push(AppendChildren { + // many: nodes_created as u32, + // }); } MountType::InsertAfter { other_node } => { @@ -470,7 +471,8 @@ impl<'bump> DiffState<'bump> { self.stack.create_component(new_idx, nextnode); // Finally, insert this scope as a seen node. - self.mutations.dirty_scopes.insert(new_idx); + self.mutations.mark_dirty_scope(new_idx); + // self.mutations.dirty_scopes.insert(new_idx); } // ================================= @@ -639,9 +641,10 @@ impl<'bump> DiffState<'bump> { } if old.children.is_empty() && !new.children.is_empty() { - self.mutations.edits.push(PushRoot { - root: root.as_u64(), - }); + self.mutations.push_root(root); + // self.mutations.edits.push(PushRoot { + // root: root.as_u64(), + // }); self.stack.element_stack.push(root); self.stack.instructions.push(DiffInstruction::PopElement); self.stack.create_children(new.children, MountType::Append); @@ -771,7 +774,7 @@ impl<'bump> DiffState<'bump> { // this should auto drop the previous props self.scopes.run_scope(scope_addr); - self.mutations.dirty_scopes.insert(scope_addr); + self.mutations.mark_dirty_scope(scope_addr); self.diff_node( self.scopes.wip_head(scope_addr), diff --git a/packages/core/src/diff_async.rs b/packages/core/src/diff_async.rs new file mode 100644 index 000000000..e5d63ffb5 --- /dev/null +++ b/packages/core/src/diff_async.rs @@ -0,0 +1,1102 @@ +use crate::innerlude::*; +use fxhash::{FxHashMap, FxHashSet}; +use smallvec::{smallvec, SmallVec}; + +pub(crate) struct AsyncDiffState<'bump> { + pub(crate) scopes: &'bump ScopeArena, + pub(crate) mutations: Mutations<'bump>, + pub(crate) force_diff: bool, + pub(crate) element_stack: SmallVec<[ElementId; 10]>, + pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, +} + +impl<'b> AsyncDiffState<'b> { + pub fn new(scopes: &'b ScopeArena) -> Self { + Self { + scopes, + mutations: Mutations::new(), + force_diff: false, + element_stack: smallvec![], + scope_stack: smallvec![], + } + } + + pub fn diff_scope(&mut self, scopeid: ScopeId) { + let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); + self.scope_stack.push(scopeid); + let scope = self.scopes.get_scope(scopeid).unwrap(); + self.element_stack.push(scope.container); + self.diff_node(old, new); + } + + pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) { + use VNode::*; + match (old_node, new_node) { + // Check the most common cases first + // these are *actual* elements, not wrappers around lists + (Text(old), Text(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - text are the sames"); + return; + } + + if let Some(root) = old.id.get() { + if old.text != new.text { + self.mutations.set_text(new.text, root.as_u64()); + } + self.scopes.update_node(new_node, root); + + new.id.set(Some(root)); + } + } + + (Placeholder(old), Placeholder(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - placeholder are the sames"); + return; + } + + if let Some(root) = old.id.get() { + self.scopes.update_node(new_node, root); + new.id.set(Some(root)) + } + } + + (Element(old), Element(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - element are the sames"); + return; + } + self.diff_element_nodes(old, new, old_node, new_node) + } + + // These two sets are pointers to nodes but are not actually nodes themselves + (Component(old), Component(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - placeholder are the sames"); + return; + } + self.diff_component_nodes(old_node, new_node, *old, *new) + } + + (Fragment(old), Fragment(new)) => { + if std::ptr::eq(old, new) { + log::trace!("skipping node diff - fragment are the sames"); + return; + } + self.diff_fragment_nodes(old, new) + } + + // The normal pathway still works, but generates slightly weird instructions + // This pathway ensures uses the ReplaceAll, not the InsertAfter and remove + (Placeholder(_), Fragment(_)) => { + log::debug!("replacing placeholder with fragment {:?}", new_node); + self.replace_node(old_node, new_node); + } + + // Anything else is just a basic replace and create + ( + Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), + Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), + ) => self.replace_node(old_node, new_node), + } + } + + pub fn create_node(&mut self, node: &'b VNode<'b>) -> usize { + match node { + VNode::Text(vtext) => self.create_text_node(vtext, node), + VNode::Placeholder(anchor) => self.create_anchor_node(anchor, node), + VNode::Element(element) => self.create_element_node(element, node), + VNode::Fragment(frag) => self.create_fragment_node(frag), + VNode::Component(component) => self.create_component_node(*component), + } + } + + fn create_text_node(&mut self, vtext: &'b VText<'b>, node: &'b VNode<'b>) -> usize { + let real_id = self.scopes.reserve_node(node); + self.mutations.create_text_node(vtext.text, real_id); + vtext.id.set(Some(real_id)); + + 1 + } + + fn create_anchor_node(&mut self, anchor: &'b VPlaceholder, node: &'b VNode<'b>) -> usize { + let real_id = self.scopes.reserve_node(node); + self.mutations.create_placeholder(real_id); + anchor.id.set(Some(real_id)); + + 1 + } + + fn create_element_node(&mut self, element: &'b VElement<'b>, node: &'b VNode<'b>) -> usize { + let VElement { + tag: tag_name, + listeners, + attributes, + children, + namespace, + id: dom_id, + parent: parent_id, + .. + } = element; + + // set the parent ID for event bubbling + // self.stack.instructions.push(DiffInstruction::PopElement); + + let parent = self.element_stack.last().unwrap(); + parent_id.set(Some(*parent)); + + // set the id of the element + let real_id = self.scopes.reserve_node(node); + self.element_stack.push(real_id); + dom_id.set(Some(real_id)); + + self.mutations.create_element(tag_name, *namespace, real_id); + + if let Some(cur_scope_id) = self.current_scope() { + for listener in *listeners { + listener.mounted_node.set(Some(real_id)); + self.mutations.new_event_listener(listener, cur_scope_id); + } + } else { + log::warn!("create element called with no scope on the stack - this is an error for a live dom"); + } + + for attr in *attributes { + self.mutations.set_attribute(attr, real_id.as_u64()); + } + + if !children.is_empty() { + self.create_and_append_children(children); + } + + 1 + } + + fn create_fragment_node(&mut self, frag: &'b VFragment<'b>) -> usize { + self.create_children(frag.children) + } + + fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize { + let parent_idx = self.current_scope().unwrap(); + + // the component might already exist - if it does, we need to reuse it + // this makes figure out when to drop the component more complicated + let new_idx = if let Some(idx) = vcomponent.scope.get() { + assert!(self.scopes.get_scope(idx).is_some()); + idx + } else { + // Insert a new scope into our component list + let props: Box = vcomponent.props.borrow_mut().take().unwrap(); + let props: Box = unsafe { std::mem::transmute(props) }; + let new_idx = self.scopes.new_with_key( + vcomponent.user_fc, + props, + Some(parent_idx), + self.element_stack.last().copied().unwrap(), + 0, + ); + + new_idx + }; + + log::info!( + "created component {:?} with parent {:?} and originator {:?}", + new_idx, + parent_idx, + vcomponent.originator + ); + + // Actually initialize the caller's slot with the right address + vcomponent.scope.set(Some(new_idx)); + + match vcomponent.can_memoize { + true => { + // todo: implement promotion logic. save us from boxing props that we don't need + } + false => { + // track this component internally so we know the right drop order + } + } + + // Run the scope for one iteration to initialize it + self.scopes.run_scope(new_idx); + + self.mutations.mark_dirty_scope(new_idx); + + // self.stack.create_component(new_idx, nextnode); + // Finally, insert this scope as a seen node. + + // Take the node that was just generated from running the component + let nextnode = self.scopes.fin_head(new_idx); + self.create_node(nextnode) + } + + fn diff_element_nodes( + &mut self, + old: &'b VElement<'b>, + new: &'b VElement<'b>, + old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + ) { + let root = old.id.get().unwrap(); + + // If the element type is completely different, the element needs to be re-rendered completely + // This is an optimization React makes due to how users structure their code + // + // This case is rather rare (typically only in non-keyed lists) + if new.tag != old.tag || new.namespace != old.namespace { + self.replace_node(old_node, new_node); + return; + } + + self.scopes.update_node(new_node, root); + + new.id.set(Some(root)); + new.parent.set(old.parent.get()); + + // todo: attributes currently rely on the element on top of the stack, but in theory, we only need the id of the + // element to modify its attributes. + // it would result in fewer instructions if we just set the id directly. + // it would also clean up this code some, but that's not very important anyways + + // Diff Attributes + // + // It's extraordinarily rare to have the number/order of attributes change + // In these cases, we just completely erase the old set and make a new set + // + // TODO: take a more efficient path than this + if old.attributes.len() == new.attributes.len() { + for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { + if old_attr.value != new_attr.value || new_attr.is_volatile { + self.mutations.set_attribute(new_attr, root.as_u64()); + } + } + } else { + for attribute in old.attributes { + self.mutations.remove_attribute(attribute, root.as_u64()); + } + for attribute in new.attributes { + self.mutations.set_attribute(attribute, root.as_u64()) + } + } + + // Diff listeners + // + // It's extraordinarily rare to have the number/order of listeners change + // In the cases where the listeners change, we completely wipe the data attributes and add new ones + // + // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) + // + // TODO: take a more efficient path than this + if let Some(cur_scope_id) = self.current_scope() { + if old.listeners.len() == new.listeners.len() { + for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { + if old_l.event != new_l.event { + self.mutations + .remove_event_listener(old_l.event, root.as_u64()); + self.mutations.new_event_listener(new_l, cur_scope_id); + } + new_l.mounted_node.set(old_l.mounted_node.get()); + } + } else { + for listener in old.listeners { + self.mutations + .remove_event_listener(listener.event, root.as_u64()); + } + for listener in new.listeners { + listener.mounted_node.set(Some(root)); + self.mutations.new_event_listener(listener, cur_scope_id); + } + } + } + + match (old.children.len(), new.children.len()) { + (0, 0) => {} + (0, _) => { + let created = self.create_children(new.children); + self.mutations.append_children(created as u32); + } + (_, _) => { + self.diff_children(old.children, new.children); + } + }; + } + + fn diff_component_nodes( + &mut self, + old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + old: &'b VComponent<'b>, + new: &'b VComponent<'b>, + ) { + let scope_addr = old.scope.get().unwrap(); + log::trace!( + "diff_component_nodes. old: {:#?} new: {:#?}", + old_node, + new_node + ); + + if std::ptr::eq(old, new) { + log::trace!("skipping component diff - component is the sames"); + return; + } + + // Make sure we're dealing with the same component (by function pointer) + if old.user_fc == new.user_fc { + self.enter_scope(scope_addr); + + // Make sure the new component vnode is referencing the right scope id + new.scope.set(Some(scope_addr)); + + // make sure the component's caller function is up to date + let scope = self + .scopes + .get_scope(scope_addr) + .unwrap_or_else(|| panic!("could not find {:?}", scope_addr)); + + // take the new props out regardless + // when memoizing, push to the existing scope if memoization happens + let new_props = new.props.borrow_mut().take().unwrap(); + + let should_run = { + if old.can_memoize { + let props_are_the_same = unsafe { + scope + .props + .borrow() + .as_ref() + .unwrap() + .memoize(new_props.as_ref()) + }; + !props_are_the_same || self.force_diff + } else { + true + } + }; + + if should_run { + let _old_props = scope + .props + .replace(unsafe { std::mem::transmute(Some(new_props)) }); + + // this should auto drop the previous props + self.scopes.run_scope(scope_addr); + self.mutations.mark_dirty_scope(scope_addr); + + self.diff_node( + self.scopes.wip_head(scope_addr), + self.scopes.fin_head(scope_addr), + ); + } else { + log::trace!("memoized"); + // memoization has taken place + drop(new_props); + }; + + self.leave_scope(); + } else { + log::debug!("scope stack is {:#?}", self.scope_stack); + self.replace_node(old_node, new_node); + } + } + + fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) { + // This is the case where options or direct vnodes might be used. + // In this case, it's faster to just skip ahead to their diff + if old.children.len() == 1 && new.children.len() == 1 { + self.diff_node(&old.children[0], &new.children[0]); + return; + } + + debug_assert!(!old.children.is_empty()); + debug_assert!(!new.children.is_empty()); + + self.diff_children(old.children, new.children); + } + + // ============================================= + // Utilities for creating new diff instructions + // ============================================= + + // Diff the given set of old and new children. + // + // The parent must be on top of the change list stack when this function is + // entered: + // + // [... parent] + // + // the change list stack is in the same state when this function returns. + // + // If old no anchors are provided, then it's assumed that we can freely append to the parent. + // + // Remember, non-empty lists does not mean that there are real elements, just that there are virtual elements. + // + // Fragment nodes cannot generate empty children lists, so we can assume that when a list is empty, it belongs only + // to an element, and appending makes sense. + fn diff_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + // Remember, fragments can never be empty (they always have a single child) + match (old, new) { + ([], []) => {} + ([], _) => self.create_and_append_children(new), + (_, []) => self.remove_nodes(old, true), + _ => { + let new_is_keyed = new[0].key().is_some(); + let old_is_keyed = old[0].key().is_some(); + + debug_assert!( + new.iter().all(|n| n.key().is_some() == new_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + debug_assert!( + old.iter().all(|o| o.key().is_some() == old_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + + if new_is_keyed && old_is_keyed { + self.diff_keyed_children(old, new); + } else { + self.diff_non_keyed_children(old, new); + } + } + } + } + + // Diff children that are not keyed. + // + // The parent must be on the top of the change list stack when entering this + // function: + // + // [... parent] + // + // the change list stack is in the same state when this function returns. + fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + // Handled these cases in `diff_children` before calling this function. + debug_assert!(!new.is_empty()); + debug_assert!(!old.is_empty()); + + use std::cmp::Ordering; + match old.len().cmp(&new.len()) { + Ordering::Greater => self.remove_nodes(&old[new.len()..], true), + Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), + Ordering::Equal => {} + } + + for (new, old) in new.iter().zip(old.iter()) { + self.diff_node(old, new); + } + } + + // Diffing "keyed" children. + // + // With keyed children, we care about whether we delete, move, or create nodes + // versus mutate existing nodes in place. Presumably there is some sort of CSS + // transition animation that makes the virtual DOM diffing algorithm + // observable. By specifying keys for nodes, we know which virtual DOM nodes + // must reuse (or not reuse) the same physical DOM nodes. + // + // This is loosely based on Inferno's keyed patching implementation. However, we + // have to modify the algorithm since we are compiling the diff down into change + // list instructions that will be executed later, rather than applying the + // changes to the DOM directly as we compare virtual DOMs. + // + // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 + // + // The stack is empty upon entry. + fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + if cfg!(debug_assertions) { + let mut keys = fxhash::FxHashSet::default(); + let mut assert_unique_keys = |children: &'b [VNode<'b>]| { + keys.clear(); + for child in children { + let key = child.key(); + debug_assert!( + key.is_some(), + "if any sibling is keyed, all siblings must be keyed" + ); + keys.insert(key); + } + debug_assert_eq!( + children.len(), + keys.len(), + "keyed siblings must each have a unique key" + ); + }; + assert_unique_keys(old); + assert_unique_keys(new); + } + + // First up, we diff all the nodes with the same key at the beginning of the + // children. + // + // `shared_prefix_count` is the count of how many nodes at the start of + // `new` and `old` share the same keys. + let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { + Some(count) => count, + None => return, + }; + + // Ok, we now hopefully have a smaller range of children in the middle + // within which to re-order nodes with the same keys, remove old nodes with + // now-unused keys, and create new nodes with fresh keys. + + let old_middle = &old[left_offset..(old.len() - right_offset)]; + let new_middle = &new[left_offset..(new.len() - right_offset)]; + + debug_assert!( + !((old_middle.len() == new_middle.len()) && old_middle.is_empty()), + "keyed children must have the same number of children" + ); + + if new_middle.is_empty() { + // remove the old elements + self.remove_nodes(old_middle, true); + } else if old_middle.is_empty() { + // there were no old elements, so just create the new elements + // we need to find the right "foothold" though - we shouldn't use the "append" at all + if left_offset == 0 { + // insert at the beginning of the old list + let foothold = &old[old.len() - right_offset]; + self.create_and_insert_before(new_middle, foothold); + } else if right_offset == 0 { + // insert at the end the old list + let foothold = old.last().unwrap(); + self.create_and_insert_after(new_middle, foothold); + } else { + // inserting in the middle + let foothold = &old[left_offset - 1]; + self.create_and_insert_after(new_middle, foothold); + } + } else { + self.diff_keyed_middle(old_middle, new_middle); + } + } + + /// Diff both ends of the children that share keys. + /// + /// Returns a left offset and right offset of that indicates a smaller section to pass onto the middle diffing. + /// + /// If there is no offset, then this function returns None and the diffing is complete. + fn diff_keyed_ends( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + ) -> Option<(usize, usize)> { + let mut left_offset = 0; + + for (old, new) in old.iter().zip(new.iter()) { + // abort early if we finally run into nodes with different keys + if old.key() != new.key() { + break; + } + self.diff_node(old, new); + left_offset += 1; + } + + // If that was all of the old children, then create and append the remaining + // new children and we're finished. + if left_offset == old.len() { + self.create_and_insert_after(&new[left_offset..], old.last().unwrap()); + return None; + } + + // And if that was all of the new children, then remove all of the remaining + // old children and we're finished. + if left_offset == new.len() { + self.remove_nodes(&old[left_offset..], true); + return None; + } + + // if the shared prefix is less than either length, then we need to walk backwards + let mut right_offset = 0; + for (old, new) in old.iter().rev().zip(new.iter().rev()) { + // abort early if we finally run into nodes with different keys + if old.key() != new.key() { + break; + } + self.diff_node(old, new); + right_offset += 1; + } + + Some((left_offset, right_offset)) + } + + // The most-general, expensive code path for keyed children diffing. + // + // We find the longest subsequence within `old` of children that are relatively + // ordered the same way in `new` (via finding a longest-increasing-subsequence + // of the old child's index within `new`). The children that are elements of + // this subsequence will remain in place, minimizing the number of DOM moves we + // will have to do. + // + // Upon entry to this function, the change list stack must be empty. + // + // This function will load the appropriate nodes onto the stack and do diffing in place. + // + // Upon exit from this function, it will be restored to that same self. + fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + /* + 1. Map the old keys into a numerical ordering based on indices. + 2. Create a map of old key to its index + 3. Map each new key to the old key, carrying over the old index. + - IE if we have ABCD becomes BACD, our sequence would be 1,0,2,3 + - if we have ABCD to ABDE, our sequence would be 0,1,3,MAX because E doesn't exist + + now, we should have a list of integers that indicates where in the old list the new items map to. + + 4. Compute the LIS of this list + - this indicates the longest list of new children that won't need to be moved. + + 5. Identify which nodes need to be removed + 6. Identify which nodes will need to be diffed + + 7. Going along each item in the new list, create it and insert it before the next closest item in the LIS. + - if the item already existed, just move it to the right place. + + 8. Finally, generate instructions to remove any old children. + 9. Generate instructions to finally diff children that are the same between both + */ + + // 0. Debug sanity checks + // Should have already diffed the shared-key prefixes and suffixes. + debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); + debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); + + // 1. Map the old keys into a numerical ordering based on indices. + // 2. Create a map of old key to its index + // IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3). + let old_key_to_old_index = old + .iter() + .enumerate() + .map(|(i, o)| (o.key().unwrap(), i)) + .collect::>(); + + let mut shared_keys = FxHashSet::default(); + + // 3. Map each new key to the old key, carrying over the old index. + let new_index_to_old_index = new + .iter() + .map(|node| { + let key = node.key().unwrap(); + if let Some(&index) = old_key_to_old_index.get(&key) { + shared_keys.insert(key); + index + } else { + u32::MAX as usize + } + }) + .collect::>(); + + // If none of the old keys are reused by the new children, then we remove all the remaining old children and + // create the new children afresh. + if shared_keys.is_empty() { + if let Some(first_old) = old.get(0) { + self.remove_nodes(&old[1..], true); + let nodes_created = self.create_children(new); + self.replace_inner(first_old, nodes_created); + } else { + // I think this is wrong - why are we appending? + // only valid of the if there are no trailing elements + self.create_and_append_children(new); + } + return; + } + + // 4. Compute the LIS of this list + let mut lis_sequence = Vec::default(); + lis_sequence.reserve(new_index_to_old_index.len()); + + let mut predecessors = vec![0; new_index_to_old_index.len()]; + let mut starts = vec![0; new_index_to_old_index.len()]; + + longest_increasing_subsequence::lis_with( + &new_index_to_old_index, + &mut lis_sequence, + |a, b| a < b, + &mut predecessors, + &mut starts, + ); + + // the lis comes out backwards, I think. can't quite tell. + lis_sequence.sort_unstable(); + + // if a new node gets u32 max and is at the end, then it might be part of our LIS (because u32 max is a valid LIS) + if lis_sequence.last().map(|f| new_index_to_old_index[*f]) == Some(u32::MAX as usize) { + lis_sequence.pop(); + } + + for idx in lis_sequence.iter() { + self.diff_node(&old[new_index_to_old_index[*idx]], &new[*idx]); + } + + let mut nodes_created = 0; + + // add mount instruction for the first items not covered by the lis + let last = *lis_sequence.last().unwrap(); + if last < (new.len() - 1) { + for (idx, new_node) in new[(last + 1)..].iter().enumerate() { + let new_idx = idx + last + 1; + let old_index = new_index_to_old_index[new_idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } + } + + self.mutations.insert_after( + self.find_last_element(&new[last]).unwrap(), + nodes_created as u32, + ); + nodes_created = 0; + } + + // for each spacing, generate a mount instruction + let mut lis_iter = lis_sequence.iter().rev(); + let mut last = *lis_iter.next().unwrap(); + for next in lis_iter { + if last - next > 1 { + for (idx, new_node) in new[(next + 1)..last].iter().enumerate() { + let new_idx = idx + next + 1; + + let old_index = new_index_to_old_index[new_idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } + } + + self.mutations.insert_before( + self.find_first_element(&new[last]).unwrap(), + nodes_created as u32, + ); + + nodes_created = 0; + } + last = *next; + } + + // add mount instruction for the last items not covered by the lis + let first_lis = *lis_sequence.first().unwrap(); + if first_lis > 0 { + for (idx, new_node) in new[..first_lis].iter().enumerate() { + let old_index = new_index_to_old_index[idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } + } + + self.mutations.insert_before( + self.find_first_element(&new[first_lis]).unwrap(), + nodes_created as u32, + ); + } + } + + fn replace_node(&mut self, old: &'b VNode<'b>, new: &'b VNode<'b>) { + // fn replace_node(&mut self, old: &'b VNode<'b>, new: &'b VNode<'b>) { + log::debug!("Replacing node {:?}", old); + let nodes_created = self.create_node(new); + self.replace_inner(old, nodes_created); + } + + fn replace_inner(&mut self, old: &'b VNode<'b>, nodes_created: usize) { + match old { + VNode::Element(el) => { + let id = old + .try_mounted_id() + .unwrap_or_else(|| panic!("broke on {:?}", old)); + + self.mutations.replace_with(id, nodes_created as u32); + self.remove_nodes(el.children, false); + self.scopes.collect_garbage(id); + } + + VNode::Text(_) | VNode::Placeholder(_) => { + let id = old + .try_mounted_id() + .unwrap_or_else(|| panic!("broke on {:?}", old)); + + self.mutations.replace_with(id, nodes_created as u32); + self.scopes.collect_garbage(id); + } + + VNode::Fragment(f) => { + self.replace_inner(&f.children[0], nodes_created); + self.remove_nodes(f.children.iter().skip(1), true); + } + + VNode::Component(c) => { + let node = self.scopes.fin_head(c.scope.get().unwrap()); + self.replace_node(node, node); + + let scope_id = c.scope.get().unwrap(); + + // we can only remove components if they are actively being diffed + if self.scope_stack.contains(&c.originator) { + self.scopes.try_remove(scope_id).unwrap(); + } + } + } + } + + pub fn remove_nodes(&mut self, nodes: impl IntoIterator>, gen_muts: bool) { + for node in nodes { + match node { + VNode::Text(t) => { + // this check exists because our null node will be removed but does not have an ID + if let Some(id) = t.id.get() { + self.scopes.collect_garbage(id); + + if gen_muts { + self.mutations.remove(id.as_u64()); + } + } + } + VNode::Placeholder(a) => { + let id = a.id.get().unwrap(); + self.scopes.collect_garbage(id); + + if gen_muts { + self.mutations.remove(id.as_u64()); + } + } + VNode::Element(e) => { + let id = e.id.get().unwrap(); + + if gen_muts { + self.mutations.remove(id.as_u64()); + } + + self.scopes.collect_garbage(id); + + self.remove_nodes(e.children, false); + } + + VNode::Fragment(f) => { + self.remove_nodes(f.children, gen_muts); + } + + VNode::Component(c) => { + let scope_id = c.scope.get().unwrap(); + let root = self.scopes.root_node(scope_id); + self.remove_nodes([root], gen_muts); + + // we can only remove this node if the originator is actively + if self.scope_stack.contains(&c.originator) { + self.scopes.try_remove(scope_id).unwrap(); + } + } + } + } + } + + fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { + let mut created = 0; + for node in nodes { + created += self.create_node(node); + } + created + } + + fn create_and_append_children(&mut self, nodes: &'b [VNode<'b>]) { + let created = self.create_children(nodes); + self.mutations.append_children(created as u32); + } + + fn create_and_insert_after(&mut self, nodes: &'b [VNode<'b>], after: &'b VNode<'b>) { + let created = self.create_children(nodes); + let last = self.find_last_element(after).unwrap(); + self.mutations.insert_after(last, created as u32); + } + + fn create_and_insert_before(&mut self, nodes: &'b [VNode<'b>], before: &'b VNode<'b>) { + let created = self.create_children(nodes); + let first = self.find_first_element(before).unwrap(); + self.mutations.insert_before(first, created as u32); + } + + fn current_scope(&self) -> Option { + self.scope_stack.last().copied() + } + + fn enter_scope(&mut self, scope: ScopeId) { + self.scope_stack.push(scope); + } + + fn leave_scope(&mut self) { + self.scope_stack.pop(); + } + + fn find_last_element(&self, vnode: &'b VNode<'b>) -> Option { + let mut search_node = Some(vnode); + + loop { + match &search_node.take().unwrap() { + VNode::Text(t) => break t.id.get(), + VNode::Element(t) => break t.id.get(), + VNode::Placeholder(t) => break t.id.get(), + VNode::Fragment(frag) => { + search_node = frag.children.last(); + } + VNode::Component(el) => { + let scope_id = el.scope.get().unwrap(); + search_node = Some(self.scopes.root_node(scope_id)); + } + } + } + } + + fn find_first_element(&self, vnode: &'b VNode<'b>) -> Option { + let mut search_node = Some(vnode); + + loop { + match &search_node.take().unwrap() { + // the ones that have a direct id + VNode::Fragment(frag) => { + search_node = Some(&frag.children[0]); + } + VNode::Component(el) => { + let scope_id = el.scope.get().unwrap(); + search_node = Some(self.scopes.root_node(scope_id)); + } + VNode::Text(t) => break t.id.get(), + VNode::Element(t) => break t.id.get(), + VNode::Placeholder(t) => break t.id.get(), + } + } + } + + // fn replace_node(&mut self, old: &'b VNode<'b>, nodes_created: usize) { + // log::debug!("Replacing node {:?}", old); + // match old { + // VNode::Element(el) => { + // let id = old + // .try_mounted_id() + // .unwrap_or_else(|| panic!("broke on {:?}", old)); + + // self.mutations.replace_with(id, nodes_created as u32); + // self.remove_nodes(el.children, false); + // } + + // VNode::Text(_) | VNode::Placeholder(_) => { + // let id = old + // .try_mounted_id() + // .unwrap_or_else(|| panic!("broke on {:?}", old)); + + // self.mutations.replace_with(id, nodes_created as u32); + // } + + // VNode::Fragment(f) => { + // self.replace_node(&f.children[0], nodes_created); + // self.remove_nodes(f.children.iter().skip(1), true); + // } + + // VNode::Component(c) => { + // let node = self.scopes.fin_head(c.scope.get().unwrap()); + // self.replace_node(node, nodes_created); + + // let scope_id = c.scope.get().unwrap(); + + // // we can only remove components if they are actively being diffed + // if self.stack.scope_stack.contains(&c.originator) { + // self.scopes.try_remove(scope_id).unwrap(); + // } + // } + // } + // } + + // /// schedules nodes for garbage collection and pushes "remove" to the mutation stack + // /// remove can happen whenever + // pub(crate) fn remove_nodes( + // &mut self, + // nodes: impl IntoIterator>, + // gen_muts: bool, + // ) { + // // or cache the vec on the diff machine + // for node in nodes { + // log::debug!("removing {:?}", node); + // match node { + // VNode::Text(t) => { + // // this check exists because our null node will be removed but does not have an ID + // if let Some(id) = t.id.get() { + // // self.scopes.collect_garbage(id); + + // if gen_muts { + // self.mutations.remove(id.as_u64()); + // } + // } + // } + // VNode::Placeholder(a) => { + // let id = a.id.get().unwrap(); + // // self.scopes.collect_garbage(id); + + // if gen_muts { + // self.mutations.remove(id.as_u64()); + // } + // } + // VNode::Element(e) => { + // let id = e.id.get().unwrap(); + + // if gen_muts { + // self.mutations.remove(id.as_u64()); + // } + + // self.remove_nodes(e.children, false); + + // // self.scopes.collect_garbage(id); + // } + + // VNode::Fragment(f) => { + // self.remove_nodes(f.children, gen_muts); + // } + + // VNode::Component(c) => { + // let scope_id = c.scope.get().unwrap(); + // let root = self.scopes.root_node(scope_id); + // self.remove_nodes(Some(root), gen_muts); + + // // we can only remove this node if the originator is actively + // if self.stack.scope_stack.contains(&c.originator) { + // self.scopes.try_remove(scope_id).unwrap(); + // } + // } + // } + // } + // } + + // recursively push all the nodes of a tree onto the stack and return how many are there + fn push_all_nodes(&mut self, node: &'b VNode<'b>) -> usize { + match node { + VNode::Text(_) | VNode::Placeholder(_) => { + self.mutations.push_root(node.mounted_id()); + 1 + } + + VNode::Fragment(_) | VNode::Component(_) => { + // + let mut added = 0; + for child in node.children() { + added += self.push_all_nodes(child); + } + added + } + + VNode::Element(el) => { + let mut num_on_stack = 0; + for child in el.children.iter() { + num_on_stack += self.push_all_nodes(child); + } + self.mutations.push_root(el.id.get().unwrap()); + + num_on_stack + 1 + } + } + } +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index dba4effbb..48a8a3505 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,7 +1,8 @@ #![allow(non_snake_case)] #![doc = include_str!("../README.md")] -pub(crate) mod diff; +// pub(crate) mod diff; +pub(crate) mod diff_async; pub(crate) mod events; pub(crate) mod lazynodes; pub(crate) mod mutations; @@ -12,7 +13,8 @@ pub(crate) mod util; pub(crate) mod virtual_dom; pub(crate) mod innerlude { - pub(crate) use crate::diff::*; + pub(crate) use crate::diff_async::*; + // pub(crate) use crate::diff::*; pub use crate::events::*; pub use crate::lazynodes::*; pub use crate::mutations::*; diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index fa96edd1b..bd94d0b71 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -143,6 +143,10 @@ impl<'a> Mutations<'a> { self.edits.push(InsertBefore { n, root }); } + pub(crate) fn append_children(&mut self, n: u32) { + self.edits.push(AppendChildren { many: n }); + } + // Remove Nodes from the dom pub(crate) fn remove(&mut self, id: u64) { self.edits.push(Remove { root: id }); @@ -217,6 +221,10 @@ impl<'a> Mutations<'a> { let name = attribute.name; self.edits.push(RemoveAttribute { name, root }); } + + pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) { + self.dirty_scopes.insert(scope); + } } // refs are only assigned once diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 64da46279..74234a84c 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -175,6 +175,7 @@ impl Debug for VNode<'_> { .field("key", &el.key) .field("attrs", &el.attributes) .field("children", &el.children) + .field("id", &el.id) .finish(), VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text), VNode::Placeholder(t) => write!(s, "VNode::VPlaceholder {{ id: {:?} }}", t.id), @@ -365,9 +366,7 @@ pub struct EventHandler<'bump, T = ()> { impl<'a, T> Default for EventHandler<'a, T> { fn default() -> Self { - Self { - callback: RefCell::new(None), - } + Self { callback: RefCell::new(None) } } } @@ -441,10 +440,7 @@ pub struct NodeFactory<'a> { impl<'a> NodeFactory<'a> { pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> { - NodeFactory { - scope, - bump: &scope.wip_frame().bump, - } + NodeFactory { scope, bump: &scope.wip_frame().bump } } #[inline] @@ -454,11 +450,10 @@ impl<'a> NodeFactory<'a> { /// Directly pass in text blocks without the need to use the format_args macro. pub fn static_text(&self, text: &'static str) -> VNode<'a> { - VNode::Text(self.bump.alloc(VText { - id: empty_cell(), - text, - is_static: true, - })) + VNode::Text( + self.bump + .alloc(VText { id: empty_cell(), text, is_static: true }), + ) } /// Parses a lazy text Arguments and returns a string and a flag indicating if the text is 'static @@ -481,11 +476,7 @@ impl<'a> NodeFactory<'a> { pub fn text(&self, args: Arguments) -> VNode<'a> { let (text, is_static) = self.raw_text(args); - VNode::Text(self.bump.alloc(VText { - text, - is_static, - id: empty_cell(), - })) + VNode::Text(self.bump.alloc(VText { text, is_static, id: empty_cell() })) } pub fn element( @@ -543,13 +534,7 @@ impl<'a> NodeFactory<'a> { is_volatile: bool, ) -> Attribute<'a> { let (value, is_static) = self.raw_text(val); - Attribute { - name, - value, - is_static, - namespace, - is_volatile, - } + Attribute { name, value, is_static, namespace, is_volatile } } pub fn component

( @@ -591,11 +576,7 @@ impl<'a> NodeFactory<'a> { } pub fn listener(self, event: &'static str, callback: InternalHandler<'a>) -> Listener<'a> { - Listener { - event, - mounted_node: Cell::new(None), - callback, - } + Listener { event, mounted_node: Cell::new(None), callback } } pub fn fragment_root<'b, 'c>( @@ -611,10 +592,10 @@ impl<'a> NodeFactory<'a> { if nodes.is_empty() { VNode::Placeholder(self.bump.alloc(VPlaceholder { id: empty_cell() })) } else { - VNode::Fragment(self.bump.alloc(VFragment { - children: nodes.into_bump_slice(), - key: None, - })) + VNode::Fragment( + self.bump + .alloc(VFragment { children: nodes.into_bump_slice(), key: None }), + ) } } @@ -650,10 +631,7 @@ impl<'a> NodeFactory<'a> { ); } - VNode::Fragment(self.bump.alloc(VFragment { - children, - key: None, - })) + VNode::Fragment(self.bump.alloc(VFragment { children, key: None })) } } @@ -676,10 +654,9 @@ impl<'a> NodeFactory<'a> { } else { let children = nodes.into_bump_slice(); - Some(VNode::Fragment(self.bump.alloc(VFragment { - children, - key: None, - }))) + Some(VNode::Fragment( + self.bump.alloc(VFragment { children, key: None }), + )) } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index d8cfd6505..542afc673 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -203,7 +203,8 @@ impl ScopeArena { } pub fn collect_garbage(&self, id: ElementId) { - log::debug!("collecting garbage for {:?}", id); + // println!("collecting garbage for {:?}", id); + // log::debug!("collecting garbage for {:?}", id); self.nodes.borrow_mut().remove(id.0); } @@ -304,11 +305,9 @@ impl ScopeArena { frame.node.set(unsafe { extend_vnode(node) }); } else { let frame = scope.wip_frame(); - let node = frame - .bump - .alloc(VNode::Placeholder(frame.bump.alloc(VPlaceholder { - id: Default::default(), - }))); + let node = frame.bump.alloc(VNode::Placeholder( + frame.bump.alloc(VPlaceholder { id: Default::default() }), + )); frame.node.set(unsafe { extend_vnode(node) }); } @@ -418,10 +417,7 @@ pub struct Scope<'a, P = ()> { impl

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

Clone for Scope<'_, P> { fn clone(&self) -> Self { - Self { - scope: self.scope, - props: self.props, - } + Self { scope: self.scope, props: self.props } } } @@ -784,10 +780,7 @@ impl ScopeState { /// } ///``` pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Option> { - Some(rsx.call(NodeFactory { - scope: self, - bump: &self.wip_frame().bump, - })) + Some(rsx.call(NodeFactory { scope: self, bump: &self.wip_frame().bump })) } /// Store a value between renders @@ -894,10 +887,7 @@ impl ScopeState { self.shared_contexts.get_mut().clear(); // next: reset the node data - let SelfReferentialItems { - borrowed_props, - listeners, - } = self.items.get_mut(); + let SelfReferentialItems { borrowed_props, listeners } = self.items.get_mut(); borrowed_props.clear(); listeners.clear(); self.frames[0].reset(); @@ -955,11 +945,7 @@ pub(crate) struct TaskQueue { pub(crate) type InnerTask = Pin>>; impl TaskQueue { fn new(sender: UnboundedSender) -> Rc { - Rc::new(Self { - tasks: RefCell::new(FxHashMap::default()), - gen: Cell::new(0), - sender, - }) + Rc::new(Self { tasks: RefCell::new(FxHashMap::default()), gen: Cell::new(0), sender }) } fn push_fut(&self, task: impl Future + 'static) -> TaskId { let pinned = Box::pin(task); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1f443db86..d879e0340 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -2,6 +2,7 @@ //! //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. +use crate::diff_async::AsyncDiffState as DiffState; use crate::innerlude::*; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::{future::poll_fn, StreamExt}; @@ -453,7 +454,7 @@ impl VirtualDom { while !self.dirty_scopes.is_empty() { let scopes = &self.scopes; - let mut diff_state = DiffState::new(scopes); + let mut diff_state = AsyncDiffState::new(scopes); let mut ran_scopes = FxHashSet::default(); @@ -473,32 +474,33 @@ impl VirtualDom { self.scopes.run_scope(scopeid); - let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); - diff_state.stack.push(DiffInstruction::Diff { new, old }); + diff_state.diff_scope(scopeid); - log::debug!("pushing scope {:?} onto scope stack", scopeid); - diff_state.stack.scope_stack.push(scopeid); + let AsyncDiffState { mutations, .. } = diff_state; - let scope = scopes.get_scope(scopeid).unwrap(); - diff_state.stack.element_stack.push(scope.container); + for scope in &mutations.dirty_scopes { + self.dirty_scopes.remove(scope); + } + + committed_mutations.push(mutations); + + // if diff_state.work(&mut deadline) { + // let DiffState { mutations, .. } = diff_state; + + // for scope in &mutations.dirty_scopes { + // self.dirty_scopes.remove(scope); + // } + + // committed_mutations.push(mutations); + // } else { + // // leave the work in an incomplete state + // // + // // todo: we should store the edits and re-apply them later + // // for now, we just dump the work completely (threadsafe) + // return committed_mutations; + // } } } - - if diff_state.work(&mut deadline) { - let DiffState { mutations, .. } = diff_state; - - for scope in &mutations.dirty_scopes { - self.dirty_scopes.remove(scope); - } - - committed_mutations.push(mutations); - } else { - // leave the work in an incomplete state - // - // todo: we should store the edits and re-apply them later - // for now, we just dump the work completely (threadsafe) - return committed_mutations; - } } committed_mutations @@ -526,13 +528,15 @@ impl VirtualDom { let mut diff_state = DiffState::new(&self.scopes); self.scopes.run_scope(scope_id); - diff_state - .stack - .create_node(self.scopes.fin_head(scope_id), MountType::Append); - diff_state.stack.element_stack.push(ElementId(0)); - diff_state.stack.scope_stack.push(scope_id); - diff_state.work(|| false); + diff_state.element_stack.push(ElementId(0)); + diff_state.scope_stack.push(scope_id); + + let node = self.scopes.fin_head(scope_id); + let created = diff_state.create_node(node); + + diff_state.mutations.append_children(created as u32); + self.dirty_scopes.clear(); assert!(self.dirty_scopes.is_empty()); @@ -579,12 +583,11 @@ impl VirtualDom { ); diff_machine.force_diff = true; - diff_machine.stack.push(DiffInstruction::Diff { old, new }); - diff_machine.stack.scope_stack.push(scope_id); - + diff_machine.scope_stack.push(scope_id); let scope = diff_machine.scopes.get_scope(scope_id).unwrap(); - diff_machine.stack.element_stack.push(scope.container); - diff_machine.work(|| false); + diff_machine.element_stack.push(scope.container); + + diff_machine.diff_node(old, new); diff_machine.mutations } @@ -623,10 +626,10 @@ impl VirtualDom { /// ``` pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { let mut machine = DiffState::new(&self.scopes); - machine.stack.push(DiffInstruction::Diff { new, old }); - machine.stack.element_stack.push(ElementId(0)); - machine.stack.scope_stack.push(ScopeId(0)); - machine.work(|| false); + machine.element_stack.push(ElementId(0)); + machine.scope_stack.push(ScopeId(0)); + machine.diff_node(old, new); + machine.mutations } @@ -645,11 +648,11 @@ impl VirtualDom { /// ``` pub fn create_vnodes<'a>(&'a self, nodes: LazyNodes<'a, '_>) -> Mutations<'a> { let mut machine = DiffState::new(&self.scopes); - machine.stack.element_stack.push(ElementId(0)); - machine - .stack - .create_node(self.render_vnodes(nodes), MountType::Append); - machine.work(|| false); + machine.scope_stack.push(ScopeId(0)); + machine.element_stack.push(ElementId(0)); + let node = self.render_vnodes(nodes); + let created = machine.create_node(node); + machine.mutations.append_children(created as u32); machine.mutations } @@ -674,16 +677,15 @@ impl VirtualDom { let (old, new) = (self.render_vnodes(left), self.render_vnodes(right)); let mut create = DiffState::new(&self.scopes); - create.stack.scope_stack.push(ScopeId(0)); - create.stack.element_stack.push(ElementId(0)); - create.stack.create_node(old, MountType::Append); - create.work(|| false); + create.scope_stack.push(ScopeId(0)); + create.element_stack.push(ElementId(0)); + let created = create.create_node(old); + create.mutations.append_children(created as u32); let mut edit = DiffState::new(&self.scopes); - edit.stack.scope_stack.push(ScopeId(0)); - edit.stack.element_stack.push(ElementId(0)); - edit.stack.push(DiffInstruction::Diff { old, new }); - edit.work(|| false); + edit.scope_stack.push(ScopeId(0)); + edit.element_stack.push(ElementId(0)); + edit.diff_node(old, new); (create.mutations, edit.mutations) } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 5b98ea80e..c407359c9 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -1,4 +1,5 @@ #![allow(unused, non_upper_case_globals)] + //! Diffing Tests //! //! These tests only verify that the diffing algorithm works properly for single components. @@ -31,26 +32,14 @@ fn html_and_rsx_generate_the_same_output() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "Hello world" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "Hello world" }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, ] ); - assert_eq!( - change.edits, - [SetText { - text: "Goodbye world", - root: 2 - },] - ); + assert_eq!(change.edits, [SetText { text: "Goodbye world", root: 2 },]); } /// Should result in 3 elements on the stack @@ -67,32 +56,14 @@ fn fragments_create_properly() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "Hello a" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "Hello a" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - root: 4, - text: "Hello b" - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { root: 4, text: "Hello b" }, AppendChildren { many: 1 }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "Hello c" - }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "Hello c" }, AppendChildren { many: 1 }, AppendChildren { many: 3 }, ] @@ -116,10 +87,7 @@ fn empty_fragments_create_anchors() { assert_eq!( change.edits, [ - CreateElement { - root: 2, - tag: "div" - }, + CreateElement { root: 2, tag: "div" }, ReplaceWith { m: 1, root: 1 } ] ); @@ -138,29 +106,15 @@ fn empty_fragments_create_many_anchors() { create.edits, [CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }] ); + assert_eq!( change.edits, [ - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateElement { root: 6, tag: "div" }, ReplaceWith { m: 5, root: 1 } ] ); @@ -188,32 +142,14 @@ fn empty_fragments_create_anchors_with_many_children() { assert_eq!( change.edits, [ - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "hello: 0", - root: 3 - }, + CreateElement { tag: "div", root: 2 }, + CreateTextNode { text: "hello: 0", root: 3 }, AppendChildren { many: 1 }, - CreateElement { - tag: "div", - root: 4, - }, - CreateTextNode { - text: "hello: 1", - root: 5 - }, + CreateElement { tag: "div", root: 4 }, + CreateTextNode { text: "hello: 1", root: 5 }, AppendChildren { many: 1 }, - CreateElement { - tag: "div", - root: 6, - }, - CreateTextNode { - text: "hello: 2", - root: 7 - }, + CreateElement { tag: "div", root: 6 }, + CreateTextNode { text: "hello: 2", root: 7 }, AppendChildren { many: 1 }, ReplaceWith { root: 1, m: 3 } ] @@ -236,23 +172,11 @@ fn many_items_become_fragment() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - text: "hello", - root: 2 - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { text: "hello", root: 2 }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - text: "hello", - root: 4 - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { text: "hello", root: 4 }, AppendChildren { many: 1 }, AppendChildren { many: 2 }, ] @@ -315,7 +239,7 @@ fn two_fragments_with_differrent_elements_are_differet() { // replace the divs with new h1s CreateElement { tag: "h1", root: 7 }, ReplaceWith { root: 1, m: 1 }, - CreateElement { tag: "h1", root: 8 }, + CreateElement { tag: "h1", root: 1 }, // notice how 1 gets re-used ReplaceWith { root: 2, m: 1 }, ] ); @@ -339,39 +263,27 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, CreateElement { root: 6, tag: "p" }, AppendChildren { many: 6 }, ] ); + + // note: key reuse is always the last node that got used + // slab maintains a linked list, essentially assert_eq!( change.edits, [ Remove { root: 3 }, Remove { root: 4 }, Remove { root: 5 }, - CreateElement { root: 7, tag: "h1" }, - ReplaceWith { root: 1, m: 1 }, - CreateElement { root: 8, tag: "h1" }, + CreateElement { root: 5, tag: "h1" }, // 3 gets reused + ReplaceWith { root: 1, m: 1 }, // 1 gets deleted + CreateElement { root: 1, tag: "h1" }, // 1 gets reused ReplaceWith { root: 2, m: 1 }, ] ); @@ -395,14 +307,8 @@ fn two_fragments_with_same_elements_are_differet() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, CreateElement { root: 3, tag: "p" }, AppendChildren { many: 3 }, ] @@ -410,18 +316,9 @@ fn two_fragments_with_same_elements_are_differet() { assert_eq!( change.edits, [ - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateElement { root: 6, tag: "div" }, InsertAfter { root: 2, n: 3 }, ] ); @@ -628,14 +525,8 @@ fn keyed_diffing_additions() { assert_eq!( change.edits, [ - CreateElement { - root: 6, - tag: "div" - }, - CreateElement { - root: 7, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, + CreateElement { root: 7, tag: "div" }, InsertAfter { n: 2, root: 5 } ] ); @@ -663,14 +554,8 @@ fn keyed_diffing_additions_and_moves_on_ends() { change.edits, [ // create 11, 12 - CreateElement { - tag: "div", - root: 5 - }, - CreateElement { - tag: "div", - root: 6 - }, + CreateElement { tag: "div", root: 5 }, + CreateElement { tag: "div", root: 6 }, InsertAfter { root: 3, n: 2 }, // move 7 to the front PushRoot { root: 4 }, @@ -684,43 +569,32 @@ fn keyed_diffing_additions_and_moves_in_middle() { let dom = new_dom(); let left = rsx!({ - [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { + [/**/ 1, 2, 3, 4 /**/].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); let right = rsx!({ - [/**/ 7, 4, 13, 17, 5, 11, 12, 6 /**/].iter().map(|f| { + [/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); + // LIS: 4, 5, 6 let (_, change) = dom.diff_lazynodes(left, right); log::debug!("{:#?}", change); assert_eq!( change.edits, [ - // create 13, 17 - CreateElement { - tag: "div", - root: 5 - }, - CreateElement { - tag: "div", - root: 6 - }, - InsertBefore { root: 2, n: 2 }, - // create 11, 12 - CreateElement { - tag: "div", - root: 7 - }, - CreateElement { - tag: "div", - root: 8 - }, + // create 5, 6 + CreateElement { tag: "div", root: 5 }, + CreateElement { tag: "div", root: 6 }, InsertBefore { root: 3, n: 2 }, + // create 7, 8 + CreateElement { tag: "div", root: 7 }, + CreateElement { tag: "div", root: 8 }, + InsertBefore { root: 2, n: 2 }, // move 7 PushRoot { root: 4 }, InsertBefore { root: 1, n: 1 } @@ -756,16 +630,10 @@ fn controlled_keyed_diffing_out_of_order() { // remove 7 // create 9 and insert before 6 - CreateElement { - root: 5, - tag: "div" - }, + CreateElement { root: 5, tag: "div" }, InsertBefore { n: 1, root: 3 }, // create 0 and insert before 5 - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, InsertBefore { n: 1, root: 2 }, ] ); @@ -792,10 +660,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { assert_eq!( changes.edits, [ - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, InsertBefore { n: 1, root: 3 }, PushRoot { root: 4 }, InsertBefore { n: 1, root: 1 }, From 1ea42799c0f63a34f4eb1208bd78b2c165eb920e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 30 Jan 2022 18:34:24 -0500 Subject: [PATCH 064/256] wip: clean up the core crate after switching to recursive diff engine --- packages/core/src/diff.rs | 1170 ++++++++++------------------- packages/core/src/diff_async.rs | 1102 --------------------------- packages/core/src/lib.rs | 6 +- packages/core/src/scopes.rs | 3 +- packages/core/src/virtual_dom.rs | 2 +- packages/core/tests/earlyabort.rs | 20 +- packages/core/tests/lifecycle.rs | 119 +-- packages/core/tests/passthru.rs | 108 +++ 8 files changed, 562 insertions(+), 1968 deletions(-) delete mode 100644 packages/core/src/diff_async.rs create mode 100644 packages/core/tests/passthru.rs diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 3b9c7692c..d790292c8 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,485 +1,35 @@ -//! This module contains the stateful DiffState and all methods to diff VNodes, their properties, and their children. -//! -//! The [`DiffState`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set -//! of mutations for the RealDom to apply. -//! -//! ## Notice: -//! -//! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support -//! Components, Fragments, Suspense, SubTree memoization, incremental diffing, cancellation, NodeRefs, pausing, priority -//! scheduling, and additional batching operations. -//! -//! ## Implementation Details: -//! -//! ### IDs for elements -//! -------------------- -//! All nodes are addressed by their IDs. The RealDom provides an imperative interface for making changes to these nodes. -//! We don't necessarily require that DOM changes happen instantly during the diffing process, so the implementor may choose -//! to batch nodes if it is more performant for their application. The element IDs are indices into the internal element -//! array. The expectation is that implementors will use the ID as an index into a Vec of real nodes, allowing for passive -//! garbage collection as the VirtualDOM replaces old nodes. -//! -//! When new vnodes are created through `cx.render`, they won't know which real node they correspond to. During diffing, -//! we always make sure to copy over the ID. If we don't do this properly, the ElementId will be populated incorrectly -//! and brick the user's page. -//! -//! ### Fragment Support -//! -------------------- -//! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments -//! can be particularly challenging when they are empty, so the anchor node lets us "reserve" a spot for the empty -//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the NodeFactory - it is -//! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. Adding -//! "dummy" nodes _is_ inefficient, but it makes our diffing algorithm faster and the implementation is completely up to -//! the platform. -//! -//! Other implementations either don't support fragments or use a "child + sibling" pattern to represent them. Our code is -//! vastly simpler and more performant when we can just create a placeholder element while the fragment has no children. -//! -//! ### Suspense -//! ------------ -//! Dioxus implements Suspense slightly differently than React. In React, each fiber is manually progressed until it runs -//! into a promise-like value. React will then work on the next "ready" fiber, checking back on the previous fiber once -//! it has finished its new work. In Dioxus, we use a similar approach, but try to completely render the tree before -//! switching sub-fibers. Instead, each future is submitted into a futures-queue and the node is manually loaded later on. -//! Due to the frequent calls to "yield_now" we can get the pure "fetch-as-you-render" behavior of React Fiber. -//! -//! We're able to use this approach because we use placeholder nodes - futures that aren't ready still get submitted to -//! DOM, but as a placeholder. -//! -//! Right now, the "suspense" queue is intertwined with hooks. In the future, we should allow any future to drive attributes -//! and contents, without the need for the "use_suspense" hook. In the interim, this is the quickest way to get Suspense working. -//! -//! ## Subtree Memoization -//! ----------------------- -//! We also employ "subtree memoization" which saves us from having to check trees which hold no dynamic content. We can -//! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the -//! calls to "create" propagate this information upwards. Structures like the one below are entirely static: -//! ```rust, ignore -//! rsx!( div { class: "hello world", "this node is entirely static" } ) -//! ``` -//! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so it's up to the reconciler to -//! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP and depends on comp-time -//! hashing of the subtree from the rsx! macro. We do a very limited form of static analysis via static string pointers as -//! a way of short-circuiting the most expensive checks. -//! -//! ## Bloom Filter and Heuristics -//! ------------------------------ -//! For all components, we employ some basic heuristics to speed up allocations and pre-size bump arenas. The heuristics are -//! currently very rough, but will get better as time goes on. The information currently tracked includes the size of a -//! bump arena after first render, the number of hooks, and the number of nodes in the tree. -//! -//! ## Garbage Collection -//! --------------------- -//! Dioxus uses a passive garbage collection system to clean up old nodes once the work has been completed. This garbage -//! collection is done internally once the main diffing work is complete. After the "garbage" is collected, Dioxus will then -//! start to re-use old keys for new nodes. This results in a passive memory management system that is very efficient. -//! -//! The IDs used by the key/map are just an index into a Vec. This means that Dioxus will drive the key allocation strategy -//! so the client only needs to maintain a simple list of nodes. By default, Dioxus will not manually clean up old nodes -//! for the client. As new nodes are created, old nodes will be over-written. -//! -//! ## Further Reading and Thoughts -//! ---------------------------- -//! There are more ways of increasing diff performance here that are currently not implemented. -//! - Strong memoization of subtrees. -//! - Guided diffing. -//! - Certain web-dom-specific optimizations. -//! -//! More info on how to improve this diffing algorithm: -//! - - use crate::innerlude::*; use fxhash::{FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -use DomEdit::*; -/// Our DiffState is an iterative tree differ. -/// -/// It uses techniques of a stack machine to allow pausing and restarting of the diff algorithm. This -/// was originally implemented using recursive techniques, but Rust lacks the ability to call async functions recursively, -/// meaning we could not "pause" the original diffing algorithm. -/// -/// Instead, we use a traditional stack machine approach to diff and create new nodes. The diff algorithm periodically -/// calls "yield_now" which allows the machine to pause and return control to the caller. The caller can then wait for -/// the next period of idle time, preventing our diff algorithm from blocking the main thread. -/// -/// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's -/// stack machines all the way down! -pub(crate) struct DiffState<'bump> { +pub(crate) struct AsyncDiffState<'bump> { pub(crate) scopes: &'bump ScopeArena, pub(crate) mutations: Mutations<'bump>, - pub(crate) stack: DiffStack<'bump>, pub(crate) force_diff: bool, + pub(crate) element_stack: SmallVec<[ElementId; 10]>, + pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, } -impl<'bump> DiffState<'bump> { - pub(crate) fn new(scopes: &'bump ScopeArena) -> Self { +impl<'b> AsyncDiffState<'b> { + pub fn new(scopes: &'b ScopeArena) -> Self { Self { scopes, mutations: Mutations::new(), - stack: DiffStack::new(), force_diff: false, - } - } -} - -/// The stack instructions we use to diff and create new nodes. -#[derive(Debug)] -pub(crate) enum DiffInstruction<'a> { - Diff { - old: &'a VNode<'a>, - new: &'a VNode<'a>, - }, - - Create { - node: &'a VNode<'a>, - }, - - /// pushes the node elements onto the stack for use in mount - PrepareMove { - node: &'a VNode<'a>, - }, - - Mount { - and: MountType<'a>, - }, - - PopScope, - PopElement, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) enum MountType<'a> { - Absorb, - Append, - Replace { old: &'a VNode<'a> }, - InsertAfter { other_node: &'a VNode<'a> }, - InsertBefore { other_node: &'a VNode<'a> }, -} - -pub(crate) struct DiffStack<'bump> { - pub(crate) instructions: Vec>, - pub(crate) nodes_created_stack: SmallVec<[usize; 10]>, - pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, - pub(crate) element_stack: SmallVec<[ElementId; 10]>, -} - -impl<'bump> DiffStack<'bump> { - fn new() -> Self { - Self { - instructions: Vec::with_capacity(1000), - nodes_created_stack: smallvec![], - scope_stack: smallvec![], element_stack: smallvec![], + scope_stack: smallvec![], } } - fn pop(&mut self) -> Option> { - self.instructions.pop() + pub fn diff_scope(&mut self, scopeid: ScopeId) { + let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); + self.scope_stack.push(scopeid); + let scope = self.scopes.get_scope(scopeid).unwrap(); + self.element_stack.push(scope.container); + self.diff_node(old, new); } - fn pop_off_scope(&mut self) { - self.scope_stack.pop(); - } - - pub(crate) fn push(&mut self, instruction: DiffInstruction<'bump>) { - self.instructions.push(instruction) - } - - fn create_children(&mut self, children: &'bump [VNode<'bump>], and: MountType<'bump>) { - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::Mount { and }); - - for child in children.iter().rev() { - self.instructions - .push(DiffInstruction::Create { node: child }); - } - } - - // todo: subtrees - // fn push_subtree(&mut self) { - // self.nodes_created_stack.push(0); - // self.instructions.push(DiffInstruction::Mount { - // and: MountType::Append, - // }); - // } - - fn push_nodes_created(&mut self, count: usize) { - self.nodes_created_stack.push(count); - } - - pub(crate) fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) { - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::Mount { and }); - self.instructions.push(DiffInstruction::Create { node }); - } - - fn add_child_count(&mut self, count: usize) { - *self.nodes_created_stack.last_mut().unwrap() += count; - } - - fn pop_nodes_created(&mut self) -> usize { - self.nodes_created_stack.pop().unwrap() - } - - pub fn current_scope(&self) -> Option { - self.scope_stack.last().copied() - } - - fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) { - // Push the new scope onto the stack - self.scope_stack.push(idx); - - self.instructions.push(DiffInstruction::PopScope); - - // Run the creation algorithm with this scope on the stack - // ?? I think we treat components as fragments?? - self.instructions.push(DiffInstruction::Create { node }); - } -} - -impl<'bump> DiffState<'bump> { - /// Progress the diffing for this "fiber" - /// - /// This method implements a depth-first iterative tree traversal. - /// - /// We do depth-first to maintain high cache locality (nodes were originally generated recursively). - /// - /// Returns a `bool` indicating that the work completed properly. - pub fn work(&mut self, mut deadline_expired: impl FnMut() -> bool) -> bool { - while let Some(instruction) = self.stack.pop() { - match instruction { - DiffInstruction::Diff { old, new } => self.diff_node(old, new), - DiffInstruction::Create { node } => self.create_node(node), - DiffInstruction::Mount { and } => self.mount(and), - DiffInstruction::PrepareMove { node } => { - let num_on_stack = self.push_all_nodes(node); - self.stack.add_child_count(num_on_stack); - } - DiffInstruction::PopScope => self.stack.pop_off_scope(), - DiffInstruction::PopElement => { - self.stack.element_stack.pop(); - } - }; - - if deadline_expired() { - log::trace!("Deadline expired before we could finish!"); - return false; - } - } - - true - } - - // recursively push all the nodes of a tree onto the stack and return how many are there - fn push_all_nodes(&mut self, node: &'bump VNode<'bump>) -> usize { - match node { - VNode::Text(_) | VNode::Placeholder(_) => { - self.mutations.push_root(node.mounted_id()); - 1 - } - - VNode::Fragment(_) | VNode::Component(_) => { - // - let mut added = 0; - for child in node.children() { - added += self.push_all_nodes(child); - } - added - } - - VNode::Element(el) => { - let mut num_on_stack = 0; - for child in el.children.iter() { - num_on_stack += self.push_all_nodes(child); - } - self.mutations.push_root(el.id.get().unwrap()); - - num_on_stack + 1 - } - } - } - - fn mount(&mut self, and: MountType<'bump>) { - let nodes_created = self.stack.pop_nodes_created(); - match and { - // add the nodes from this virtual list to the parent - // used by fragments and components - MountType::Absorb => { - self.stack.add_child_count(nodes_created); - } - - MountType::Replace { old } => { - self.replace_node(old, nodes_created); - } - - MountType::Append => { - self.mutations.append_children(nodes_created as u32); - // self.mutations.edits.push(AppendChildren { - // many: nodes_created as u32, - // }); - } - - MountType::InsertAfter { other_node } => { - let root = self.find_last_element(other_node).unwrap(); - self.mutations.insert_after(root, nodes_created as u32); - } - - MountType::InsertBefore { other_node } => { - let root = self.find_first_element_id(other_node).unwrap(); - self.mutations.insert_before(root, nodes_created as u32); - } - } - } - - // ================================= - // Tools for creating new nodes - // ================================= - - fn create_node(&mut self, node: &'bump VNode<'bump>) { - match node { - VNode::Text(vtext) => self.create_text_node(vtext, node), - VNode::Placeholder(anchor) => self.create_anchor_node(anchor, node), - VNode::Element(element) => self.create_element_node(element, node), - VNode::Fragment(frag) => self.create_fragment_node(frag), - VNode::Component(component) => self.create_component_node(*component), - } - } - - fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) { - let real_id = self.scopes.reserve_node(node); - - self.mutations.create_text_node(vtext.text, real_id); - vtext.id.set(Some(real_id)); - self.stack.add_child_count(1); - } - - fn create_anchor_node(&mut self, anchor: &'bump VPlaceholder, node: &'bump VNode<'bump>) { - let real_id = self.scopes.reserve_node(node); - - self.mutations.create_placeholder(real_id); - anchor.id.set(Some(real_id)); - - self.stack.add_child_count(1); - } - - fn create_element_node(&mut self, element: &'bump VElement<'bump>, node: &'bump VNode<'bump>) { - let VElement { - tag: tag_name, - listeners, - attributes, - children, - namespace, - id: dom_id, - parent: parent_id, - .. - } = element; - - // set the parent ID for event bubbling - self.stack.instructions.push(DiffInstruction::PopElement); - - let parent = self.stack.element_stack.last().unwrap(); - parent_id.set(Some(*parent)); - - // set the id of the element - let real_id = self.scopes.reserve_node(node); - self.stack.element_stack.push(real_id); - dom_id.set(Some(real_id)); - - self.mutations.create_element(tag_name, *namespace, real_id); - - self.stack.add_child_count(1); - - if let Some(cur_scope_id) = self.stack.current_scope() { - for listener in *listeners { - listener.mounted_node.set(Some(real_id)); - self.mutations.new_event_listener(listener, cur_scope_id); - } - } else { - log::warn!("create element called with no scope on the stack - this is an error for a live dom"); - } - - for attr in *attributes { - self.mutations.set_attribute(attr, real_id.as_u64()); - } - - // todo: the settext optimization - // - // if children.len() == 1 { - // if let VNode::Text(vtext) = children[0] { - // self.mutations.set_text(vtext.text, real_id.as_u64()); - // return; - // } - // } - - if !children.is_empty() { - self.stack.create_children(children, MountType::Append); - } - } - - fn create_fragment_node(&mut self, frag: &'bump VFragment<'bump>) { - self.stack.create_children(frag.children, MountType::Absorb); - } - - fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { - let parent_idx = self.stack.current_scope().unwrap(); - - // the component might already exist - if it does, we need to reuse it - // this makes figure out when to drop the component more complicated - let new_idx = if let Some(idx) = vcomponent.scope.get() { - assert!(self.scopes.get_scope(idx).is_some()); - idx - } else { - // Insert a new scope into our component list - let props: Box = vcomponent.props.borrow_mut().take().unwrap(); - let props: Box = unsafe { std::mem::transmute(props) }; - let new_idx = self.scopes.new_with_key( - vcomponent.user_fc, - props, - Some(parent_idx), - self.stack.element_stack.last().copied().unwrap(), - 0, - ); - - new_idx - }; - - log::info!( - "created component {:?} with parent {:?} and originator {:?}", - new_idx, - parent_idx, - vcomponent.originator - ); - - // Actually initialize the caller's slot with the right address - vcomponent.scope.set(Some(new_idx)); - - match vcomponent.can_memoize { - true => { - // todo: implement promotion logic. save us from boxing props that we don't need - } - false => { - // track this component internally so we know the right drop order - } - } - - // Run the scope for one iteration to initialize it - self.scopes.run_scope(new_idx); - - // Take the node that was just generated from running the component - let nextnode = self.scopes.fin_head(new_idx); - self.stack.create_component(new_idx, nextnode); - - // Finally, insert this scope as a seen node. - self.mutations.mark_dirty_scope(new_idx); - // self.mutations.dirty_scopes.insert(new_idx); - } - - // ================================= - // Tools for diffing nodes - // ================================= - - pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { + pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) { use VNode::*; match (old_node, new_node) { // Check the most common cases first @@ -539,28 +89,157 @@ impl<'bump> DiffState<'bump> { // The normal pathway still works, but generates slightly weird instructions // This pathway ensures uses the ReplaceAll, not the InsertAfter and remove - (Placeholder(_), Fragment(new)) => { + (Placeholder(_), Fragment(_)) => { log::debug!("replacing placeholder with fragment {:?}", new_node); - self.stack - .create_children(new.children, MountType::Replace { old: old_node }); + self.replace_node(old_node, new_node); } // Anything else is just a basic replace and create ( Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), - ) => self - .stack - .create_node(new_node, MountType::Replace { old: old_node }), + ) => self.replace_node(old_node, new_node), } } + pub fn create_node(&mut self, node: &'b VNode<'b>) -> usize { + match node { + VNode::Text(vtext) => self.create_text_node(vtext, node), + VNode::Placeholder(anchor) => self.create_anchor_node(anchor, node), + VNode::Element(element) => self.create_element_node(element, node), + VNode::Fragment(frag) => self.create_fragment_node(frag), + VNode::Component(component) => self.create_component_node(*component), + } + } + + fn create_text_node(&mut self, vtext: &'b VText<'b>, node: &'b VNode<'b>) -> usize { + let real_id = self.scopes.reserve_node(node); + self.mutations.create_text_node(vtext.text, real_id); + vtext.id.set(Some(real_id)); + + 1 + } + + fn create_anchor_node(&mut self, anchor: &'b VPlaceholder, node: &'b VNode<'b>) -> usize { + let real_id = self.scopes.reserve_node(node); + self.mutations.create_placeholder(real_id); + anchor.id.set(Some(real_id)); + + 1 + } + + fn create_element_node(&mut self, element: &'b VElement<'b>, node: &'b VNode<'b>) -> usize { + let VElement { + tag: tag_name, + listeners, + attributes, + children, + namespace, + id: dom_id, + parent: parent_id, + .. + } = element; + + // set the parent ID for event bubbling + // self.stack.instructions.push(DiffInstruction::PopElement); + + let parent = self.element_stack.last().unwrap(); + parent_id.set(Some(*parent)); + + // set the id of the element + let real_id = self.scopes.reserve_node(node); + self.element_stack.push(real_id); + dom_id.set(Some(real_id)); + + self.mutations.create_element(tag_name, *namespace, real_id); + + if let Some(cur_scope_id) = self.current_scope() { + for listener in *listeners { + listener.mounted_node.set(Some(real_id)); + self.mutations.new_event_listener(listener, cur_scope_id); + } + } else { + log::warn!("create element called with no scope on the stack - this is an error for a live dom"); + } + + for attr in *attributes { + self.mutations.set_attribute(attr, real_id.as_u64()); + } + + if !children.is_empty() { + self.create_and_append_children(children); + } + + 1 + } + + fn create_fragment_node(&mut self, frag: &'b VFragment<'b>) -> usize { + self.create_children(frag.children) + } + + fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize { + let parent_idx = self.current_scope().unwrap(); + + // the component might already exist - if it does, we need to reuse it + // this makes figure out when to drop the component more complicated + let new_idx = if let Some(idx) = vcomponent.scope.get() { + assert!(self.scopes.get_scope(idx).is_some()); + idx + } else { + // Insert a new scope into our component list + let props: Box = vcomponent.props.borrow_mut().take().unwrap(); + let props: Box = unsafe { std::mem::transmute(props) }; + let new_idx = self.scopes.new_with_key( + vcomponent.user_fc, + props, + Some(parent_idx), + self.element_stack.last().copied().unwrap(), + 0, + ); + + new_idx + }; + + log::info!( + "created component {:?} with parent {:?} and originator {:?}", + new_idx, + parent_idx, + vcomponent.originator + ); + + // Actually initialize the caller's slot with the right address + vcomponent.scope.set(Some(new_idx)); + + match vcomponent.can_memoize { + true => { + // todo: implement promotion logic. save us from boxing props that we don't need + } + false => { + // track this component internally so we know the right drop order + } + } + + self.enter_scope(new_idx); + + // Run the scope for one iteration to initialize it + self.scopes.run_scope(new_idx); + self.mutations.mark_dirty_scope(new_idx); + + // Take the node that was just generated from running the component + let nextnode = self.scopes.fin_head(new_idx); + let created = self.create_node(nextnode); + + self.leave_scope(); + + created + } + fn diff_element_nodes( &mut self, - old: &'bump VElement<'bump>, - new: &'bump VElement<'bump>, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, + old: &'b VElement<'b>, + new: &'b VElement<'b>, + old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, ) { let root = old.id.get().unwrap(); @@ -569,13 +248,7 @@ impl<'bump> DiffState<'bump> { // // This case is rather rare (typically only in non-keyed lists) if new.tag != old.tag || new.namespace != old.namespace { - // maybe make this an instruction? - // issue is that we need the "vnode" but this method only has the velement - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::Replace { old: old_node }, - }); - self.create_element_node(new, new_node); + self.replace_node(old_node, new_node); return; } @@ -618,7 +291,7 @@ impl<'bump> DiffState<'bump> { // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) // // TODO: take a more efficient path than this - if let Some(cur_scope_id) = self.stack.current_scope() { + if let Some(cur_scope_id) = self.current_scope() { if old.listeners.len() == new.listeners.len() { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { if old_l.event != new_l.event { @@ -640,87 +313,24 @@ impl<'bump> DiffState<'bump> { } } - if old.children.is_empty() && !new.children.is_empty() { - self.mutations.push_root(root); - // self.mutations.edits.push(PushRoot { - // root: root.as_u64(), - // }); - self.stack.element_stack.push(root); - self.stack.instructions.push(DiffInstruction::PopElement); - self.stack.create_children(new.children, MountType::Append); - } else { - self.stack.element_stack.push(root); - self.stack.instructions.push(DiffInstruction::PopElement); - self.diff_children(old.children, new.children); - } - - // todo: this is for the "settext" optimization - // it works, but i'm not sure if it's the direction we want to take right away - // I haven't benchmarked the performance imporvemenet yet. Perhaps - // we can make it a config? - - // match (old.children.len(), new.children.len()) { - // (0, 0) => {} - // (1, 1) => { - // let old1 = &old.children[0]; - // let new1 = &new.children[0]; - - // match (old1, new1) { - // (VNode::Text(old_text), VNode::Text(new_text)) => { - // if old_text.text != new_text.text { - // self.mutations.set_text(new_text.text, root.as_u64()); - // } - // } - // (VNode::Text(_old_text), _) => { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.stack.create_node(new1, MountType::Append); - // } - // (_, VNode::Text(new_text)) => { - // self.remove_nodes([old1], false); - // self.mutations.set_text(new_text.text, root.as_u64()); - // } - // _ => { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.diff_children(old.children, new.children); - // } - // } - // } - // (0, 1) => { - // if let VNode::Text(text) = &new.children[0] { - // self.mutations.set_text(text.text, root.as_u64()); - // } else { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // } - // } - // (0, _) => { - // self.mutations.edits.push(PushRoot { - // root: root.as_u64(), - // }); - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.stack.create_children(new.children, MountType::Append); - // } - // (_, 0) => { - // self.remove_nodes(old.children, false); - // self.mutations.set_text("", root.as_u64()); - // } - // (_, _) => { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.diff_children(old.children, new.children); - // } - // } + match (old.children.len(), new.children.len()) { + (0, 0) => {} + (0, _) => { + let created = self.create_children(new.children); + self.mutations.append_children(created as u32); + } + (_, _) => { + self.diff_children(old.children, new.children); + } + }; } fn diff_component_nodes( &mut self, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, - old: &'bump VComponent<'bump>, - new: &'bump VComponent<'bump>, + old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + old: &'b VComponent<'b>, + new: &'b VComponent<'b>, ) { let scope_addr = old.scope.get().unwrap(); log::trace!( @@ -736,7 +346,7 @@ impl<'bump> DiffState<'bump> { // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { - self.stack.scope_stack.push(scope_addr); + self.enter_scope(scope_addr); // Make sure the new component vnode is referencing the right scope id new.scope.set(Some(scope_addr)); @@ -786,16 +396,14 @@ impl<'bump> DiffState<'bump> { drop(new_props); }; - self.stack.scope_stack.pop(); + self.leave_scope(); } else { - // - log::debug!("scope stack is {:#?}", self.stack.scope_stack); - self.stack - .create_node(new_node, MountType::Replace { old: old_node }); + log::debug!("scope stack is {:#?}", self.scope_stack); + self.replace_node(old_node, new_node); } } - fn diff_fragment_nodes(&mut self, old: &'bump VFragment<'bump>, new: &'bump VFragment<'bump>) { + fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) { // This is the case where options or direct vnodes might be used. // In this case, it's faster to just skip ahead to their diff if old.children.len() == 1 && new.children.len() == 1 { @@ -828,15 +436,12 @@ impl<'bump> DiffState<'bump> { // // Fragment nodes cannot generate empty children lists, so we can assume that when a list is empty, it belongs only // to an element, and appending makes sense. - fn diff_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { // Remember, fragments can never be empty (they always have a single child) match (old, new) { ([], []) => {} - ([], _) => self.stack.create_children(new, MountType::Append), - (_, []) => { - log::debug!("removing nodes {:?}", old); - self.remove_nodes(old, true) - } + ([], _) => self.create_and_append_children(new), + (_, []) => self.remove_nodes(old, true), _ => { let new_is_keyed = new[0].key().is_some(); let old_is_keyed = old[0].key().is_some(); @@ -867,29 +472,20 @@ impl<'bump> DiffState<'bump> { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { // Handled these cases in `diff_children` before calling this function. debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); - for (new, old) in new.iter().zip(old.iter()).rev() { - self.stack.push(DiffInstruction::Diff { new, old }); - } - use std::cmp::Ordering; match old.len().cmp(&new.len()) { Ordering::Greater => self.remove_nodes(&old[new.len()..], true), - Ordering::Less => { - self.stack.create_children( - &new[old.len()..], - MountType::InsertAfter { - other_node: old.last().unwrap(), - }, - ); - } - Ordering::Equal => { - // nothing - they're the same size - } + Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), + Ordering::Equal => {} + } + + for (new, old) in new.iter().zip(old.iter()) { + self.diff_node(old, new); } } @@ -909,10 +505,10 @@ impl<'bump> DiffState<'bump> { // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 // // The stack is empty upon entry. - fn diff_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { if cfg!(debug_assertions) { let mut keys = fxhash::FxHashSet::default(); - let mut assert_unique_keys = |children: &'bump [VNode<'bump>]| { + let mut assert_unique_keys = |children: &'b [VNode<'b>]| { keys.clear(); for child in children { let key = child.key(); @@ -953,6 +549,7 @@ impl<'bump> DiffState<'bump> { !((old_middle.len() == new_middle.len()) && old_middle.is_empty()), "keyed children must have the same number of children" ); + if new_middle.is_empty() { // remove the old elements self.remove_nodes(old_middle, true); @@ -962,30 +559,15 @@ impl<'bump> DiffState<'bump> { if left_offset == 0 { // insert at the beginning of the old list let foothold = &old[old.len() - right_offset]; - self.stack.create_children( - new_middle, - MountType::InsertBefore { - other_node: foothold, - }, - ); + self.create_and_insert_before(new_middle, foothold); } else if right_offset == 0 { // insert at the end the old list let foothold = old.last().unwrap(); - self.stack.create_children( - new_middle, - MountType::InsertAfter { - other_node: foothold, - }, - ); + self.create_and_insert_after(new_middle, foothold); } else { // inserting in the middle let foothold = &old[left_offset - 1]; - self.stack.create_children( - new_middle, - MountType::InsertAfter { - other_node: foothold, - }, - ); + self.create_and_insert_after(new_middle, foothold); } } else { self.diff_keyed_middle(old_middle, new_middle); @@ -999,9 +581,8 @@ impl<'bump> DiffState<'bump> { /// If there is no offset, then this function returns None and the diffing is complete. fn diff_keyed_ends( &mut self, - - old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], ) -> Option<(usize, usize)> { let mut left_offset = 0; @@ -1010,19 +591,14 @@ impl<'bump> DiffState<'bump> { if old.key() != new.key() { break; } - self.stack.push(DiffInstruction::Diff { old, new }); + self.diff_node(old, new); left_offset += 1; } // If that was all of the old children, then create and append the remaining // new children and we're finished. if left_offset == old.len() { - self.stack.create_children( - &new[left_offset..], - MountType::InsertAfter { - other_node: old.last().unwrap(), - }, - ); + self.create_and_insert_after(&new[left_offset..], old.last().unwrap()); return None; } @@ -1060,7 +636,7 @@ impl<'bump> DiffState<'bump> { // This function will load the appropriate nodes onto the stack and do diffing in place. // // Upon exit from this function, it will be restored to that same self. - fn diff_keyed_middle(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { /* 1. Map the old keys into a numerical ordering based on indices. 2. Create a map of old key to its index @@ -1118,10 +694,12 @@ impl<'bump> DiffState<'bump> { if shared_keys.is_empty() { if let Some(first_old) = old.get(0) { self.remove_nodes(&old[1..], true); - self.stack - .create_children(new, MountType::Replace { old: first_old }) + let nodes_created = self.create_children(new); + self.replace_inner(first_old, nodes_created); } else { - self.stack.create_children(new, MountType::Append {}); + // I think this is wrong - why are we appending? + // only valid of the if there are no trailing elements + self.create_and_append_children(new); } return; } @@ -1149,33 +727,31 @@ impl<'bump> DiffState<'bump> { lis_sequence.pop(); } - let apply = |new_idx, new_node: &'bump VNode<'bump>, stack: &mut DiffStack<'bump>| { - let old_index = new_index_to_old_index[new_idx]; - if old_index == u32::MAX as usize { - stack.create_node(new_node, MountType::Absorb); - } else { - // this function should never take LIS indices - stack.push(DiffInstruction::PrepareMove { node: new_node }); - stack.push(DiffInstruction::Diff { - new: new_node, - old: &old[old_index], - }); - } - }; + for idx in lis_sequence.iter() { + self.diff_node(&old[new_index_to_old_index[*idx]], &new[*idx]); + } - // add mount instruction for the last items not covered by the lis - let first_lis = *lis_sequence.first().unwrap(); - if first_lis > 0 { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertBefore { - other_node: &new[first_lis], - }, - }); + let mut nodes_created = 0; - for (idx, new_node) in new[..first_lis].iter().enumerate().rev() { - apply(idx, new_node, &mut self.stack); + // add mount instruction for the first items not covered by the lis + let last = *lis_sequence.last().unwrap(); + if last < (new.len() - 1) { + for (idx, new_node) in new[(last + 1)..].iter().enumerate() { + let new_idx = idx + last + 1; + let old_index = new_index_to_old_index[new_idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } } + + self.mutations.insert_after( + self.find_last_element(&new[last]).unwrap(), + nodes_created as u32, + ); + nodes_created = 0; } // for each spacing, generate a mount instruction @@ -1183,46 +759,185 @@ impl<'bump> DiffState<'bump> { let mut last = *lis_iter.next().unwrap(); for next in lis_iter { if last - next > 1 { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertBefore { - other_node: &new[last], - }, - }); - for (idx, new_node) in new[(next + 1)..last].iter().enumerate().rev() { - apply(idx + next + 1, new_node, &mut self.stack); + for (idx, new_node) in new[(next + 1)..last].iter().enumerate() { + let new_idx = idx + next + 1; + + let old_index = new_index_to_old_index[new_idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } } + + self.mutations.insert_before( + self.find_first_element(&new[last]).unwrap(), + nodes_created as u32, + ); + + nodes_created = 0; } last = *next; } - // add mount instruction for the first items not covered by the lis - let last = *lis_sequence.last().unwrap(); - if last < (new.len() - 1) { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertAfter { - other_node: &new[last], - }, - }); - for (idx, new_node) in new[(last + 1)..].iter().enumerate().rev() { - apply(idx + last + 1, new_node, &mut self.stack); + // add mount instruction for the last items not covered by the lis + let first_lis = *lis_sequence.first().unwrap(); + if first_lis > 0 { + for (idx, new_node) in new[..first_lis].iter().enumerate() { + let old_index = new_index_to_old_index[idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } } - } - for idx in lis_sequence.iter().rev() { - self.stack.push(DiffInstruction::Diff { - new: &new[*idx], - old: &old[new_index_to_old_index[*idx]], - }); + self.mutations.insert_before( + self.find_first_element(&new[first_lis]).unwrap(), + nodes_created as u32, + ); } } - // ===================== - // Utilities - // ===================== + fn replace_node(&mut self, old: &'b VNode<'b>, new: &'b VNode<'b>) { + log::debug!("Replacing node\n old: {:?}\n new: {:?}", old, new); + let nodes_created = self.create_node(new); + self.replace_inner(old, nodes_created); + } - fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> Option { + fn replace_inner(&mut self, old: &'b VNode<'b>, nodes_created: usize) { + match old { + VNode::Element(el) => { + let id = old + .try_mounted_id() + .unwrap_or_else(|| panic!("broke on {:?}", old)); + + self.mutations.replace_with(id, nodes_created as u32); + self.remove_nodes(el.children, false); + self.scopes.collect_garbage(id); + } + + VNode::Text(_) | VNode::Placeholder(_) => { + let id = old + .try_mounted_id() + .unwrap_or_else(|| panic!("broke on {:?}", old)); + + self.mutations.replace_with(id, nodes_created as u32); + self.scopes.collect_garbage(id); + } + + VNode::Fragment(f) => { + self.replace_inner(&f.children[0], nodes_created); + self.remove_nodes(f.children.iter().skip(1), true); + } + + VNode::Component(c) => { + let node = self.scopes.fin_head(c.scope.get().unwrap()); + self.replace_inner(node, nodes_created); + + let scope_id = c.scope.get().unwrap(); + + // we can only remove components if they are actively being diffed + if self.scope_stack.contains(&c.originator) { + log::debug!("Removing component {:?}", old); + + self.scopes.try_remove(scope_id).unwrap(); + } + } + } + } + + pub fn remove_nodes(&mut self, nodes: impl IntoIterator>, gen_muts: bool) { + for node in nodes { + match node { + VNode::Text(t) => { + // this check exists because our null node will be removed but does not have an ID + if let Some(id) = t.id.get() { + self.scopes.collect_garbage(id); + + if gen_muts { + self.mutations.remove(id.as_u64()); + } + } + } + VNode::Placeholder(a) => { + let id = a.id.get().unwrap(); + self.scopes.collect_garbage(id); + + if gen_muts { + self.mutations.remove(id.as_u64()); + } + } + VNode::Element(e) => { + let id = e.id.get().unwrap(); + + if gen_muts { + self.mutations.remove(id.as_u64()); + } + + self.scopes.collect_garbage(id); + + self.remove_nodes(e.children, false); + } + + VNode::Fragment(f) => { + self.remove_nodes(f.children, gen_muts); + } + + VNode::Component(c) => { + let scope_id = c.scope.get().unwrap(); + let root = self.scopes.root_node(scope_id); + self.remove_nodes([root], gen_muts); + + // we can only remove this node if the originator is actively + if self.scope_stack.contains(&c.originator) { + self.scopes.try_remove(scope_id).unwrap(); + } + } + } + } + } + + fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { + let mut created = 0; + for node in nodes { + created += self.create_node(node); + } + created + } + + fn create_and_append_children(&mut self, nodes: &'b [VNode<'b>]) { + let created = self.create_children(nodes); + self.mutations.append_children(created as u32); + } + + fn create_and_insert_after(&mut self, nodes: &'b [VNode<'b>], after: &'b VNode<'b>) { + let created = self.create_children(nodes); + let last = self.find_last_element(after).unwrap(); + self.mutations.insert_after(last, created as u32); + } + + fn create_and_insert_before(&mut self, nodes: &'b [VNode<'b>], before: &'b VNode<'b>) { + let created = self.create_children(nodes); + let first = self.find_first_element(before).unwrap(); + self.mutations.insert_before(first, created as u32); + } + + fn current_scope(&self) -> Option { + self.scope_stack.last().copied() + } + + fn enter_scope(&mut self, scope: ScopeId) { + self.scope_stack.push(scope); + } + + fn leave_scope(&mut self) { + self.scope_stack.pop(); + } + + fn find_last_element(&self, vnode: &'b VNode<'b>) -> Option { let mut search_node = Some(vnode); loop { @@ -1241,7 +956,7 @@ impl<'bump> DiffState<'bump> { } } - fn find_first_element_id(&mut self, vnode: &'bump VNode<'bump>) -> Option { + fn find_first_element(&self, vnode: &'b VNode<'b>) -> Option { let mut search_node = Some(vnode); loop { @@ -1261,100 +976,31 @@ impl<'bump> DiffState<'bump> { } } - fn replace_node(&mut self, old: &'bump VNode<'bump>, nodes_created: usize) { - log::debug!("Replacing node {:?}", old); - match old { - VNode::Element(el) => { - let id = old - .try_mounted_id() - .unwrap_or_else(|| panic!("broke on {:?}", old)); - - self.mutations.replace_with(id, nodes_created as u32); - self.remove_nodes(el.children, false); - } - + // recursively push all the nodes of a tree onto the stack and return how many are there + fn push_all_nodes(&mut self, node: &'b VNode<'b>) -> usize { + match node { VNode::Text(_) | VNode::Placeholder(_) => { - let id = old - .try_mounted_id() - .unwrap_or_else(|| panic!("broke on {:?}", old)); - - self.mutations.replace_with(id, nodes_created as u32); + self.mutations.push_root(node.mounted_id()); + 1 } - VNode::Fragment(f) => { - self.replace_node(&f.children[0], nodes_created); - self.remove_nodes(f.children.iter().skip(1), true); + VNode::Fragment(_) | VNode::Component(_) => { + // + let mut added = 0; + for child in node.children() { + added += self.push_all_nodes(child); + } + added } - VNode::Component(c) => { - let node = self.scopes.fin_head(c.scope.get().unwrap()); - self.replace_node(node, nodes_created); - - let scope_id = c.scope.get().unwrap(); - - // we can only remove components if they are actively being diffed - if self.stack.scope_stack.contains(&c.originator) { - self.scopes.try_remove(scope_id).unwrap(); + VNode::Element(el) => { + let mut num_on_stack = 0; + for child in el.children.iter() { + num_on_stack += self.push_all_nodes(child); } - } - } - } + self.mutations.push_root(el.id.get().unwrap()); - /// schedules nodes for garbage collection and pushes "remove" to the mutation stack - /// remove can happen whenever - pub(crate) fn remove_nodes( - &mut self, - nodes: impl IntoIterator>, - gen_muts: bool, - ) { - // or cache the vec on the diff machine - for node in nodes { - log::debug!("removing {:?}", node); - match node { - VNode::Text(t) => { - // this check exists because our null node will be removed but does not have an ID - if let Some(id) = t.id.get() { - // self.scopes.collect_garbage(id); - - if gen_muts { - self.mutations.remove(id.as_u64()); - } - } - } - VNode::Placeholder(a) => { - let id = a.id.get().unwrap(); - // self.scopes.collect_garbage(id); - - if gen_muts { - self.mutations.remove(id.as_u64()); - } - } - VNode::Element(e) => { - let id = e.id.get().unwrap(); - - if gen_muts { - self.mutations.remove(id.as_u64()); - } - - self.remove_nodes(e.children, false); - - // self.scopes.collect_garbage(id); - } - - VNode::Fragment(f) => { - self.remove_nodes(f.children, gen_muts); - } - - VNode::Component(c) => { - let scope_id = c.scope.get().unwrap(); - let root = self.scopes.root_node(scope_id); - self.remove_nodes(Some(root), gen_muts); - - // we can only remove this node if the originator is actively - if self.stack.scope_stack.contains(&c.originator) { - self.scopes.try_remove(scope_id).unwrap(); - } - } + num_on_stack + 1 } } } diff --git a/packages/core/src/diff_async.rs b/packages/core/src/diff_async.rs deleted file mode 100644 index e5d63ffb5..000000000 --- a/packages/core/src/diff_async.rs +++ /dev/null @@ -1,1102 +0,0 @@ -use crate::innerlude::*; -use fxhash::{FxHashMap, FxHashSet}; -use smallvec::{smallvec, SmallVec}; - -pub(crate) struct AsyncDiffState<'bump> { - pub(crate) scopes: &'bump ScopeArena, - pub(crate) mutations: Mutations<'bump>, - pub(crate) force_diff: bool, - pub(crate) element_stack: SmallVec<[ElementId; 10]>, - pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, -} - -impl<'b> AsyncDiffState<'b> { - pub fn new(scopes: &'b ScopeArena) -> Self { - Self { - scopes, - mutations: Mutations::new(), - force_diff: false, - element_stack: smallvec![], - scope_stack: smallvec![], - } - } - - pub fn diff_scope(&mut self, scopeid: ScopeId) { - let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); - self.scope_stack.push(scopeid); - let scope = self.scopes.get_scope(scopeid).unwrap(); - self.element_stack.push(scope.container); - self.diff_node(old, new); - } - - pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) { - use VNode::*; - match (old_node, new_node) { - // Check the most common cases first - // these are *actual* elements, not wrappers around lists - (Text(old), Text(new)) => { - if std::ptr::eq(old, new) { - log::trace!("skipping node diff - text are the sames"); - return; - } - - if let Some(root) = old.id.get() { - if old.text != new.text { - self.mutations.set_text(new.text, root.as_u64()); - } - self.scopes.update_node(new_node, root); - - new.id.set(Some(root)); - } - } - - (Placeholder(old), Placeholder(new)) => { - if std::ptr::eq(old, new) { - log::trace!("skipping node diff - placeholder are the sames"); - return; - } - - if let Some(root) = old.id.get() { - self.scopes.update_node(new_node, root); - new.id.set(Some(root)) - } - } - - (Element(old), Element(new)) => { - if std::ptr::eq(old, new) { - log::trace!("skipping node diff - element are the sames"); - return; - } - self.diff_element_nodes(old, new, old_node, new_node) - } - - // These two sets are pointers to nodes but are not actually nodes themselves - (Component(old), Component(new)) => { - if std::ptr::eq(old, new) { - log::trace!("skipping node diff - placeholder are the sames"); - return; - } - self.diff_component_nodes(old_node, new_node, *old, *new) - } - - (Fragment(old), Fragment(new)) => { - if std::ptr::eq(old, new) { - log::trace!("skipping node diff - fragment are the sames"); - return; - } - self.diff_fragment_nodes(old, new) - } - - // The normal pathway still works, but generates slightly weird instructions - // This pathway ensures uses the ReplaceAll, not the InsertAfter and remove - (Placeholder(_), Fragment(_)) => { - log::debug!("replacing placeholder with fragment {:?}", new_node); - self.replace_node(old_node, new_node); - } - - // Anything else is just a basic replace and create - ( - Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), - Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), - ) => self.replace_node(old_node, new_node), - } - } - - pub fn create_node(&mut self, node: &'b VNode<'b>) -> usize { - match node { - VNode::Text(vtext) => self.create_text_node(vtext, node), - VNode::Placeholder(anchor) => self.create_anchor_node(anchor, node), - VNode::Element(element) => self.create_element_node(element, node), - VNode::Fragment(frag) => self.create_fragment_node(frag), - VNode::Component(component) => self.create_component_node(*component), - } - } - - fn create_text_node(&mut self, vtext: &'b VText<'b>, node: &'b VNode<'b>) -> usize { - let real_id = self.scopes.reserve_node(node); - self.mutations.create_text_node(vtext.text, real_id); - vtext.id.set(Some(real_id)); - - 1 - } - - fn create_anchor_node(&mut self, anchor: &'b VPlaceholder, node: &'b VNode<'b>) -> usize { - let real_id = self.scopes.reserve_node(node); - self.mutations.create_placeholder(real_id); - anchor.id.set(Some(real_id)); - - 1 - } - - fn create_element_node(&mut self, element: &'b VElement<'b>, node: &'b VNode<'b>) -> usize { - let VElement { - tag: tag_name, - listeners, - attributes, - children, - namespace, - id: dom_id, - parent: parent_id, - .. - } = element; - - // set the parent ID for event bubbling - // self.stack.instructions.push(DiffInstruction::PopElement); - - let parent = self.element_stack.last().unwrap(); - parent_id.set(Some(*parent)); - - // set the id of the element - let real_id = self.scopes.reserve_node(node); - self.element_stack.push(real_id); - dom_id.set(Some(real_id)); - - self.mutations.create_element(tag_name, *namespace, real_id); - - if let Some(cur_scope_id) = self.current_scope() { - for listener in *listeners { - listener.mounted_node.set(Some(real_id)); - self.mutations.new_event_listener(listener, cur_scope_id); - } - } else { - log::warn!("create element called with no scope on the stack - this is an error for a live dom"); - } - - for attr in *attributes { - self.mutations.set_attribute(attr, real_id.as_u64()); - } - - if !children.is_empty() { - self.create_and_append_children(children); - } - - 1 - } - - fn create_fragment_node(&mut self, frag: &'b VFragment<'b>) -> usize { - self.create_children(frag.children) - } - - fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize { - let parent_idx = self.current_scope().unwrap(); - - // the component might already exist - if it does, we need to reuse it - // this makes figure out when to drop the component more complicated - let new_idx = if let Some(idx) = vcomponent.scope.get() { - assert!(self.scopes.get_scope(idx).is_some()); - idx - } else { - // Insert a new scope into our component list - let props: Box = vcomponent.props.borrow_mut().take().unwrap(); - let props: Box = unsafe { std::mem::transmute(props) }; - let new_idx = self.scopes.new_with_key( - vcomponent.user_fc, - props, - Some(parent_idx), - self.element_stack.last().copied().unwrap(), - 0, - ); - - new_idx - }; - - log::info!( - "created component {:?} with parent {:?} and originator {:?}", - new_idx, - parent_idx, - vcomponent.originator - ); - - // Actually initialize the caller's slot with the right address - vcomponent.scope.set(Some(new_idx)); - - match vcomponent.can_memoize { - true => { - // todo: implement promotion logic. save us from boxing props that we don't need - } - false => { - // track this component internally so we know the right drop order - } - } - - // Run the scope for one iteration to initialize it - self.scopes.run_scope(new_idx); - - self.mutations.mark_dirty_scope(new_idx); - - // self.stack.create_component(new_idx, nextnode); - // Finally, insert this scope as a seen node. - - // Take the node that was just generated from running the component - let nextnode = self.scopes.fin_head(new_idx); - self.create_node(nextnode) - } - - fn diff_element_nodes( - &mut self, - old: &'b VElement<'b>, - new: &'b VElement<'b>, - old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - ) { - let root = old.id.get().unwrap(); - - // If the element type is completely different, the element needs to be re-rendered completely - // This is an optimization React makes due to how users structure their code - // - // This case is rather rare (typically only in non-keyed lists) - if new.tag != old.tag || new.namespace != old.namespace { - self.replace_node(old_node, new_node); - return; - } - - self.scopes.update_node(new_node, root); - - new.id.set(Some(root)); - new.parent.set(old.parent.get()); - - // todo: attributes currently rely on the element on top of the stack, but in theory, we only need the id of the - // element to modify its attributes. - // it would result in fewer instructions if we just set the id directly. - // it would also clean up this code some, but that's not very important anyways - - // Diff Attributes - // - // It's extraordinarily rare to have the number/order of attributes change - // In these cases, we just completely erase the old set and make a new set - // - // TODO: take a more efficient path than this - if old.attributes.len() == new.attributes.len() { - for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { - if old_attr.value != new_attr.value || new_attr.is_volatile { - self.mutations.set_attribute(new_attr, root.as_u64()); - } - } - } else { - for attribute in old.attributes { - self.mutations.remove_attribute(attribute, root.as_u64()); - } - for attribute in new.attributes { - self.mutations.set_attribute(attribute, root.as_u64()) - } - } - - // Diff listeners - // - // It's extraordinarily rare to have the number/order of listeners change - // In the cases where the listeners change, we completely wipe the data attributes and add new ones - // - // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) - // - // TODO: take a more efficient path than this - if let Some(cur_scope_id) = self.current_scope() { - if old.listeners.len() == new.listeners.len() { - for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { - if old_l.event != new_l.event { - self.mutations - .remove_event_listener(old_l.event, root.as_u64()); - self.mutations.new_event_listener(new_l, cur_scope_id); - } - new_l.mounted_node.set(old_l.mounted_node.get()); - } - } else { - for listener in old.listeners { - self.mutations - .remove_event_listener(listener.event, root.as_u64()); - } - for listener in new.listeners { - listener.mounted_node.set(Some(root)); - self.mutations.new_event_listener(listener, cur_scope_id); - } - } - } - - match (old.children.len(), new.children.len()) { - (0, 0) => {} - (0, _) => { - let created = self.create_children(new.children); - self.mutations.append_children(created as u32); - } - (_, _) => { - self.diff_children(old.children, new.children); - } - }; - } - - fn diff_component_nodes( - &mut self, - old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - old: &'b VComponent<'b>, - new: &'b VComponent<'b>, - ) { - let scope_addr = old.scope.get().unwrap(); - log::trace!( - "diff_component_nodes. old: {:#?} new: {:#?}", - old_node, - new_node - ); - - if std::ptr::eq(old, new) { - log::trace!("skipping component diff - component is the sames"); - return; - } - - // Make sure we're dealing with the same component (by function pointer) - if old.user_fc == new.user_fc { - self.enter_scope(scope_addr); - - // Make sure the new component vnode is referencing the right scope id - new.scope.set(Some(scope_addr)); - - // make sure the component's caller function is up to date - let scope = self - .scopes - .get_scope(scope_addr) - .unwrap_or_else(|| panic!("could not find {:?}", scope_addr)); - - // take the new props out regardless - // when memoizing, push to the existing scope if memoization happens - let new_props = new.props.borrow_mut().take().unwrap(); - - let should_run = { - if old.can_memoize { - let props_are_the_same = unsafe { - scope - .props - .borrow() - .as_ref() - .unwrap() - .memoize(new_props.as_ref()) - }; - !props_are_the_same || self.force_diff - } else { - true - } - }; - - if should_run { - let _old_props = scope - .props - .replace(unsafe { std::mem::transmute(Some(new_props)) }); - - // this should auto drop the previous props - self.scopes.run_scope(scope_addr); - self.mutations.mark_dirty_scope(scope_addr); - - self.diff_node( - self.scopes.wip_head(scope_addr), - self.scopes.fin_head(scope_addr), - ); - } else { - log::trace!("memoized"); - // memoization has taken place - drop(new_props); - }; - - self.leave_scope(); - } else { - log::debug!("scope stack is {:#?}", self.scope_stack); - self.replace_node(old_node, new_node); - } - } - - fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) { - // This is the case where options or direct vnodes might be used. - // In this case, it's faster to just skip ahead to their diff - if old.children.len() == 1 && new.children.len() == 1 { - self.diff_node(&old.children[0], &new.children[0]); - return; - } - - debug_assert!(!old.children.is_empty()); - debug_assert!(!new.children.is_empty()); - - self.diff_children(old.children, new.children); - } - - // ============================================= - // Utilities for creating new diff instructions - // ============================================= - - // Diff the given set of old and new children. - // - // The parent must be on top of the change list stack when this function is - // entered: - // - // [... parent] - // - // the change list stack is in the same state when this function returns. - // - // If old no anchors are provided, then it's assumed that we can freely append to the parent. - // - // Remember, non-empty lists does not mean that there are real elements, just that there are virtual elements. - // - // Fragment nodes cannot generate empty children lists, so we can assume that when a list is empty, it belongs only - // to an element, and appending makes sense. - fn diff_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { - // Remember, fragments can never be empty (they always have a single child) - match (old, new) { - ([], []) => {} - ([], _) => self.create_and_append_children(new), - (_, []) => self.remove_nodes(old, true), - _ => { - let new_is_keyed = new[0].key().is_some(); - let old_is_keyed = old[0].key().is_some(); - - debug_assert!( - new.iter().all(|n| n.key().is_some() == new_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - debug_assert!( - old.iter().all(|o| o.key().is_some() == old_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - - if new_is_keyed && old_is_keyed { - self.diff_keyed_children(old, new); - } else { - self.diff_non_keyed_children(old, new); - } - } - } - } - - // Diff children that are not keyed. - // - // The parent must be on the top of the change list stack when entering this - // function: - // - // [... parent] - // - // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { - // Handled these cases in `diff_children` before calling this function. - debug_assert!(!new.is_empty()); - debug_assert!(!old.is_empty()); - - use std::cmp::Ordering; - match old.len().cmp(&new.len()) { - Ordering::Greater => self.remove_nodes(&old[new.len()..], true), - Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), - Ordering::Equal => {} - } - - for (new, old) in new.iter().zip(old.iter()) { - self.diff_node(old, new); - } - } - - // Diffing "keyed" children. - // - // With keyed children, we care about whether we delete, move, or create nodes - // versus mutate existing nodes in place. Presumably there is some sort of CSS - // transition animation that makes the virtual DOM diffing algorithm - // observable. By specifying keys for nodes, we know which virtual DOM nodes - // must reuse (or not reuse) the same physical DOM nodes. - // - // This is loosely based on Inferno's keyed patching implementation. However, we - // have to modify the algorithm since we are compiling the diff down into change - // list instructions that will be executed later, rather than applying the - // changes to the DOM directly as we compare virtual DOMs. - // - // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 - // - // The stack is empty upon entry. - fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { - if cfg!(debug_assertions) { - let mut keys = fxhash::FxHashSet::default(); - let mut assert_unique_keys = |children: &'b [VNode<'b>]| { - keys.clear(); - for child in children { - let key = child.key(); - debug_assert!( - key.is_some(), - "if any sibling is keyed, all siblings must be keyed" - ); - keys.insert(key); - } - debug_assert_eq!( - children.len(), - keys.len(), - "keyed siblings must each have a unique key" - ); - }; - assert_unique_keys(old); - assert_unique_keys(new); - } - - // First up, we diff all the nodes with the same key at the beginning of the - // children. - // - // `shared_prefix_count` is the count of how many nodes at the start of - // `new` and `old` share the same keys. - let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { - Some(count) => count, - None => return, - }; - - // Ok, we now hopefully have a smaller range of children in the middle - // within which to re-order nodes with the same keys, remove old nodes with - // now-unused keys, and create new nodes with fresh keys. - - let old_middle = &old[left_offset..(old.len() - right_offset)]; - let new_middle = &new[left_offset..(new.len() - right_offset)]; - - debug_assert!( - !((old_middle.len() == new_middle.len()) && old_middle.is_empty()), - "keyed children must have the same number of children" - ); - - if new_middle.is_empty() { - // remove the old elements - self.remove_nodes(old_middle, true); - } else if old_middle.is_empty() { - // there were no old elements, so just create the new elements - // we need to find the right "foothold" though - we shouldn't use the "append" at all - if left_offset == 0 { - // insert at the beginning of the old list - let foothold = &old[old.len() - right_offset]; - self.create_and_insert_before(new_middle, foothold); - } else if right_offset == 0 { - // insert at the end the old list - let foothold = old.last().unwrap(); - self.create_and_insert_after(new_middle, foothold); - } else { - // inserting in the middle - let foothold = &old[left_offset - 1]; - self.create_and_insert_after(new_middle, foothold); - } - } else { - self.diff_keyed_middle(old_middle, new_middle); - } - } - - /// Diff both ends of the children that share keys. - /// - /// Returns a left offset and right offset of that indicates a smaller section to pass onto the middle diffing. - /// - /// If there is no offset, then this function returns None and the diffing is complete. - fn diff_keyed_ends( - &mut self, - old: &'b [VNode<'b>], - new: &'b [VNode<'b>], - ) -> Option<(usize, usize)> { - let mut left_offset = 0; - - for (old, new) in old.iter().zip(new.iter()) { - // abort early if we finally run into nodes with different keys - if old.key() != new.key() { - break; - } - self.diff_node(old, new); - left_offset += 1; - } - - // If that was all of the old children, then create and append the remaining - // new children and we're finished. - if left_offset == old.len() { - self.create_and_insert_after(&new[left_offset..], old.last().unwrap()); - return None; - } - - // And if that was all of the new children, then remove all of the remaining - // old children and we're finished. - if left_offset == new.len() { - self.remove_nodes(&old[left_offset..], true); - return None; - } - - // if the shared prefix is less than either length, then we need to walk backwards - let mut right_offset = 0; - for (old, new) in old.iter().rev().zip(new.iter().rev()) { - // abort early if we finally run into nodes with different keys - if old.key() != new.key() { - break; - } - self.diff_node(old, new); - right_offset += 1; - } - - Some((left_offset, right_offset)) - } - - // The most-general, expensive code path for keyed children diffing. - // - // We find the longest subsequence within `old` of children that are relatively - // ordered the same way in `new` (via finding a longest-increasing-subsequence - // of the old child's index within `new`). The children that are elements of - // this subsequence will remain in place, minimizing the number of DOM moves we - // will have to do. - // - // Upon entry to this function, the change list stack must be empty. - // - // This function will load the appropriate nodes onto the stack and do diffing in place. - // - // Upon exit from this function, it will be restored to that same self. - fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { - /* - 1. Map the old keys into a numerical ordering based on indices. - 2. Create a map of old key to its index - 3. Map each new key to the old key, carrying over the old index. - - IE if we have ABCD becomes BACD, our sequence would be 1,0,2,3 - - if we have ABCD to ABDE, our sequence would be 0,1,3,MAX because E doesn't exist - - now, we should have a list of integers that indicates where in the old list the new items map to. - - 4. Compute the LIS of this list - - this indicates the longest list of new children that won't need to be moved. - - 5. Identify which nodes need to be removed - 6. Identify which nodes will need to be diffed - - 7. Going along each item in the new list, create it and insert it before the next closest item in the LIS. - - if the item already existed, just move it to the right place. - - 8. Finally, generate instructions to remove any old children. - 9. Generate instructions to finally diff children that are the same between both - */ - - // 0. Debug sanity checks - // Should have already diffed the shared-key prefixes and suffixes. - debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); - debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); - - // 1. Map the old keys into a numerical ordering based on indices. - // 2. Create a map of old key to its index - // IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3). - let old_key_to_old_index = old - .iter() - .enumerate() - .map(|(i, o)| (o.key().unwrap(), i)) - .collect::>(); - - let mut shared_keys = FxHashSet::default(); - - // 3. Map each new key to the old key, carrying over the old index. - let new_index_to_old_index = new - .iter() - .map(|node| { - let key = node.key().unwrap(); - if let Some(&index) = old_key_to_old_index.get(&key) { - shared_keys.insert(key); - index - } else { - u32::MAX as usize - } - }) - .collect::>(); - - // If none of the old keys are reused by the new children, then we remove all the remaining old children and - // create the new children afresh. - if shared_keys.is_empty() { - if let Some(first_old) = old.get(0) { - self.remove_nodes(&old[1..], true); - let nodes_created = self.create_children(new); - self.replace_inner(first_old, nodes_created); - } else { - // I think this is wrong - why are we appending? - // only valid of the if there are no trailing elements - self.create_and_append_children(new); - } - return; - } - - // 4. Compute the LIS of this list - let mut lis_sequence = Vec::default(); - lis_sequence.reserve(new_index_to_old_index.len()); - - let mut predecessors = vec![0; new_index_to_old_index.len()]; - let mut starts = vec![0; new_index_to_old_index.len()]; - - longest_increasing_subsequence::lis_with( - &new_index_to_old_index, - &mut lis_sequence, - |a, b| a < b, - &mut predecessors, - &mut starts, - ); - - // the lis comes out backwards, I think. can't quite tell. - lis_sequence.sort_unstable(); - - // if a new node gets u32 max and is at the end, then it might be part of our LIS (because u32 max is a valid LIS) - if lis_sequence.last().map(|f| new_index_to_old_index[*f]) == Some(u32::MAX as usize) { - lis_sequence.pop(); - } - - for idx in lis_sequence.iter() { - self.diff_node(&old[new_index_to_old_index[*idx]], &new[*idx]); - } - - let mut nodes_created = 0; - - // add mount instruction for the first items not covered by the lis - let last = *lis_sequence.last().unwrap(); - if last < (new.len() - 1) { - for (idx, new_node) in new[(last + 1)..].iter().enumerate() { - let new_idx = idx + last + 1; - let old_index = new_index_to_old_index[new_idx]; - if old_index == u32::MAX as usize { - nodes_created += self.create_node(new_node); - } else { - self.diff_node(&old[old_index], new_node); - nodes_created += self.push_all_nodes(new_node); - } - } - - self.mutations.insert_after( - self.find_last_element(&new[last]).unwrap(), - nodes_created as u32, - ); - nodes_created = 0; - } - - // for each spacing, generate a mount instruction - let mut lis_iter = lis_sequence.iter().rev(); - let mut last = *lis_iter.next().unwrap(); - for next in lis_iter { - if last - next > 1 { - for (idx, new_node) in new[(next + 1)..last].iter().enumerate() { - let new_idx = idx + next + 1; - - let old_index = new_index_to_old_index[new_idx]; - if old_index == u32::MAX as usize { - nodes_created += self.create_node(new_node); - } else { - self.diff_node(&old[old_index], new_node); - nodes_created += self.push_all_nodes(new_node); - } - } - - self.mutations.insert_before( - self.find_first_element(&new[last]).unwrap(), - nodes_created as u32, - ); - - nodes_created = 0; - } - last = *next; - } - - // add mount instruction for the last items not covered by the lis - let first_lis = *lis_sequence.first().unwrap(); - if first_lis > 0 { - for (idx, new_node) in new[..first_lis].iter().enumerate() { - let old_index = new_index_to_old_index[idx]; - if old_index == u32::MAX as usize { - nodes_created += self.create_node(new_node); - } else { - self.diff_node(&old[old_index], new_node); - nodes_created += self.push_all_nodes(new_node); - } - } - - self.mutations.insert_before( - self.find_first_element(&new[first_lis]).unwrap(), - nodes_created as u32, - ); - } - } - - fn replace_node(&mut self, old: &'b VNode<'b>, new: &'b VNode<'b>) { - // fn replace_node(&mut self, old: &'b VNode<'b>, new: &'b VNode<'b>) { - log::debug!("Replacing node {:?}", old); - let nodes_created = self.create_node(new); - self.replace_inner(old, nodes_created); - } - - fn replace_inner(&mut self, old: &'b VNode<'b>, nodes_created: usize) { - match old { - VNode::Element(el) => { - let id = old - .try_mounted_id() - .unwrap_or_else(|| panic!("broke on {:?}", old)); - - self.mutations.replace_with(id, nodes_created as u32); - self.remove_nodes(el.children, false); - self.scopes.collect_garbage(id); - } - - VNode::Text(_) | VNode::Placeholder(_) => { - let id = old - .try_mounted_id() - .unwrap_or_else(|| panic!("broke on {:?}", old)); - - self.mutations.replace_with(id, nodes_created as u32); - self.scopes.collect_garbage(id); - } - - VNode::Fragment(f) => { - self.replace_inner(&f.children[0], nodes_created); - self.remove_nodes(f.children.iter().skip(1), true); - } - - VNode::Component(c) => { - let node = self.scopes.fin_head(c.scope.get().unwrap()); - self.replace_node(node, node); - - let scope_id = c.scope.get().unwrap(); - - // we can only remove components if they are actively being diffed - if self.scope_stack.contains(&c.originator) { - self.scopes.try_remove(scope_id).unwrap(); - } - } - } - } - - pub fn remove_nodes(&mut self, nodes: impl IntoIterator>, gen_muts: bool) { - for node in nodes { - match node { - VNode::Text(t) => { - // this check exists because our null node will be removed but does not have an ID - if let Some(id) = t.id.get() { - self.scopes.collect_garbage(id); - - if gen_muts { - self.mutations.remove(id.as_u64()); - } - } - } - VNode::Placeholder(a) => { - let id = a.id.get().unwrap(); - self.scopes.collect_garbage(id); - - if gen_muts { - self.mutations.remove(id.as_u64()); - } - } - VNode::Element(e) => { - let id = e.id.get().unwrap(); - - if gen_muts { - self.mutations.remove(id.as_u64()); - } - - self.scopes.collect_garbage(id); - - self.remove_nodes(e.children, false); - } - - VNode::Fragment(f) => { - self.remove_nodes(f.children, gen_muts); - } - - VNode::Component(c) => { - let scope_id = c.scope.get().unwrap(); - let root = self.scopes.root_node(scope_id); - self.remove_nodes([root], gen_muts); - - // we can only remove this node if the originator is actively - if self.scope_stack.contains(&c.originator) { - self.scopes.try_remove(scope_id).unwrap(); - } - } - } - } - } - - fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { - let mut created = 0; - for node in nodes { - created += self.create_node(node); - } - created - } - - fn create_and_append_children(&mut self, nodes: &'b [VNode<'b>]) { - let created = self.create_children(nodes); - self.mutations.append_children(created as u32); - } - - fn create_and_insert_after(&mut self, nodes: &'b [VNode<'b>], after: &'b VNode<'b>) { - let created = self.create_children(nodes); - let last = self.find_last_element(after).unwrap(); - self.mutations.insert_after(last, created as u32); - } - - fn create_and_insert_before(&mut self, nodes: &'b [VNode<'b>], before: &'b VNode<'b>) { - let created = self.create_children(nodes); - let first = self.find_first_element(before).unwrap(); - self.mutations.insert_before(first, created as u32); - } - - fn current_scope(&self) -> Option { - self.scope_stack.last().copied() - } - - fn enter_scope(&mut self, scope: ScopeId) { - self.scope_stack.push(scope); - } - - fn leave_scope(&mut self) { - self.scope_stack.pop(); - } - - fn find_last_element(&self, vnode: &'b VNode<'b>) -> Option { - let mut search_node = Some(vnode); - - loop { - match &search_node.take().unwrap() { - VNode::Text(t) => break t.id.get(), - VNode::Element(t) => break t.id.get(), - VNode::Placeholder(t) => break t.id.get(), - VNode::Fragment(frag) => { - search_node = frag.children.last(); - } - VNode::Component(el) => { - let scope_id = el.scope.get().unwrap(); - search_node = Some(self.scopes.root_node(scope_id)); - } - } - } - } - - fn find_first_element(&self, vnode: &'b VNode<'b>) -> Option { - let mut search_node = Some(vnode); - - loop { - match &search_node.take().unwrap() { - // the ones that have a direct id - VNode::Fragment(frag) => { - search_node = Some(&frag.children[0]); - } - VNode::Component(el) => { - let scope_id = el.scope.get().unwrap(); - search_node = Some(self.scopes.root_node(scope_id)); - } - VNode::Text(t) => break t.id.get(), - VNode::Element(t) => break t.id.get(), - VNode::Placeholder(t) => break t.id.get(), - } - } - } - - // fn replace_node(&mut self, old: &'b VNode<'b>, nodes_created: usize) { - // log::debug!("Replacing node {:?}", old); - // match old { - // VNode::Element(el) => { - // let id = old - // .try_mounted_id() - // .unwrap_or_else(|| panic!("broke on {:?}", old)); - - // self.mutations.replace_with(id, nodes_created as u32); - // self.remove_nodes(el.children, false); - // } - - // VNode::Text(_) | VNode::Placeholder(_) => { - // let id = old - // .try_mounted_id() - // .unwrap_or_else(|| panic!("broke on {:?}", old)); - - // self.mutations.replace_with(id, nodes_created as u32); - // } - - // VNode::Fragment(f) => { - // self.replace_node(&f.children[0], nodes_created); - // self.remove_nodes(f.children.iter().skip(1), true); - // } - - // VNode::Component(c) => { - // let node = self.scopes.fin_head(c.scope.get().unwrap()); - // self.replace_node(node, nodes_created); - - // let scope_id = c.scope.get().unwrap(); - - // // we can only remove components if they are actively being diffed - // if self.stack.scope_stack.contains(&c.originator) { - // self.scopes.try_remove(scope_id).unwrap(); - // } - // } - // } - // } - - // /// schedules nodes for garbage collection and pushes "remove" to the mutation stack - // /// remove can happen whenever - // pub(crate) fn remove_nodes( - // &mut self, - // nodes: impl IntoIterator>, - // gen_muts: bool, - // ) { - // // or cache the vec on the diff machine - // for node in nodes { - // log::debug!("removing {:?}", node); - // match node { - // VNode::Text(t) => { - // // this check exists because our null node will be removed but does not have an ID - // if let Some(id) = t.id.get() { - // // self.scopes.collect_garbage(id); - - // if gen_muts { - // self.mutations.remove(id.as_u64()); - // } - // } - // } - // VNode::Placeholder(a) => { - // let id = a.id.get().unwrap(); - // // self.scopes.collect_garbage(id); - - // if gen_muts { - // self.mutations.remove(id.as_u64()); - // } - // } - // VNode::Element(e) => { - // let id = e.id.get().unwrap(); - - // if gen_muts { - // self.mutations.remove(id.as_u64()); - // } - - // self.remove_nodes(e.children, false); - - // // self.scopes.collect_garbage(id); - // } - - // VNode::Fragment(f) => { - // self.remove_nodes(f.children, gen_muts); - // } - - // VNode::Component(c) => { - // let scope_id = c.scope.get().unwrap(); - // let root = self.scopes.root_node(scope_id); - // self.remove_nodes(Some(root), gen_muts); - - // // we can only remove this node if the originator is actively - // if self.stack.scope_stack.contains(&c.originator) { - // self.scopes.try_remove(scope_id).unwrap(); - // } - // } - // } - // } - // } - - // recursively push all the nodes of a tree onto the stack and return how many are there - fn push_all_nodes(&mut self, node: &'b VNode<'b>) -> usize { - match node { - VNode::Text(_) | VNode::Placeholder(_) => { - self.mutations.push_root(node.mounted_id()); - 1 - } - - VNode::Fragment(_) | VNode::Component(_) => { - // - let mut added = 0; - for child in node.children() { - added += self.push_all_nodes(child); - } - added - } - - VNode::Element(el) => { - let mut num_on_stack = 0; - for child in el.children.iter() { - num_on_stack += self.push_all_nodes(child); - } - self.mutations.push_root(el.id.get().unwrap()); - - num_on_stack + 1 - } - } - } -} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 48a8a3505..dba4effbb 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,8 +1,7 @@ #![allow(non_snake_case)] #![doc = include_str!("../README.md")] -// pub(crate) mod diff; -pub(crate) mod diff_async; +pub(crate) mod diff; pub(crate) mod events; pub(crate) mod lazynodes; pub(crate) mod mutations; @@ -13,8 +12,7 @@ pub(crate) mod util; pub(crate) mod virtual_dom; pub(crate) mod innerlude { - pub(crate) use crate::diff_async::*; - // pub(crate) use crate::diff::*; + pub(crate) use crate::diff::*; pub use crate::events::*; pub use crate::lazynodes::*; pub use crate::mutations::*; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 542afc673..0ebbea9b9 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -255,7 +255,7 @@ impl ScopeArena { let scope = unsafe { &mut *self.get_scope_raw(id).expect("could not find scope") }; // if cfg!(debug_assertions) { - log::debug!("running scope {:?} symbol: {:?}", id, scope.fnptr); + // log::debug!("running scope {:?} symbol: {:?}", id, scope.fnptr); // todo: resolve frames properly backtrace::resolve(scope.fnptr, |symbol| { @@ -733,7 +733,6 @@ impl ScopeState { while let Some(parent_ptr) = search_parent { // safety: all parent pointers are valid thanks to the bump arena let parent = unsafe { &*parent_ptr }; - log::trace!("Searching parent scope {:?}", parent.scope_id()); if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::()) { return Some(shared.clone().downcast::().unwrap()); } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index d879e0340..032aa6198 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -2,7 +2,7 @@ //! //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. -use crate::diff_async::AsyncDiffState as DiffState; +use crate::diff::AsyncDiffState as DiffState; use crate::innerlude::*; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::{future::poll_fn, StreamExt}; diff --git a/packages/core/tests/earlyabort.rs b/packages/core/tests/earlyabort.rs index 7dc2ae4d4..3999a7402 100644 --- a/packages/core/tests/earlyabort.rs +++ b/packages/core/tests/earlyabort.rs @@ -42,14 +42,8 @@ fn test_early_abort() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 1, - }, - CreateTextNode { - text: "Hello, world!", - root: 2, - }, + CreateElement { tag: "div", root: 1 }, + CreateTextNode { text: "Hello, world!", root: 2 }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, ] @@ -65,14 +59,8 @@ fn test_early_abort() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "Hello, world!", - root: 4, - }, + CreateElement { tag: "div", root: 1 }, // keys get reused + CreateTextNode { text: "Hello, world!", root: 2 }, // keys get reused AppendChildren { many: 1 }, ReplaceWith { root: 3, m: 1 }, ] diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 6c0df4e1e..f977ba96e 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -27,12 +27,7 @@ fn manual_diffing() { }; let value = Arc::new(Mutex::new("Hello")); - let mut dom = VirtualDom::new_with_props( - App, - AppProps { - value: value.clone(), - }, - ); + let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone() }); let _ = dom.rebuild(); @@ -45,7 +40,7 @@ fn manual_diffing() { #[test] fn events_generate() { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let count = cx.use_hook(|_| 0); let inner = match *count { @@ -66,7 +61,7 @@ fn events_generate() { cx.render(inner) }; - let mut dom = VirtualDom::new(App); + let mut dom = VirtualDom::new(app); let mut channel = dom.get_scheduler_channel(); assert!(dom.has_work()); @@ -74,28 +69,12 @@ fn events_generate() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 1, - }, - NewEventListener { - event_name: "click", - scope: ScopeId(0), - root: 1, - }, - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "nested", - root: 3, - }, + CreateElement { tag: "div", root: 1 }, + NewEventListener { event_name: "click", scope: ScopeId(0), root: 1 }, + CreateElement { tag: "div", root: 2 }, + CreateTextNode { text: "nested", root: 3 }, AppendChildren { many: 1 }, - CreateTextNode { - text: "Click me!", - root: 4, - }, + CreateTextNode { text: "Click me!", root: 4 }, AppendChildren { many: 2 }, AppendChildren { many: 1 }, ] @@ -104,7 +83,7 @@ fn events_generate() { #[test] fn components_generate() { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let render_phase = cx.use_hook(|_| 0); *render_phase += 1; @@ -121,106 +100,84 @@ fn components_generate() { }) }; - static Child: Component = |cx| { + fn Child(cx: Scope) -> Element { + log::debug!("Running child"); cx.render(rsx! { h1 {} }) - }; + } - let mut dom = VirtualDom::new(App); + let mut dom = VirtualDom::new(app); let edits = dom.rebuild(); assert_eq!( edits.edits, [ - CreateTextNode { - text: "Text0", - root: 1, - }, + CreateTextNode { text: "Text0", root: 1 }, AppendChildren { many: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateElement { - tag: "div", - root: 2, - }, + CreateElement { tag: "div", root: 2 }, ReplaceWith { root: 1, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateTextNode { - text: "Text2", - root: 3, - }, + CreateTextNode { text: "Text2", root: 1 }, ReplaceWith { root: 2, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); + // child {} assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateElement { tag: "h1", root: 4 }, - ReplaceWith { root: 3, m: 1 }, + CreateElement { tag: "h1", root: 2 }, + ReplaceWith { root: 1, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); + // placeholder assert_eq!( - edits.edits, - [CreatePlaceholder { root: 5 }, ReplaceWith { root: 4, m: 1 },] + dom.hard_diff(ScopeId(0)).edits, + [CreatePlaceholder { root: 1 }, ReplaceWith { root: 2, m: 1 },] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateTextNode { - text: "text 3", - root: 6, - }, - ReplaceWith { root: 5, m: 1 }, + CreateTextNode { text: "text 3", root: 2 }, + ReplaceWith { root: 1, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateTextNode { - text: "text 0", - root: 7, - }, - CreateTextNode { - text: "text 1", - root: 8, - }, - ReplaceWith { root: 6, m: 2 }, + CreateTextNode { text: "text 0", root: 1 }, + CreateTextNode { text: "text 1", root: 3 }, + ReplaceWith { root: 2, m: 2 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateElement { tag: "h1", root: 9 }, - ReplaceWith { root: 7, m: 1 }, - Remove { root: 8 }, + CreateElement { tag: "h1", root: 2 }, + ReplaceWith { root: 1, m: 1 }, + Remove { root: 3 }, ] ); } #[test] fn component_swap() { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let render_phase = cx.use_hook(|_| 0); *render_phase += 1; @@ -296,7 +253,7 @@ fn component_swap() { }) }; - let mut dom = VirtualDom::new(App); + let mut dom = VirtualDom::new(app); let edits = dom.rebuild(); dbg!(&edits); diff --git a/packages/core/tests/passthru.rs b/packages/core/tests/passthru.rs new file mode 100644 index 000000000..e00aa545c --- /dev/null +++ b/packages/core/tests/passthru.rs @@ -0,0 +1,108 @@ +#![allow(unused, non_upper_case_globals)] + +//! Diffing Tests +//! +//! These tests only verify that the diffing algorithm works properly for single components. +//! +//! It does not validated that component lifecycles work properly. This is done in another test file. + +use dioxus::{prelude::*, DomEdit, ScopeId}; +use dioxus_core as dioxus; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; + +mod test_logging; + +fn new_dom() -> VirtualDom { + const IS_LOGGING_ENABLED: bool = false; + test_logging::set_up_logging(IS_LOGGING_ENABLED); + VirtualDom::new(|cx| rsx!(cx, "hi")) +} + +use DomEdit::*; + +/// Should push the text node onto the stack and modify it +#[test] +fn nested_passthru_creates() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + Child { + Child { + Child { + div { + "hi" + } + } + } + } + }) + }; + + #[inline_props] + fn Child<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { + children + }) + }; + + let mut dom = VirtualDom::new(app); + let mut channel = dom.get_scheduler_channel(); + assert!(dom.has_work()); + + let edits = dom.rebuild(); + assert_eq!( + edits.edits, + [ + CreateElement { tag: "div", root: 1 }, + CreateTextNode { text: "hi", root: 2 }, + AppendChildren { many: 1 }, + AppendChildren { many: 1 }, + ] + ) +} + +/// Should push the text node onto the stack and modify it +#[test] +fn nested_passthru_creates_add() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + Child { + "1" + Child { + "2" + Child { + "3" + div { + "hi" + } + } + } + } + }) + }; + + #[inline_props] + fn Child<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { + children + }) + }; + + let mut dom = VirtualDom::new(app); + let mut channel = dom.get_scheduler_channel(); + assert!(dom.has_work()); + + let edits = dom.rebuild(); + assert_eq!( + edits.edits, + [ + CreateTextNode { text: "1", root: 1 }, + CreateTextNode { text: "2", root: 2 }, + CreateTextNode { text: "3", root: 3 }, + CreateElement { tag: "div", root: 4 }, + CreateTextNode { text: "hi", root: 5 }, + AppendChildren { many: 1 }, + AppendChildren { many: 4 }, + ] + ) +} From a0ffe66a371744f5a38697f06e6fee89f2db2809 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 31 Jan 2022 09:47:09 +0800 Subject: [PATCH 065/256] feat: add `borderless` example --- .gitignore | 1 + examples/borderless.rs | 47 +++++++++++++++++++------ packages/desktop/src/desktop_context.rs | 5 ++- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 9534dc207..9df7e9f93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/dist Cargo.lock .DS_Store diff --git a/examples/borderless.rs b/examples/borderless.rs index 18bbbb80b..7f85ff22d 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -1,21 +1,48 @@ -use dioxus::prelude::*; +use dioxus::{events::onmousedown, prelude::*}; use dioxus_desktop::desktop_context::DesktopContext; fn main() { dioxus::desktop::launch_cfg(app, |cfg| { - cfg.with_window(|w| { - w.with_title("BorderLess Demo") - .with_decorations(false) - }) + cfg.with_window(|w| w.with_title("BorderLess Demo").with_decorations(false)) }); } -fn app (cx: Scope) -> Element { +fn app(cx: Scope) -> Element { let desktop = cx.consume_context::().unwrap(); + + let drag = desktop.clone(); + let close = desktop.clone(); + let min = desktop.clone(); + cx.render(rsx!( - div { - style: "background-color: black; height: 20px; width: 100%", - onmousedown: move |_| desktop.drag_window(), + link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } + header { + class: "text-gray-400 bg-gray-900 body-font", + onmousedown: move |_| drag.drag_window(), + div { + class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", + a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", + span { class: "ml-3 text-xl", "Dioxus"} + } + nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center", + // a { class: "mr-5 hover:text-white", "First Link"} + // a { class: "mr-5 hover:text-white", "Second Link"} + // a { class: "mr-5 hover:text-white", "Third Link"} + // a { class: "mr-5 hover:text-white", "Fourth Link"} + } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| min.minimize(true), + "Minimize" + } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| close.close(), + "Close" + } + } } )) -} \ No newline at end of file +} diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 577e33c54..456dba1c1 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -20,7 +20,7 @@ pub struct DesktopContext { impl DesktopContext { - pub fn new(proxy: ProxyType) -> Self { + pub(crate) fn new(proxy: ProxyType) -> Self { Self { proxy } } @@ -36,14 +36,17 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::DragWindow); } + /// set window minimize state pub fn minimize(&self, minimized: bool) { let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized)); } + /// set window maximize state pub fn maximize(&self, maximized: bool) { let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); } + /// close window pub fn close(&self) { let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); } From e4eb982a367ed588b370e4a28e8cf2a94d0f4b24 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 31 Jan 2022 10:11:58 +0800 Subject: [PATCH 066/256] fix: format code --- examples/borderless.rs | 14 ++++---------- packages/desktop/src/desktop_context.rs | 14 ++++++-------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/examples/borderless.rs b/examples/borderless.rs index 7f85ff22d..e6cd4eda1 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -1,4 +1,4 @@ -use dioxus::{events::onmousedown, prelude::*}; +use dioxus::prelude::*; use dioxus_desktop::desktop_context::DesktopContext; fn main() { @@ -12,11 +12,10 @@ fn app(cx: Scope) -> Element { let drag = desktop.clone(); let close = desktop.clone(); - let min = desktop.clone(); cx.render(rsx!( link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } - header { + header { class: "text-gray-400 bg-gray-900 body-font", onmousedown: move |_| drag.drag_window(), div { @@ -24,16 +23,11 @@ fn app(cx: Scope) -> Element { a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", span { class: "ml-3 text-xl", "Dioxus"} } - nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center", - // a { class: "mr-5 hover:text-white", "First Link"} - // a { class: "mr-5 hover:text-white", "Second Link"} - // a { class: "mr-5 hover:text-white", "Third Link"} - // a { class: "mr-5 hover:text-white", "Fourth Link"} - } + nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" } button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| min.minimize(true), + onclick: move |_| desktop.minimize(true), "Minimize" } button { diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 456dba1c1..0a1fa2b02 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -5,11 +5,11 @@ use crate::UserWindowEvent; type ProxyType = EventLoopProxy; /// Desktop-Window handle api context -/// +/// /// you can use this context control some window event -/// +/// /// you can use `cx.consume_context::` to get this context -/// +/// /// ```rust /// let desktop = cx.consume_context::().unwrap(); /// ``` @@ -19,15 +19,14 @@ pub struct DesktopContext { } impl DesktopContext { - pub(crate) fn new(proxy: ProxyType) -> Self { Self { proxy } } /// trigger the drag-window event - /// + /// /// Moves the window with the left mouse button until the button is released. - /// + /// /// you need use it in `onmousedown` event: /// ```rust /// onmousedown: move |_| { desktop.drag_window(); } @@ -50,5 +49,4 @@ impl DesktopContext { pub fn close(&self) { let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); } - -} \ No newline at end of file +} From cbd471fa46d80fb3ea4d5e484be4bf62a31f770f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 01:24:11 -0500 Subject: [PATCH 067/256] debugging: add some more debug tooling --- packages/core/src/diff.rs | 24 ++++++++++++++++++++---- packages/core/src/nodes.rs | 16 +++++++--------- packages/core/src/scopes.rs | 8 ++++---- packages/core/src/virtual_dom.rs | 13 +++++++++++++ 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index d790292c8..6f7f12fa4 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -406,10 +406,14 @@ impl<'b> AsyncDiffState<'b> { fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) { // This is the case where options or direct vnodes might be used. // In this case, it's faster to just skip ahead to their diff - if old.children.len() == 1 && new.children.len() == 1 { - self.diff_node(&old.children[0], &new.children[0]); - return; - } + // if old.children.len() == 1 && new.children.len() == 1 { + // if std::ptr::eq(old, new) { + // log::debug!("skipping fragment diff - fragment is the same"); + // return; + // } + // self.diff_node(&old.children[0], &new.children[0]); + // return; + // } debug_assert!(!old.children.is_empty()); debug_assert!(!new.children.is_empty()); @@ -437,6 +441,11 @@ impl<'b> AsyncDiffState<'b> { // Fragment nodes cannot generate empty children lists, so we can assume that when a list is empty, it belongs only // to an element, and appending makes sense. fn diff_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + if std::ptr::eq(old, new) { + log::debug!("skipping fragment diff - fragment is the same"); + return; + } + // Remember, fragments can never be empty (they always have a single child) match (old, new) { ([], []) => {} @@ -484,7 +493,13 @@ impl<'b> AsyncDiffState<'b> { Ordering::Equal => {} } + // panic!( + // "diff_children: new_is_keyed: {:#?}, old_is_keyed: {:#?}. stack: {:#?}, oldptr: {:#?}, newptr: {:#?}", + // old, new, self.scope_stack, old as *const _, new as *const _ + // ); + for (new, old) in new.iter().zip(old.iter()) { + log::debug!("diffing nonkeyed {:#?} {:#?}", old, new); self.diff_node(old, new); } } @@ -893,6 +908,7 @@ impl<'b> AsyncDiffState<'b> { // we can only remove this node if the originator is actively if self.scope_stack.contains(&c.originator) { + log::debug!("I am allowed to remove component because scope stack contains originator. Scope stack: {:#?} {:#?} {:#?}", self.scope_stack, c.originator, c.scope); self.scopes.try_remove(scope_id).unwrap(); } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 74234a84c..2f1453e1d 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -182,15 +182,13 @@ impl Debug for VNode<'_> { VNode::Fragment(frag) => { write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children) } - VNode::Component(comp) => { - s.debug_struct("VNode::VComponent") - .field("fnptr", &comp.user_fc) - .field("key", &comp.key) - .field("scope", &comp.scope) - .field("originator", &comp.originator) - .finish() - //write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc) - } + VNode::Component(comp) => s + .debug_struct("VNode::VComponent") + .field("fnptr", &comp.user_fc) + .field("key", &comp.key) + .field("scope", &comp.scope) + .field("originator", &comp.originator) + .finish(), } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 0ebbea9b9..93a0c94a7 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -98,7 +98,8 @@ impl ScopeArena { let height = parent_scope .map(|id| self.get_scope(id).map(|scope| scope.height)) .flatten() - .unwrap_or_default(); + .unwrap_or_default() + + 1; let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten(); @@ -203,9 +204,8 @@ impl ScopeArena { } pub fn collect_garbage(&self, id: ElementId) { - // println!("collecting garbage for {:?}", id); - // log::debug!("collecting garbage for {:?}", id); - self.nodes.borrow_mut().remove(id.0); + let node = self.nodes.borrow_mut().remove(id.0); + log::debug!("collecting garbage for {:?}, {:?}", id, unsafe { &*node }); } /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 032aa6198..613ea9292 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -462,14 +462,27 @@ impl VirtualDom { self.dirty_scopes .retain(|id| scopes.get_scope(*id).is_some()); + log::debug!("dirty_scopes: {:?}", self.dirty_scopes); + + for id in &self.dirty_scopes { + let scope = scopes.get_scope(*id).unwrap(); + + log::debug!("dirty scope: {:?} with height {:?}", id, scope.height); + } + self.dirty_scopes.sort_by(|a, b| { let h1 = scopes.get_scope(*a).unwrap().height; let h2 = scopes.get_scope(*b).unwrap().height; h1.cmp(&h2).reverse() }); + log::debug!("dirty_scopes: {:?}", self.dirty_scopes); + if let Some(scopeid) = self.dirty_scopes.pop() { + log::debug!("diffing scope {:?}", scopeid); if !ran_scopes.contains(&scopeid) { + log::debug!("running scope scope {:?}", scopeid); + ran_scopes.insert(scopeid); self.scopes.run_scope(scopeid); From 11f6b938891ab67a05de6926373fbf331577775d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 02:33:16 -0500 Subject: [PATCH 068/256] fix: remove nodes is in a happier state --- packages/core-macro/src/rsx/component.rs | 3 ++ packages/core/src/diff.rs | 37 ++++++++++++++++-------- packages/core/src/mutations.rs | 6 ++-- packages/core/src/nodes.rs | 4 +++ packages/core/src/scopes.rs | 24 ++++++++++----- packages/core/src/virtual_dom.rs | 14 ++------- 6 files changed, 54 insertions(+), 34 deletions(-) diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 5257022a4..4e9d449cf 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -135,11 +135,14 @@ impl ToTokens for Component { None => quote! { None }, }; + let fn_name = self.name.segments.last().unwrap().ident.to_string(); + tokens.append_all(quote! { __cx.component( #name, #builder, #key_token, + #fn_name ) }) } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 6f7f12fa4..db898f194 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -26,7 +26,10 @@ impl<'b> AsyncDiffState<'b> { self.scope_stack.push(scopeid); let scope = self.scopes.get_scope(scopeid).unwrap(); self.element_stack.push(scope.container); + self.diff_node(old, new); + + self.mutations.mark_dirty_scope(scopeid); } pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) { @@ -87,13 +90,6 @@ impl<'b> AsyncDiffState<'b> { self.diff_fragment_nodes(old, new) } - // The normal pathway still works, but generates slightly weird instructions - // This pathway ensures uses the ReplaceAll, not the InsertAfter and remove - (Placeholder(_), Fragment(_)) => { - log::debug!("replacing placeholder with fragment {:?}", new_node); - self.replace_node(old_node, new_node); - } - // Anything else is just a basic replace and create ( Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), @@ -201,7 +197,8 @@ impl<'b> AsyncDiffState<'b> { }; log::info!( - "created component {:?} with parent {:?} and originator {:?}", + "created component \"{}\", id: {:?} parent {:?} orig: {:?}", + vcomponent.fn_name, new_idx, parent_idx, vcomponent.originator @@ -442,7 +439,7 @@ impl<'b> AsyncDiffState<'b> { // to an element, and appending makes sense. fn diff_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { if std::ptr::eq(old, new) { - log::debug!("skipping fragment diff - fragment is the same"); + log::debug!("skipping fragment diff - fragment is the same {:?}", old); return; } @@ -499,7 +496,7 @@ impl<'b> AsyncDiffState<'b> { // ); for (new, old) in new.iter().zip(old.iter()) { - log::debug!("diffing nonkeyed {:#?} {:#?}", old, new); + // log::debug!("diffing nonkeyed {:#?} {:#?}", old, new); self.diff_node(old, new); } } @@ -849,10 +846,15 @@ impl<'b> AsyncDiffState<'b> { } VNode::Component(c) => { - let node = self.scopes.fin_head(c.scope.get().unwrap()); + log::info!("Replacing component {:?}", old); + let scope_id = c.scope.get().unwrap(); + let node = self.scopes.fin_head(scope_id); + + self.enter_scope(scope_id); + self.replace_inner(node, nodes_created); - let scope_id = c.scope.get().unwrap(); + log::info!("Replacing component x2 {:?}", old); // we can only remove components if they are actively being diffed if self.scope_stack.contains(&c.originator) { @@ -860,6 +862,7 @@ impl<'b> AsyncDiffState<'b> { self.scopes.try_remove(scope_id).unwrap(); } + self.leave_scope(); } } } @@ -902,15 +905,25 @@ impl<'b> AsyncDiffState<'b> { } VNode::Component(c) => { + self.enter_scope(c.scope.get().unwrap()); + let scope_id = c.scope.get().unwrap(); let root = self.scopes.root_node(scope_id); self.remove_nodes([root], gen_muts); + log::info!( + "trying to remove scope {:?}, stack is {:#?}, originator is {:?}", + scope_id, + self.scope_stack, + c.originator + ); + // we can only remove this node if the originator is actively if self.scope_stack.contains(&c.originator) { log::debug!("I am allowed to remove component because scope stack contains originator. Scope stack: {:#?} {:#?} {:#?}", self.scope_stack, c.originator, c.scope); self.scopes.try_remove(scope_id).unwrap(); } + self.leave_scope(); } } } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index bd94d0b71..5af727154 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -17,7 +17,7 @@ use std::{any::Any, fmt::Debug}; /// Mutations are the only link between the RealDOM and the VirtualDOM. pub struct Mutations<'a> { pub edits: Vec>, - pub dirty_scopes: FxHashSet, + pub diffed_scopes: FxHashSet, pub refs: Vec>, } @@ -118,7 +118,7 @@ impl<'a> Mutations<'a> { Self { edits: Vec::new(), refs: Vec::new(), - dirty_scopes: Default::default(), + diffed_scopes: Default::default(), } } @@ -223,7 +223,7 @@ impl<'a> Mutations<'a> { } pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) { - self.dirty_scopes.insert(scope); + self.diffed_scopes.insert(scope); } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 2f1453e1d..e95f3f825 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -184,6 +184,7 @@ impl Debug for VNode<'_> { } VNode::Component(comp) => s .debug_struct("VNode::VComponent") + .field("name", &comp.fn_name) .field("fnptr", &comp.user_fc) .field("key", &comp.key) .field("scope", &comp.scope) @@ -390,6 +391,7 @@ pub struct VComponent<'src> { pub scope: Cell>, pub can_memoize: bool, pub user_fc: FcSlot, + pub fn_name: &'static str, pub props: RefCell>>, } @@ -540,6 +542,7 @@ impl<'a> NodeFactory<'a> { component: fn(Scope<'a, P>) -> Element, props: P, key: Option, + fn_name: &'static str, ) -> VNode<'a> where P: Properties + 'a, @@ -550,6 +553,7 @@ impl<'a> NodeFactory<'a> { can_memoize: P::IS_STATIC, user_fc: component as *mut std::os::raw::c_void, originator: self.scope.scope_id(), + fn_name, props: RefCell::new(Some(Box::new(VComponentProps { // local_props: RefCell::new(Some(props)), // heap_props: RefCell::new(None), diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 93a0c94a7..351bcf4f7 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -113,13 +113,22 @@ impl ScopeArena { if let Some(old_scope) = self.free_scopes.borrow_mut().pop() { // reuse the old scope let scope = unsafe { &mut *old_scope }; - scope.props.get_mut().replace(vcomp); + + scope.container = container; + scope.our_arena_idx = new_scope_id; scope.parent_scope = parent_scope; scope.height = height; - scope.subtree.set(subtree); - scope.our_arena_idx = new_scope_id; - scope.container = container; scope.fnptr = fc_ptr; + scope.props.get_mut().replace(vcomp); + scope.subtree.set(subtree); + scope.frames[0].reset(); + scope.frames[1].reset(); + scope.shared_contexts.get_mut().clear(); + scope.items.get_mut().listeners.clear(); + scope.items.get_mut().borrowed_props.clear(); + scope.hook_idx.set(0); + scope.hook_vals.get_mut().clear(); + let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope); debug_assert!(any_item.is_none()); } else { @@ -177,7 +186,7 @@ impl ScopeArena { let scope = unsafe { &mut *self.scopes.borrow_mut().remove(&id).unwrap() }; scope.reset(); - self.free_scopes.borrow_mut().push(scope); + // self.free_scopes.borrow_mut().push(scope); Some(()) } @@ -204,8 +213,9 @@ impl ScopeArena { } pub fn collect_garbage(&self, id: ElementId) { - let node = self.nodes.borrow_mut().remove(id.0); - log::debug!("collecting garbage for {:?}, {:?}", id, unsafe { &*node }); + let node = self.nodes.borrow_mut().get(id.0).unwrap().clone(); + // let node = self.nodes.borrow_mut().remove(id.0); + // log::debug!("collecting garbage for {:?}, {:?}", id, unsafe { &*node }); } /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 613ea9292..9c4cd4018 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -462,14 +462,6 @@ impl VirtualDom { self.dirty_scopes .retain(|id| scopes.get_scope(*id).is_some()); - log::debug!("dirty_scopes: {:?}", self.dirty_scopes); - - for id in &self.dirty_scopes { - let scope = scopes.get_scope(*id).unwrap(); - - log::debug!("dirty scope: {:?} with height {:?}", id, scope.height); - } - self.dirty_scopes.sort_by(|a, b| { let h1 = scopes.get_scope(*a).unwrap().height; let h2 = scopes.get_scope(*b).unwrap().height; @@ -479,10 +471,7 @@ impl VirtualDom { log::debug!("dirty_scopes: {:?}", self.dirty_scopes); if let Some(scopeid) = self.dirty_scopes.pop() { - log::debug!("diffing scope {:?}", scopeid); if !ran_scopes.contains(&scopeid) { - log::debug!("running scope scope {:?}", scopeid); - ran_scopes.insert(scopeid); self.scopes.run_scope(scopeid); @@ -491,7 +480,8 @@ impl VirtualDom { let AsyncDiffState { mutations, .. } = diff_state; - for scope in &mutations.dirty_scopes { + log::debug!("succesffuly resolved scopes {:?}", mutations.diffed_scopes); + for scope in &mutations.diffed_scopes { self.dirty_scopes.remove(scope); } From 5bffbba682c634b2882d43e574d7415981f0d45e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 02:40:12 -0500 Subject: [PATCH 069/256] fmt: apply cargofmt with new rules for lit structs --- packages/core/src/events.rs | 4 +- packages/core/src/lazynodes.rs | 27 +----- packages/core/src/mutations.rs | 34 ++----- packages/core/src/nodes.rs | 6 +- packages/core/src/scopes.rs | 10 +- packages/core/src/util.rs | 5 +- packages/core/tests/create_dom.rs | 140 ++++++---------------------- packages/core/tests/diffing.rs | 1 - packages/core/tests/miri_stress.rs | 7 +- packages/core/tests/sharedstate.rs | 5 +- packages/core/tests/vdom_rebuild.rs | 10 +- 11 files changed, 54 insertions(+), 195 deletions(-) diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 8386cedd8..597a12992 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -12,9 +12,7 @@ pub(crate) struct BubbleState { impl BubbleState { pub fn new() -> Self { - Self { - canceled: Cell::new(false), - } + Self { canceled: Cell::new(false) } } } diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 1340c916c..0fbed45e1 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -55,9 +55,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> { fac.map(inner) }; - Self { - inner: StackNodeStorage::Heap(Box::new(val)), - } + Self { inner: StackNodeStorage::Heap(Box::new(val)) } } pub fn new(_val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self { @@ -71,9 +69,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> { // miri does not know how to work with mucking directly into bytes if cfg!(miri) { - Self { - inner: StackNodeStorage::Heap(Box::new(val)), - } + Self { inner: StackNodeStorage::Heap(Box::new(val)) } } else { unsafe { LazyNodes::new_inner(val) } } @@ -117,9 +113,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> { let max_size = mem::size_of::(); if stored_size > max_size { - Self { - inner: StackNodeStorage::Heap(Box::new(val)), - } + Self { inner: StackNodeStorage::Heap(Box::new(val)) } } else { let mut buf: StackHeapSize = StackHeapSize::default(); @@ -143,13 +137,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> { std::mem::forget(val); - Self { - inner: StackNodeStorage::Stack(LazyStack { - _align: [], - buf, - dropped: false, - }), - } + Self { inner: StackNodeStorage::Stack(LazyStack { _align: [], buf, dropped: false }) } } } @@ -298,12 +286,7 @@ mod tests { } let inner = Rc::new(0); - let mut dom = VirtualDom::new_with_props( - app, - AppProps { - inner: inner.clone(), - }, - ); + let mut dom = VirtualDom::new_with_props(app, AppProps { inner: inner.clone() }); dom.rebuild(); drop(dom); diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 5af727154..6a0785e23 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -115,11 +115,7 @@ use DomEdit::*; impl<'a> Mutations<'a> { pub(crate) fn new() -> Self { - Self { - edits: Vec::new(), - refs: Vec::new(), - diffed_scopes: Default::default(), - } + Self { edits: Vec::new(), refs: Vec::new(), diffed_scopes: Default::default() } } // Navigation @@ -178,19 +174,12 @@ impl<'a> Mutations<'a> { // events pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) { - let Listener { - event, - mounted_node, - .. - } = listener; + let Listener { event, mounted_node, .. } = listener; let element_id = mounted_node.get().unwrap().as_u64(); - self.edits.push(NewEventListener { - scope, - event_name: event, - root: element_id, - }); + self.edits + .push(NewEventListener { scope, event_name: event, root: element_id }); } pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: u64) { self.edits.push(RemoveEventListener { event, root }); @@ -202,19 +191,10 @@ impl<'a> Mutations<'a> { } pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute, root: u64) { - let Attribute { - name, - value, - namespace, - .. - } = attribute; + let Attribute { name, value, namespace, .. } = attribute; - self.edits.push(SetAttribute { - field: name, - value, - ns: *namespace, - root, - }); + self.edits + .push(SetAttribute { field: name, value, ns: *namespace, root }); } pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index e95f3f825..264fb56bb 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -4,7 +4,7 @@ //! cheap and *very* fast to construct - building a full tree should be quick. use crate::{ - innerlude::{Element, FcSlot, Properties, Scope, ScopeId, ScopeState}, + innerlude::{ComponentPtr, Element, Properties, Scope, ScopeId, ScopeState}, lazynodes::LazyNodes, AnyEvent, Component, }; @@ -390,7 +390,7 @@ pub struct VComponent<'src> { pub originator: ScopeId, pub scope: Cell>, pub can_memoize: bool, - pub user_fc: FcSlot, + pub user_fc: ComponentPtr, pub fn_name: &'static str, pub props: RefCell>>, } @@ -551,7 +551,7 @@ impl<'a> NodeFactory<'a> { key: key.map(|f| self.raw_text(f).0), scope: Default::default(), can_memoize: P::IS_STATIC, - user_fc: component as *mut std::os::raw::c_void, + user_fc: component as ComponentPtr, originator: self.scope.scope_id(), fn_name, props: RefCell::new(Some(Box::new(VComponentProps { diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 351bcf4f7..6b65c9197 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -14,8 +14,8 @@ use std::{ }; /// for traceability, we use the raw fn pointer to identify the function -/// we can use this with the traceback crate to resolve funciton names -pub(crate) type FcSlot = *mut std::os::raw::c_void; +/// we also get the component name, but that's not necessarily unique in the app +pub(crate) type ComponentPtr = *mut std::os::raw::c_void; pub(crate) struct Heuristic { hook_arena_size: usize, @@ -30,7 +30,7 @@ pub(crate) struct ScopeArena { pub scope_gen: Cell, pub bump: Bump, pub scopes: RefCell>, - pub heuristics: RefCell>, + pub heuristics: RefCell>, pub free_scopes: RefCell>, pub nodes: RefCell>>, pub tasks: Rc, @@ -84,7 +84,7 @@ impl ScopeArena { pub(crate) fn new_with_key( &self, - fc_ptr: FcSlot, + fc_ptr: ComponentPtr, vcomp: Box, parent_scope: Option, container: ElementId, @@ -469,7 +469,7 @@ pub struct ScopeState { pub(crate) container: ElementId, pub(crate) our_arena_idx: ScopeId, pub(crate) height: u32, - pub(crate) fnptr: FcSlot, + pub(crate) fnptr: ComponentPtr, // todo: subtrees pub(crate) is_subtree_root: Cell, diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index b9640792d..24caab579 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -10,10 +10,7 @@ pub struct ElementIdIterator<'a> { impl<'a> ElementIdIterator<'a> { pub fn new(vdom: &'a VirtualDom, node: &'a VNode<'a>) -> Self { - Self { - vdom, - stack: smallvec::smallvec![(0, node)], - } + Self { vdom, stack: smallvec::smallvec![(0, node)] } } } diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs index 044be289c..0cdc37c6d 100644 --- a/packages/core/tests/create_dom.rs +++ b/packages/core/tests/create_dom.rs @@ -36,18 +36,9 @@ fn test_original_diff() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "Hello, world!" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateTextNode { root: 3, text: "Hello, world!" }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, @@ -81,34 +72,13 @@ fn create() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "Hello, world!" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "hello" - }, - CreateTextNode { - root: 7, - text: "world" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateTextNode { root: 3, text: "Hello, world!" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "hello" }, + CreateTextNode { root: 7, text: "world" }, AppendChildren { many: 2 }, AppendChildren { many: 1 }, AppendChildren { many: 2 }, @@ -135,32 +105,14 @@ fn create_list() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - root: 4, - text: "hello" - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { root: 4, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "hello" - }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "hello" }, AppendChildren { many: 1 }, AppendChildren { many: 3 }, ] @@ -185,22 +137,10 @@ fn create_simple() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, AppendChildren { many: 4 }, ] ); @@ -235,36 +175,18 @@ fn create_components() { mutations.edits, [ CreateElement { root: 1, tag: "h1" }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "abc1" - }, + CreateElement { root: 2, tag: "div" }, + CreateTextNode { root: 3, text: "abc1" }, AppendChildren { many: 1 }, CreateElement { root: 4, tag: "p" }, CreateElement { root: 5, tag: "h1" }, - CreateElement { - root: 6, - tag: "div" - }, - CreateTextNode { - root: 7, - text: "abc2" - }, + CreateElement { root: 6, tag: "div" }, + CreateTextNode { root: 7, text: "abc2" }, AppendChildren { many: 1 }, CreateElement { root: 8, tag: "p" }, CreateElement { root: 9, tag: "h1" }, - CreateElement { - root: 10, - tag: "div" - }, - CreateTextNode { - root: 11, - text: "abc3" - }, + CreateElement { root: 10, tag: "div" }, + CreateTextNode { root: 11, text: "abc3" }, AppendChildren { many: 1 }, CreateElement { root: 12, tag: "p" }, AppendChildren { many: 9 }, @@ -285,14 +207,8 @@ fn anchors() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, CreatePlaceholder { root: 3 }, AppendChildren { many: 2 }, diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index c407359c9..6bc9b5596 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -580,7 +580,6 @@ fn keyed_diffing_additions_and_moves_in_middle() { }) }); - // LIS: 4, 5, 6 let (_, change) = dom.diff_lazynodes(left, right); log::debug!("{:#?}", change); diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 0f4f64546..8ce030147 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -152,12 +152,7 @@ fn free_works_on_root_props() { } } - let mut dom = new_dom( - app, - Custom { - val: String::from("asd"), - }, - ); + let mut dom = new_dom(app, Custom { val: String::from("asd") }); dom.rebuild(); } diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs index 41b178793..70e823569 100644 --- a/packages/core/tests/sharedstate.rs +++ b/packages/core/tests/sharedstate.rs @@ -29,10 +29,7 @@ fn shared_state_test() { assert_eq!( edits, [ - CreateTextNode { - root: 1, - text: "Hello, world!" - }, + CreateTextNode { root: 1, text: "Hello, world!" }, AppendChildren { many: 1 }, ] ); diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index 5d1dbcdf8..83b4ff35f 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -68,15 +68,9 @@ fn conditional_rendering() { mutations.edits, [ CreateElement { root: 1, tag: "h1" }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "span" - }, + CreateElement { root: 3, tag: "span" }, CreateTextNode { root: 4, text: "a" }, AppendChildren { many: 1 }, CreatePlaceholder { root: 5 }, From 00aa0e5e86d298c8498b15c742d0bbe3981dd649 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 02:44:16 -0500 Subject: [PATCH 070/256] fmt: apply formatting just to tests --- packages/core/src/events.rs | 4 +- packages/core/src/lazynodes.rs | 27 +++++++++--- packages/core/src/mutations.rs | 34 ++++++++++++--- packages/core/src/nodes.rs | 58 +++++++++++++++++-------- packages/core/src/scopes.rs | 29 ++++++++++--- packages/core/src/util.rs | 5 ++- packages/core/{ => tests}/.rustfmt.toml | 0 7 files changed, 119 insertions(+), 38 deletions(-) rename packages/core/{ => tests}/.rustfmt.toml (100%) diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 597a12992..8386cedd8 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -12,7 +12,9 @@ pub(crate) struct BubbleState { impl BubbleState { pub fn new() -> Self { - Self { canceled: Cell::new(false) } + Self { + canceled: Cell::new(false), + } } } diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 0fbed45e1..1340c916c 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -55,7 +55,9 @@ impl<'a, 'b> LazyNodes<'a, 'b> { fac.map(inner) }; - Self { inner: StackNodeStorage::Heap(Box::new(val)) } + Self { + inner: StackNodeStorage::Heap(Box::new(val)), + } } pub fn new(_val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self { @@ -69,7 +71,9 @@ impl<'a, 'b> LazyNodes<'a, 'b> { // miri does not know how to work with mucking directly into bytes if cfg!(miri) { - Self { inner: StackNodeStorage::Heap(Box::new(val)) } + Self { + inner: StackNodeStorage::Heap(Box::new(val)), + } } else { unsafe { LazyNodes::new_inner(val) } } @@ -113,7 +117,9 @@ impl<'a, 'b> LazyNodes<'a, 'b> { let max_size = mem::size_of::(); if stored_size > max_size { - Self { inner: StackNodeStorage::Heap(Box::new(val)) } + Self { + inner: StackNodeStorage::Heap(Box::new(val)), + } } else { let mut buf: StackHeapSize = StackHeapSize::default(); @@ -137,7 +143,13 @@ impl<'a, 'b> LazyNodes<'a, 'b> { std::mem::forget(val); - Self { inner: StackNodeStorage::Stack(LazyStack { _align: [], buf, dropped: false }) } + Self { + inner: StackNodeStorage::Stack(LazyStack { + _align: [], + buf, + dropped: false, + }), + } } } @@ -286,7 +298,12 @@ mod tests { } let inner = Rc::new(0); - let mut dom = VirtualDom::new_with_props(app, AppProps { inner: inner.clone() }); + let mut dom = VirtualDom::new_with_props( + app, + AppProps { + inner: inner.clone(), + }, + ); dom.rebuild(); drop(dom); diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 6a0785e23..5af727154 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -115,7 +115,11 @@ use DomEdit::*; impl<'a> Mutations<'a> { pub(crate) fn new() -> Self { - Self { edits: Vec::new(), refs: Vec::new(), diffed_scopes: Default::default() } + Self { + edits: Vec::new(), + refs: Vec::new(), + diffed_scopes: Default::default(), + } } // Navigation @@ -174,12 +178,19 @@ impl<'a> Mutations<'a> { // events pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) { - let Listener { event, mounted_node, .. } = listener; + let Listener { + event, + mounted_node, + .. + } = listener; let element_id = mounted_node.get().unwrap().as_u64(); - self.edits - .push(NewEventListener { scope, event_name: event, root: element_id }); + self.edits.push(NewEventListener { + scope, + event_name: event, + root: element_id, + }); } pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: u64) { self.edits.push(RemoveEventListener { event, root }); @@ -191,10 +202,19 @@ impl<'a> Mutations<'a> { } pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute, root: u64) { - let Attribute { name, value, namespace, .. } = attribute; + let Attribute { + name, + value, + namespace, + .. + } = attribute; - self.edits - .push(SetAttribute { field: name, value, ns: *namespace, root }); + self.edits.push(SetAttribute { + field: name, + value, + ns: *namespace, + root, + }); } pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 264fb56bb..b849819f0 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -365,7 +365,9 @@ pub struct EventHandler<'bump, T = ()> { impl<'a, T> Default for EventHandler<'a, T> { fn default() -> Self { - Self { callback: RefCell::new(None) } + Self { + callback: RefCell::new(None), + } } } @@ -440,7 +442,10 @@ pub struct NodeFactory<'a> { impl<'a> NodeFactory<'a> { pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> { - NodeFactory { scope, bump: &scope.wip_frame().bump } + NodeFactory { + scope, + bump: &scope.wip_frame().bump, + } } #[inline] @@ -450,10 +455,11 @@ impl<'a> NodeFactory<'a> { /// Directly pass in text blocks without the need to use the format_args macro. pub fn static_text(&self, text: &'static str) -> VNode<'a> { - VNode::Text( - self.bump - .alloc(VText { id: empty_cell(), text, is_static: true }), - ) + VNode::Text(self.bump.alloc(VText { + id: empty_cell(), + text, + is_static: true, + })) } /// Parses a lazy text Arguments and returns a string and a flag indicating if the text is 'static @@ -476,7 +482,11 @@ impl<'a> NodeFactory<'a> { pub fn text(&self, args: Arguments) -> VNode<'a> { let (text, is_static) = self.raw_text(args); - VNode::Text(self.bump.alloc(VText { text, is_static, id: empty_cell() })) + VNode::Text(self.bump.alloc(VText { + text, + is_static, + id: empty_cell(), + })) } pub fn element( @@ -534,7 +544,13 @@ impl<'a> NodeFactory<'a> { is_volatile: bool, ) -> Attribute<'a> { let (value, is_static) = self.raw_text(val); - Attribute { name, value, is_static, namespace, is_volatile } + Attribute { + name, + value, + is_static, + namespace, + is_volatile, + } } pub fn component

( @@ -578,7 +594,11 @@ impl<'a> NodeFactory<'a> { } pub fn listener(self, event: &'static str, callback: InternalHandler<'a>) -> Listener<'a> { - Listener { event, mounted_node: Cell::new(None), callback } + Listener { + event, + mounted_node: Cell::new(None), + callback, + } } pub fn fragment_root<'b, 'c>( @@ -594,10 +614,10 @@ impl<'a> NodeFactory<'a> { if nodes.is_empty() { VNode::Placeholder(self.bump.alloc(VPlaceholder { id: empty_cell() })) } else { - VNode::Fragment( - self.bump - .alloc(VFragment { children: nodes.into_bump_slice(), key: None }), - ) + VNode::Fragment(self.bump.alloc(VFragment { + children: nodes.into_bump_slice(), + key: None, + })) } } @@ -633,7 +653,10 @@ impl<'a> NodeFactory<'a> { ); } - VNode::Fragment(self.bump.alloc(VFragment { children, key: None })) + VNode::Fragment(self.bump.alloc(VFragment { + children, + key: None, + })) } } @@ -656,9 +679,10 @@ impl<'a> NodeFactory<'a> { } else { let children = nodes.into_bump_slice(); - Some(VNode::Fragment( - self.bump.alloc(VFragment { children, key: None }), - )) + Some(VNode::Fragment(self.bump.alloc(VFragment { + children, + key: None, + }))) } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 6b65c9197..c6c0cb846 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -315,9 +315,11 @@ impl ScopeArena { frame.node.set(unsafe { extend_vnode(node) }); } else { let frame = scope.wip_frame(); - let node = frame.bump.alloc(VNode::Placeholder( - frame.bump.alloc(VPlaceholder { id: Default::default() }), - )); + let node = frame + .bump + .alloc(VNode::Placeholder(frame.bump.alloc(VPlaceholder { + id: Default::default(), + }))); frame.node.set(unsafe { extend_vnode(node) }); } @@ -427,7 +429,10 @@ pub struct Scope<'a, P = ()> { impl

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

Clone for Scope<'_, P> { fn clone(&self) -> Self { - Self { scope: self.scope, props: self.props } + Self { + scope: self.scope, + props: self.props, + } } } @@ -789,7 +794,10 @@ impl ScopeState { /// } ///``` pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Option> { - Some(rsx.call(NodeFactory { scope: self, bump: &self.wip_frame().bump })) + Some(rsx.call(NodeFactory { + scope: self, + bump: &self.wip_frame().bump, + })) } /// Store a value between renders @@ -896,7 +904,10 @@ impl ScopeState { self.shared_contexts.get_mut().clear(); // next: reset the node data - let SelfReferentialItems { borrowed_props, listeners } = self.items.get_mut(); + let SelfReferentialItems { + borrowed_props, + listeners, + } = self.items.get_mut(); borrowed_props.clear(); listeners.clear(); self.frames[0].reset(); @@ -954,7 +965,11 @@ pub(crate) struct TaskQueue { pub(crate) type InnerTask = Pin>>; impl TaskQueue { fn new(sender: UnboundedSender) -> Rc { - Rc::new(Self { tasks: RefCell::new(FxHashMap::default()), gen: Cell::new(0), sender }) + Rc::new(Self { + tasks: RefCell::new(FxHashMap::default()), + gen: Cell::new(0), + sender, + }) } fn push_fut(&self, task: impl Future + 'static) -> TaskId { let pinned = Box::pin(task); diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 24caab579..b9640792d 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -10,7 +10,10 @@ pub struct ElementIdIterator<'a> { impl<'a> ElementIdIterator<'a> { pub fn new(vdom: &'a VirtualDom, node: &'a VNode<'a>) -> Self { - Self { vdom, stack: smallvec::smallvec![(0, node)] } + Self { + vdom, + stack: smallvec::smallvec![(0, node)], + } } } diff --git a/packages/core/.rustfmt.toml b/packages/core/tests/.rustfmt.toml similarity index 100% rename from packages/core/.rustfmt.toml rename to packages/core/tests/.rustfmt.toml From 4ae11b5756340b086d8df5137cbf27a84b8f83b9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 02:46:26 -0500 Subject: [PATCH 071/256] fmt: make tests easier to visually parse with fmt rules --- packages/core/tests/.rustfmt.toml | 1 + packages/core/tests/create_dom.rs | 140 ++++------------- packages/core/tests/diffing.rs | 233 ++++++---------------------- packages/core/tests/earlyabort.rs | 20 +-- packages/core/tests/lifecycle.rs | 63 ++------ packages/core/tests/miri_stress.rs | 7 +- packages/core/tests/sharedstate.rs | 5 +- packages/core/tests/vdom_rebuild.rs | 10 +- 8 files changed, 95 insertions(+), 384 deletions(-) create mode 100644 packages/core/tests/.rustfmt.toml diff --git a/packages/core/tests/.rustfmt.toml b/packages/core/tests/.rustfmt.toml new file mode 100644 index 000000000..70a9483a8 --- /dev/null +++ b/packages/core/tests/.rustfmt.toml @@ -0,0 +1 @@ +struct_lit_width = 80 diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs index 044be289c..0cdc37c6d 100644 --- a/packages/core/tests/create_dom.rs +++ b/packages/core/tests/create_dom.rs @@ -36,18 +36,9 @@ fn test_original_diff() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "Hello, world!" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateTextNode { root: 3, text: "Hello, world!" }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, @@ -81,34 +72,13 @@ fn create() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "Hello, world!" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "hello" - }, - CreateTextNode { - root: 7, - text: "world" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateTextNode { root: 3, text: "Hello, world!" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "hello" }, + CreateTextNode { root: 7, text: "world" }, AppendChildren { many: 2 }, AppendChildren { many: 1 }, AppendChildren { many: 2 }, @@ -135,32 +105,14 @@ fn create_list() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - root: 4, - text: "hello" - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { root: 4, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "hello" - }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "hello" }, AppendChildren { many: 1 }, AppendChildren { many: 3 }, ] @@ -185,22 +137,10 @@ fn create_simple() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, AppendChildren { many: 4 }, ] ); @@ -235,36 +175,18 @@ fn create_components() { mutations.edits, [ CreateElement { root: 1, tag: "h1" }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "abc1" - }, + CreateElement { root: 2, tag: "div" }, + CreateTextNode { root: 3, text: "abc1" }, AppendChildren { many: 1 }, CreateElement { root: 4, tag: "p" }, CreateElement { root: 5, tag: "h1" }, - CreateElement { - root: 6, - tag: "div" - }, - CreateTextNode { - root: 7, - text: "abc2" - }, + CreateElement { root: 6, tag: "div" }, + CreateTextNode { root: 7, text: "abc2" }, AppendChildren { many: 1 }, CreateElement { root: 8, tag: "p" }, CreateElement { root: 9, tag: "h1" }, - CreateElement { - root: 10, - tag: "div" - }, - CreateTextNode { - root: 11, - text: "abc3" - }, + CreateElement { root: 10, tag: "div" }, + CreateTextNode { root: 11, text: "abc3" }, AppendChildren { many: 1 }, CreateElement { root: 12, tag: "p" }, AppendChildren { many: 9 }, @@ -285,14 +207,8 @@ fn anchors() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, CreatePlaceholder { root: 3 }, AppendChildren { many: 2 }, diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 5b98ea80e..66012cf99 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -31,26 +31,14 @@ fn html_and_rsx_generate_the_same_output() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "Hello world" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "Hello world" }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, ] ); - assert_eq!( - change.edits, - [SetText { - text: "Goodbye world", - root: 2 - },] - ); + assert_eq!(change.edits, [SetText { text: "Goodbye world", root: 2 },]); } /// Should result in 3 elements on the stack @@ -67,32 +55,14 @@ fn fragments_create_properly() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "Hello a" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "Hello a" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - root: 4, - text: "Hello b" - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { root: 4, text: "Hello b" }, AppendChildren { many: 1 }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "Hello c" - }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "Hello c" }, AppendChildren { many: 1 }, AppendChildren { many: 3 }, ] @@ -116,10 +86,7 @@ fn empty_fragments_create_anchors() { assert_eq!( change.edits, [ - CreateElement { - root: 2, - tag: "div" - }, + CreateElement { root: 2, tag: "div" }, ReplaceWith { m: 1, root: 1 } ] ); @@ -141,26 +108,11 @@ fn empty_fragments_create_many_anchors() { assert_eq!( change.edits, [ - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateElement { root: 6, tag: "div" }, ReplaceWith { m: 5, root: 1 } ] ); @@ -188,32 +140,14 @@ fn empty_fragments_create_anchors_with_many_children() { assert_eq!( change.edits, [ - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "hello: 0", - root: 3 - }, + CreateElement { tag: "div", root: 2 }, + CreateTextNode { text: "hello: 0", root: 3 }, AppendChildren { many: 1 }, - CreateElement { - tag: "div", - root: 4, - }, - CreateTextNode { - text: "hello: 1", - root: 5 - }, + CreateElement { tag: "div", root: 4 }, + CreateTextNode { text: "hello: 1", root: 5 }, AppendChildren { many: 1 }, - CreateElement { - tag: "div", - root: 6, - }, - CreateTextNode { - text: "hello: 2", - root: 7 - }, + CreateElement { tag: "div", root: 6 }, + CreateTextNode { text: "hello: 2", root: 7 }, AppendChildren { many: 1 }, ReplaceWith { root: 1, m: 3 } ] @@ -236,23 +170,11 @@ fn many_items_become_fragment() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - text: "hello", - root: 2 - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { text: "hello", root: 2 }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - text: "hello", - root: 4 - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { text: "hello", root: 4 }, AppendChildren { many: 1 }, AppendChildren { many: 2 }, ] @@ -339,26 +261,11 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, CreateElement { root: 6, tag: "p" }, AppendChildren { many: 6 }, ] @@ -395,14 +302,8 @@ fn two_fragments_with_same_elements_are_differet() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, CreateElement { root: 3, tag: "p" }, AppendChildren { many: 3 }, ] @@ -410,18 +311,9 @@ fn two_fragments_with_same_elements_are_differet() { assert_eq!( change.edits, [ - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateElement { root: 6, tag: "div" }, InsertAfter { root: 2, n: 3 }, ] ); @@ -628,14 +520,8 @@ fn keyed_diffing_additions() { assert_eq!( change.edits, [ - CreateElement { - root: 6, - tag: "div" - }, - CreateElement { - root: 7, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, + CreateElement { root: 7, tag: "div" }, InsertAfter { n: 2, root: 5 } ] ); @@ -663,14 +549,8 @@ fn keyed_diffing_additions_and_moves_on_ends() { change.edits, [ // create 11, 12 - CreateElement { - tag: "div", - root: 5 - }, - CreateElement { - tag: "div", - root: 6 - }, + CreateElement { tag: "div", root: 5 }, + CreateElement { tag: "div", root: 6 }, InsertAfter { root: 3, n: 2 }, // move 7 to the front PushRoot { root: 4 }, @@ -702,24 +582,12 @@ fn keyed_diffing_additions_and_moves_in_middle() { change.edits, [ // create 13, 17 - CreateElement { - tag: "div", - root: 5 - }, - CreateElement { - tag: "div", - root: 6 - }, + CreateElement { tag: "div", root: 5 }, + CreateElement { tag: "div", root: 6 }, InsertBefore { root: 2, n: 2 }, // create 11, 12 - CreateElement { - tag: "div", - root: 7 - }, - CreateElement { - tag: "div", - root: 8 - }, + CreateElement { tag: "div", root: 7 }, + CreateElement { tag: "div", root: 8 }, InsertBefore { root: 3, n: 2 }, // move 7 PushRoot { root: 4 }, @@ -756,16 +624,10 @@ fn controlled_keyed_diffing_out_of_order() { // remove 7 // create 9 and insert before 6 - CreateElement { - root: 5, - tag: "div" - }, + CreateElement { root: 5, tag: "div" }, InsertBefore { n: 1, root: 3 }, // create 0 and insert before 5 - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, InsertBefore { n: 1, root: 2 }, ] ); @@ -792,10 +654,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { assert_eq!( changes.edits, [ - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, InsertBefore { n: 1, root: 3 }, PushRoot { root: 4 }, InsertBefore { n: 1, root: 1 }, diff --git a/packages/core/tests/earlyabort.rs b/packages/core/tests/earlyabort.rs index 7dc2ae4d4..ae26c97b6 100644 --- a/packages/core/tests/earlyabort.rs +++ b/packages/core/tests/earlyabort.rs @@ -42,14 +42,8 @@ fn test_early_abort() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 1, - }, - CreateTextNode { - text: "Hello, world!", - root: 2, - }, + CreateElement { tag: "div", root: 1 }, + CreateTextNode { text: "Hello, world!", root: 2 }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, ] @@ -65,14 +59,8 @@ fn test_early_abort() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "Hello, world!", - root: 4, - }, + CreateElement { tag: "div", root: 2 }, + CreateTextNode { text: "Hello, world!", root: 4 }, AppendChildren { many: 1 }, ReplaceWith { root: 3, m: 1 }, ] diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 6c0df4e1e..8b7d7bb4a 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -27,12 +27,7 @@ fn manual_diffing() { }; let value = Arc::new(Mutex::new("Hello")); - let mut dom = VirtualDom::new_with_props( - App, - AppProps { - value: value.clone(), - }, - ); + let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone() }); let _ = dom.rebuild(); @@ -74,28 +69,12 @@ fn events_generate() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 1, - }, - NewEventListener { - event_name: "click", - scope: ScopeId(0), - root: 1, - }, - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "nested", - root: 3, - }, + CreateElement { tag: "div", root: 1 }, + NewEventListener { event_name: "click", scope: ScopeId(0), root: 1 }, + CreateElement { tag: "div", root: 2 }, + CreateTextNode { text: "nested", root: 3 }, AppendChildren { many: 1 }, - CreateTextNode { - text: "Click me!", - root: 4, - }, + CreateTextNode { text: "Click me!", root: 4 }, AppendChildren { many: 2 }, AppendChildren { many: 1 }, ] @@ -132,10 +111,7 @@ fn components_generate() { assert_eq!( edits.edits, [ - CreateTextNode { - text: "Text0", - root: 1, - }, + CreateTextNode { text: "Text0", root: 1 }, AppendChildren { many: 1 }, ] ); @@ -144,10 +120,7 @@ fn components_generate() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 2, - }, + CreateElement { tag: "div", root: 2 }, ReplaceWith { root: 1, m: 1 }, ] ); @@ -156,10 +129,7 @@ fn components_generate() { assert_eq!( edits.edits, [ - CreateTextNode { - text: "Text2", - root: 3, - }, + CreateTextNode { text: "Text2", root: 3 }, ReplaceWith { root: 2, m: 1 }, ] ); @@ -183,10 +153,7 @@ fn components_generate() { assert_eq!( edits.edits, [ - CreateTextNode { - text: "text 3", - root: 6, - }, + CreateTextNode { text: "text 3", root: 6 }, ReplaceWith { root: 5, m: 1 }, ] ); @@ -195,14 +162,8 @@ fn components_generate() { assert_eq!( edits.edits, [ - CreateTextNode { - text: "text 0", - root: 7, - }, - CreateTextNode { - text: "text 1", - root: 8, - }, + CreateTextNode { text: "text 0", root: 7 }, + CreateTextNode { text: "text 1", root: 8 }, ReplaceWith { root: 6, m: 2 }, ] ); diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 6b7805261..87d108094 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -152,12 +152,7 @@ fn free_works_on_root_props() { } } - let mut dom = new_dom( - app, - Custom { - val: String::from("asd"), - }, - ); + let mut dom = new_dom(app, Custom { val: String::from("asd") }); dom.rebuild(); } diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs index 3f04aa154..38195ccd8 100644 --- a/packages/core/tests/sharedstate.rs +++ b/packages/core/tests/sharedstate.rs @@ -29,10 +29,7 @@ fn shared_state_test() { assert_eq!( edits, [ - CreateTextNode { - root: 1, - text: "Hello, world!" - }, + CreateTextNode { root: 1, text: "Hello, world!" }, AppendChildren { many: 1 }, ] ); diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index 5d1dbcdf8..83b4ff35f 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -68,15 +68,9 @@ fn conditional_rendering() { mutations.edits, [ CreateElement { root: 1, tag: "h1" }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "span" - }, + CreateElement { root: 3, tag: "span" }, CreateTextNode { root: 4, text: "a" }, AppendChildren { many: 1 }, CreatePlaceholder { root: 5 }, From 0820e18d3dd1aac2f713251dfaddc6a8ebb041d0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 02:49:37 -0500 Subject: [PATCH 072/256] feat: make component name accessible --- packages/core-macro/src/rsx/component.rs | 3 +++ packages/core/src/nodes.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 5257022a4..4e9d449cf 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -135,11 +135,14 @@ impl ToTokens for Component { None => quote! { None }, }; + let fn_name = self.name.segments.last().unwrap().ident.to_string(); + tokens.append_all(quote! { __cx.component( #name, #builder, #key_token, + #fn_name ) }) } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index b18aeaf2d..1ce39a682 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -385,6 +385,7 @@ pub struct VComponent<'src> { pub scope: Cell>, pub can_memoize: bool, pub user_fc: *const (), + pub fn_name: &'static str, pub props: RefCell>>, } @@ -549,6 +550,7 @@ impl<'a> NodeFactory<'a> { component: fn(Scope<'a, P>) -> Element, props: P, key: Option, + fn_name: &'static str, ) -> VNode<'a> where P: Properties + 'a, @@ -559,6 +561,7 @@ impl<'a> NodeFactory<'a> { can_memoize: P::IS_STATIC, user_fc: component as *const (), originator: self.scope.scope_id(), + fn_name, props: RefCell::new(Some(Box::new(VComponentProps { // local_props: RefCell::new(Some(props)), // heap_props: RefCell::new(None), From a4ea0ba4fe3b615179b01fab3c4704a938d5a5e4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 02:52:40 -0500 Subject: [PATCH 073/256] chore: undo dirty_scopes rename --- packages/core/src/mutations.rs | 6 +++--- packages/core/src/virtual_dom.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 5af727154..bd94d0b71 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -17,7 +17,7 @@ use std::{any::Any, fmt::Debug}; /// Mutations are the only link between the RealDOM and the VirtualDOM. pub struct Mutations<'a> { pub edits: Vec>, - pub diffed_scopes: FxHashSet, + pub dirty_scopes: FxHashSet, pub refs: Vec>, } @@ -118,7 +118,7 @@ impl<'a> Mutations<'a> { Self { edits: Vec::new(), refs: Vec::new(), - diffed_scopes: Default::default(), + dirty_scopes: Default::default(), } } @@ -223,7 +223,7 @@ impl<'a> Mutations<'a> { } pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) { - self.diffed_scopes.insert(scope); + self.dirty_scopes.insert(scope); } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 9c4cd4018..b273e6239 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -480,8 +480,8 @@ impl VirtualDom { let AsyncDiffState { mutations, .. } = diff_state; - log::debug!("succesffuly resolved scopes {:?}", mutations.diffed_scopes); - for scope in &mutations.diffed_scopes { + log::debug!("succesffuly resolved scopes {:?}", mutations.dirty_scopes); + for scope in &mutations.dirty_scopes { self.dirty_scopes.remove(scope); } From 9dda7b168b15e31f739780c04b772d9dfa09066e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 03:04:47 -0500 Subject: [PATCH 074/256] chore: clean up scopes --- packages/core/src/scopes.rs | 7 +------ packages/core/src/virtual_dom.rs | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index c6c0cb846..b90ff6f6e 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -213,9 +213,7 @@ impl ScopeArena { } pub fn collect_garbage(&self, id: ElementId) { - let node = self.nodes.borrow_mut().get(id.0).unwrap().clone(); - // let node = self.nodes.borrow_mut().remove(id.0); - // log::debug!("collecting garbage for {:?}, {:?}", id, unsafe { &*node }); + self.nodes.borrow_mut().remove(id.0); } /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from @@ -231,8 +229,6 @@ impl ScopeArena { /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will /// be dropped. pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { - // log::trace!("Ensuring drop safety for scope {:?}", scope_id); - if let Some(scope) = self.get_scope(scope_id) { let mut items = scope.items.borrow_mut(); @@ -243,7 +239,6 @@ impl ScopeArena { if let Some(scope_id) = comp.scope.get() { self.ensure_drop_safety(scope_id); } - drop(comp.props.take()); }); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index b273e6239..670613dd4 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -449,6 +449,7 @@ impl VirtualDom { /// apply_mutations(mutations); /// } /// ``` + #[allow(unused)] pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec { let mut committed_mutations = vec![]; @@ -487,13 +488,12 @@ impl VirtualDom { committed_mutations.push(mutations); + // todo: pause the diff machine // if diff_state.work(&mut deadline) { // let DiffState { mutations, .. } = diff_state; - // for scope in &mutations.dirty_scopes { // self.dirty_scopes.remove(scope); // } - // committed_mutations.push(mutations); // } else { // // leave the work in an incomplete state From 120ee1836812f0ebf5c82685817f0866aee25f45 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 03:22:34 -0500 Subject: [PATCH 075/256] chore: improve debugability of fcptrs and vcomponents this commit changes the fc slot type from a thin pointer to a c pointer. this lets us provide frame inspection of components using backtrace. --- packages/core/src/nodes.rs | 17 ++++++++++++----- packages/core/src/scopes.rs | 6 +++--- packages/core/src/virtual_dom.rs | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1ce39a682..bc76e6bb9 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -4,7 +4,7 @@ //! cheap and *very* fast to construct - building a full tree should be quick. use crate::{ - innerlude::{Element, Properties, Scope, ScopeId, ScopeState}, + innerlude::{ComponentPtr, Element, Properties, Scope, ScopeId, ScopeState}, lazynodes::LazyNodes, AnyEvent, Component, }; @@ -177,11 +177,18 @@ impl Debug for VNode<'_> { .field("children", &el.children) .finish(), VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text), - VNode::Placeholder(_) => write!(s, "VNode::VPlaceholder"), + VNode::Placeholder(t) => write!(s, "VNode::VPlaceholder {{ id: {:?} }}", t.id), VNode::Fragment(frag) => { write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children) } - VNode::Component(comp) => write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc), + VNode::Component(comp) => s + .debug_struct("VNode::VComponent") + .field("name", &comp.fn_name) + .field("fnptr", &comp.user_fc) + .field("key", &comp.key) + .field("scope", &comp.scope) + .field("originator", &comp.originator) + .finish(), } } } @@ -384,7 +391,7 @@ pub struct VComponent<'src> { pub originator: ScopeId, pub scope: Cell>, pub can_memoize: bool, - pub user_fc: *const (), + pub user_fc: ComponentPtr, pub fn_name: &'static str, pub props: RefCell>>, } @@ -559,7 +566,7 @@ impl<'a> NodeFactory<'a> { key: key.map(|f| self.raw_text(f).0), scope: Default::default(), can_memoize: P::IS_STATIC, - user_fc: component as *const (), + user_fc: component as ComponentPtr, originator: self.scope.scope_id(), fn_name, props: RefCell::new(Some(Box::new(VComponentProps { diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 77c648d18..2faa47dab 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -13,7 +13,7 @@ use std::{ rc::Rc, }; -pub(crate) type FcSlot = *const (); +pub(crate) type ComponentPtr = *mut std::os::raw::c_void; pub(crate) struct Heuristic { hook_arena_size: usize, @@ -28,7 +28,7 @@ pub(crate) struct ScopeArena { pub scope_gen: Cell, pub bump: Bump, pub scopes: RefCell>, - pub heuristics: RefCell>, + pub heuristics: RefCell>, pub free_scopes: RefCell>, pub nodes: RefCell>>, pub tasks: Rc, @@ -82,7 +82,7 @@ impl ScopeArena { pub(crate) fn new_with_key( &self, - fc_ptr: *const (), + fc_ptr: ComponentPtr, vcomp: Box, parent_scope: Option, container: ElementId, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 4e426caad..0bd93335f 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -212,7 +212,7 @@ impl VirtualDom { let scopes = ScopeArena::new(channel.0.clone()); scopes.new_with_key( - root as *const _, + root as ComponentPtr, Box::new(VComponentProps { props: root_props, memo: |_a, _b| unreachable!("memo on root will neve be run"), From b4697fc9f93377c497a8eb25fa56c4e3c347c4ef Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 03:27:43 -0500 Subject: [PATCH 076/256] chore: clean up some more of the scopes file --- packages/core/src/diff.rs | 4 ++-- packages/core/src/scopes.rs | 34 +++++++------------------------- packages/core/src/virtual_dom.rs | 6 +++--- 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index db898f194..572ad7f66 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -2,7 +2,7 @@ use crate::innerlude::*; use fxhash::{FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -pub(crate) struct AsyncDiffState<'bump> { +pub(crate) struct DiffState<'bump> { pub(crate) scopes: &'bump ScopeArena, pub(crate) mutations: Mutations<'bump>, pub(crate) force_diff: bool, @@ -10,7 +10,7 @@ pub(crate) struct AsyncDiffState<'bump> { pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, } -impl<'b> AsyncDiffState<'b> { +impl<'b> DiffState<'b> { pub fn new(scopes: &'b ScopeArena) -> Self { Self { scopes, diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index b90ff6f6e..ea935b433 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -186,7 +186,7 @@ impl ScopeArena { let scope = unsafe { &mut *self.scopes.borrow_mut().remove(&id).unwrap() }; scope.reset(); - // self.free_scopes.borrow_mut().push(scope); + self.free_scopes.borrow_mut().push(scope); Some(()) } @@ -202,14 +202,11 @@ impl ScopeArena { } pub fn update_node<'a>(&self, node: &'a VNode<'a>, id: ElementId) { - let node = unsafe { extend_vnode(node) }; - - let mut nodes = self.nodes.borrow_mut(); - let entry = nodes.get_mut(id.0); - match entry { - Some(_node) => *_node = node, - None => panic!("cannot update node {}", id), - } + *self + .nodes + .borrow_mut() + .get_mut(id.0) + .expect("node to exist if it's being updated") = unsafe { extend_vnode(node) }; } pub fn collect_garbage(&self, id: ElementId) { @@ -259,24 +256,13 @@ impl ScopeArena { // todo: we *know* that this is aliased by the contents of the scope itself let scope = unsafe { &mut *self.get_scope_raw(id).expect("could not find scope") }; - // if cfg!(debug_assertions) { - // log::debug!("running scope {:?} symbol: {:?}", id, scope.fnptr); - - // todo: resolve frames properly - backtrace::resolve(scope.fnptr, |symbol| { - // backtrace::resolve(scope.fnptr as *mut std::os::raw::c_void, |symbol| { - // panic!("asd"); - // log::trace!("Running scope {:?}, ptr {:?}", id, scope.fnptr); - log::debug!("running scope {:?} symbol: {:?}", id, symbol.name()); - }); - // } + log::trace!("running scope {:?} symbol: {:?}", id, scope.fnptr); // Safety: // - We dropped the listeners, so no more &mut T can be used while these are held // - All children nodes that rely on &mut T are replaced with a new reference scope.hook_idx.set(0); - // book keeping to ensure safety around the borrowed data { // Safety: // - We've dropped all references to the wip bump frame with "ensure_drop_safety" @@ -289,8 +275,6 @@ impl ScopeArena { debug_assert!(items.borrowed_props.is_empty()); } - // safety: this is definitely not dropped - /* If the component returns None, then we fill in a placeholder node. This will wipe what was there. An alternate approach is to leave the Real Dom the same, but that can lead to safety issues and a lot more checks. @@ -332,14 +316,10 @@ impl ScopeArena { while let Some(id) = cur_el.take() { if let Some(el) = nodes.get(id.0) { - log::trace!("Found valid receiver element"); - let real_el = unsafe { &**el }; if let VNode::Element(real_el) = real_el { for listener in real_el.listeners.borrow().iter() { if listener.event == event.name { - log::trace!("Found valid receiver event"); - if state.canceled.get() { // stop bubbling if canceled break; diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 5af569de1..a7092104c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -2,7 +2,7 @@ //! //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. -use crate::diff::AsyncDiffState as DiffState; +use crate::diff::DiffState; use crate::innerlude::*; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::{future::poll_fn, StreamExt}; @@ -455,7 +455,7 @@ impl VirtualDom { while !self.dirty_scopes.is_empty() { let scopes = &self.scopes; - let mut diff_state = AsyncDiffState::new(scopes); + let mut diff_state = DiffState::new(scopes); let mut ran_scopes = FxHashSet::default(); @@ -479,7 +479,7 @@ impl VirtualDom { diff_state.diff_scope(scopeid); - let AsyncDiffState { mutations, .. } = diff_state; + let DiffState { mutations, .. } = diff_state; log::debug!("succesffuly resolved scopes {:?}", mutations.dirty_scopes); for scope in &mutations.dirty_scopes { From 923fb0701d91eafb9f7225ba376154b072728645 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 03:34:52 -0500 Subject: [PATCH 077/256] fix: clippy --- packages/core/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index dba4effbb..e81ddca87 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -12,7 +12,6 @@ pub(crate) mod util; pub(crate) mod virtual_dom; pub(crate) mod innerlude { - pub(crate) use crate::diff::*; pub use crate::events::*; pub use crate::lazynodes::*; pub use crate::mutations::*; From 366cf758870f6bc54c36acfecfbda261ed28bebf Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 31 Jan 2022 19:39:44 +0800 Subject: [PATCH 078/256] feat: add `use_desktop_context` hook --- examples/borderless.rs | 10 +++------- packages/desktop/src/desktop_context.rs | 10 ++++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/examples/borderless.rs b/examples/borderless.rs index e6cd4eda1..15756503e 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -1,5 +1,4 @@ use dioxus::prelude::*; -use dioxus_desktop::desktop_context::DesktopContext; fn main() { dioxus::desktop::launch_cfg(app, |cfg| { @@ -8,16 +7,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let desktop = cx.consume_context::().unwrap(); - - let drag = desktop.clone(); - let close = desktop.clone(); + let desktop = dioxus::desktop::desktop_context::use_desktop_context(&cx); cx.render(rsx!( link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } header { class: "text-gray-400 bg-gray-900 body-font", - onmousedown: move |_| drag.drag_window(), + onmousedown: move |_| desktop.drag_window(), div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", @@ -33,7 +29,7 @@ fn app(cx: Scope) -> Element { button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| close.close(), + onclick: move |_| desktop.close(), "Close" } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 0a1fa2b02..0e2fbdddd 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,3 +1,6 @@ +use std::rc::Rc; + +use dioxus_core::ScopeState; use wry::application::event_loop::EventLoopProxy; use crate::UserWindowEvent; @@ -50,3 +53,10 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); } } + +/// use this function can get the `DesktopContext` context. +pub fn use_desktop_context(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| cx.consume_context::()) + .as_ref() + .unwrap() +} From 7ca92be015090ff2f4c44d778f10e97edcf90a92 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 31 Jan 2022 19:44:40 +0800 Subject: [PATCH 079/256] feat: add `focus` api --- packages/desktop/src/desktop_context.rs | 5 +++++ packages/desktop/src/lib.rs | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 0e2fbdddd..a41712904 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -52,6 +52,11 @@ impl DesktopContext { pub fn close(&self) { let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); } + + /// set window to focus + pub fn focus(&self) { + let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); + } } /// use this function can get the `DesktopContext` context. diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 45314df34..eb76ce63d 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -313,6 +313,12 @@ pub fn launch_with_props( window.set_maximized(state); } } + UserWindowEvent::FocusWindow => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_focus(); + } + } } } Event::MainEventsCleared => {} @@ -329,6 +335,7 @@ pub enum UserWindowEvent { Update, DragWindow, CloseWindow, + FocusWindow, Minimize(bool), Maximize(bool), } From 06418f73db4bf3721d2d9ca9bf2fdd184663158d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 12:29:11 -0500 Subject: [PATCH 080/256] fix: element stack not being updated properly --- packages/core/src/diff.rs | 2 ++ packages/core/src/scopes.rs | 15 +++++++------- packages/router/src/service.rs | 37 +--------------------------------- 3 files changed, 10 insertions(+), 44 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 572ad7f66..1a53b01c4 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -166,6 +166,8 @@ impl<'b> DiffState<'b> { self.create_and_append_children(children); } + self.element_stack.pop(); + 1 } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ea935b433..00c3024bf 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -96,10 +96,9 @@ impl ScopeArena { // Get the height of the scope let height = parent_scope - .map(|id| self.get_scope(id).map(|scope| scope.height)) + .map(|id| self.get_scope(id).map(|scope| scope.height + 1)) .flatten() - .unwrap_or_default() - + 1; + .unwrap_or_default(); let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten(); @@ -202,11 +201,8 @@ impl ScopeArena { } pub fn update_node<'a>(&self, node: &'a VNode<'a>, id: ElementId) { - *self - .nodes - .borrow_mut() - .get_mut(id.0) - .expect("node to exist if it's being updated") = unsafe { extend_vnode(node) }; + let node = unsafe { extend_vnode(node) }; + *self.nodes.borrow_mut().get_mut(id.0).unwrap() = node; } pub fn collect_garbage(&self, id: ElementId) { @@ -317,9 +313,12 @@ impl ScopeArena { while let Some(id) = cur_el.take() { if let Some(el) = nodes.get(id.0) { let real_el = unsafe { &**el }; + log::debug!("looking for listener on {:?}", real_el); + if let VNode::Element(real_el) = real_el { for listener in real_el.listeners.borrow().iter() { if listener.event == event.name { + log::debug!("calling listener {:?}", listener.event); if state.canceled.get() { // stop bubbling if canceled break; diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index b6d8ba11b..a4ab4dc0d 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -62,12 +62,10 @@ impl RouterService { root_found.set(None); // checking if the route is valid is cheap, so we do it for (slot, root) in slots.borrow_mut().iter().rev() { - // log::trace!("regenerating slot {:?} for root '{}'", slot, root); regen_route(*slot); } for listener in onchange_listeners.borrow_mut().iter() { - // log::trace!("regenerating listener {:?}", listener); regen_route(*listener); } @@ -91,31 +89,24 @@ impl RouterService { } pub fn push_route(&self, route: &str) { - // log::trace!("Pushing route: {}", route); self.history.borrow_mut().push(route); } pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { let clean = clean_route(route); - // log::trace!("Registered route '{}' with scope id {:?}", clean, scope); self.slots.borrow_mut().push((scope, clean)); } pub fn should_render(&self, scope: ScopeId) -> bool { - // log::trace!("Should render scope id {:?}?", scope); if let Some(root_id) = self.root_found.get() { - // log::trace!(" we already found a root with scope id {:?}", root_id); if root_id == scope { - // log::trace!(" yes - it's a match"); return true; } - // log::trace!(" no - it's not a match"); return false; } let location = self.history.borrow().location(); let path = location.path(); - // log::trace!(" current path is '{}'", path); let roots = self.slots.borrow(); @@ -124,23 +115,15 @@ impl RouterService { // fallback logic match root { Some((id, route)) => { - // log::trace!( - // " matched given scope id {:?} with route root '{}'", - // scope, - // route, - // ); if let Some(params) = route_matches_path(route, path) { - // log::trace!(" and it matches the current path '{}'", path); self.root_found.set(Some(*id)); *self.cur_path_params.borrow_mut() = params; true } else { if route == "" { - // log::trace!(" and the route is the root, so we will use that without a better match"); self.root_found.set(Some(*id)); true } else { - // log::trace!(" and the route '{}' is not the root nor does it match the current path", route); false } } @@ -158,12 +141,10 @@ impl RouterService { } pub fn subscribe_onchange(&self, id: ScopeId) { - // log::trace!("Subscribing onchange for scope id {:?}", id); self.onchange_listeners.borrow_mut().insert(id); } pub fn unsubscribe_onchange(&self, id: ScopeId) { - // log::trace!("Subscribing onchange for scope id {:?}", id); self.onchange_listeners.borrow_mut().remove(&id); } } @@ -186,36 +167,20 @@ fn route_matches_path(route: &str, path: &str) -> Option let route_pieces = route.split('/').collect::>(); let path_pieces = clean_path(path).split('/').collect::>(); - // log::trace!( - // " checking route pieces {:?} vs path pieces {:?}", - // route_pieces, - // path_pieces, - // ); - if route_pieces.len() != path_pieces.len() { - // log::trace!(" the routes are different lengths"); return None; } let mut matches = HashMap::new(); for (i, r) in route_pieces.iter().enumerate() { - // log::trace!(" checking route piece '{}' vs path", r); // If this is a parameter then it matches as long as there's // _any_thing in that spot in the path. if r.starts_with(':') { - // log::trace!( - // " route piece '{}' starts with a colon so it matches anything", - // r, - // ); let param = &r[1..]; matches.insert(param.to_string(), path_pieces[i].to_string()); continue; } - // log::trace!( - // " route piece '{}' must be an exact match for path piece '{}'", - // r, - // path_pieces[i], - // ); + if path_pieces[i] != *r { return None; } From c4e6496d9d7064d97359edd7ae267d4bc6b98843 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 12:49:21 -0500 Subject: [PATCH 081/256] chore: enable a pedantic clippy on the diffing algorithm --- packages/core/src/diff.rs | 130 ++++++++++++++++--------------------- packages/core/src/nodes.rs | 7 -- 2 files changed, 56 insertions(+), 81 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 1a53b01c4..d40f49934 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,4 +1,10 @@ -use crate::innerlude::*; +#![warn(clippy::pedantic)] +#![allow(clippy::cast_possible_truncation)] + +use crate::innerlude::{ + AnyProps, ElementId, Mutations, ScopeArena, ScopeId, VComponent, VElement, VFragment, VNode, + VPlaceholder, VText, +}; use fxhash::{FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; @@ -33,7 +39,7 @@ impl<'b> DiffState<'b> { } pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) { - use VNode::*; + use VNode::{Component, Element, Fragment, Placeholder, Text}; match (old_node, new_node) { // Check the most common cases first // these are *actual* elements, not wrappers around lists @@ -61,7 +67,7 @@ impl<'b> DiffState<'b> { if let Some(root) = old.id.get() { self.scopes.update_node(new_node, root); - new.id.set(Some(root)) + new.id.set(Some(root)); } } @@ -70,7 +76,7 @@ impl<'b> DiffState<'b> { log::trace!("skipping node diff - element are the sames"); return; } - self.diff_element_nodes(old, new, old_node, new_node) + self.diff_element_nodes(old, new, old_node, new_node); } // These two sets are pointers to nodes but are not actually nodes themselves @@ -79,7 +85,7 @@ impl<'b> DiffState<'b> { log::trace!("skipping node diff - placeholder are the sames"); return; } - self.diff_component_nodes(old_node, new_node, *old, *new) + self.diff_component_nodes(old_node, new_node, *old, *new); } (Fragment(old), Fragment(new)) => { @@ -87,7 +93,7 @@ impl<'b> DiffState<'b> { log::trace!("skipping node diff - fragment are the sames"); return; } - self.diff_fragment_nodes(old, new) + self.diff_fragment_nodes(old, new); } // Anything else is just a basic replace and create @@ -136,17 +142,15 @@ impl<'b> DiffState<'b> { .. } = element; - // set the parent ID for event bubbling - // self.stack.instructions.push(DiffInstruction::PopElement); - let parent = self.element_stack.last().unwrap(); parent_id.set(Some(*parent)); // set the id of the element let real_id = self.scopes.reserve_node(node); - self.element_stack.push(real_id); dom_id.set(Some(real_id)); + self.element_stack.push(real_id); + self.mutations.create_element(tag_name, *namespace, real_id); if let Some(cur_scope_id) = self.current_scope() { @@ -155,7 +159,7 @@ impl<'b> DiffState<'b> { self.mutations.new_event_listener(listener, cur_scope_id); } } else { - log::warn!("create element called with no scope on the stack - this is an error for a live dom"); + log::trace!("create element called with no scope on the stack - this is an error for a live dom"); } for attr in *attributes { @@ -198,7 +202,7 @@ impl<'b> DiffState<'b> { new_idx }; - log::info!( + log::trace!( "created component \"{}\", id: {:?} parent {:?} orig: {:?}", vcomponent.fn_name, new_idx, @@ -209,14 +213,11 @@ impl<'b> DiffState<'b> { // Actually initialize the caller's slot with the right address vcomponent.scope.set(Some(new_idx)); - match vcomponent.can_memoize { - true => { - // todo: implement promotion logic. save us from boxing props that we don't need - } - false => { - // track this component internally so we know the right drop order - } - } + // if vcomponent.can_memoize { + // // todo: implement promotion logic. save us from boxing props that we don't need + // } else { + // // track this component internally so we know the right drop order + // } self.enter_scope(new_idx); @@ -278,7 +279,7 @@ impl<'b> DiffState<'b> { self.mutations.remove_attribute(attribute, root.as_u64()); } for attribute in new.attributes { - self.mutations.set_attribute(attribute, root.as_u64()) + self.mutations.set_attribute(attribute, root.as_u64()); } } @@ -318,9 +319,7 @@ impl<'b> DiffState<'b> { let created = self.create_children(new.children); self.mutations.append_children(created as u32); } - (_, _) => { - self.diff_children(old.children, new.children); - } + (_, _) => self.diff_children(old.children, new.children), }; } @@ -332,14 +331,8 @@ impl<'b> DiffState<'b> { new: &'b VComponent<'b>, ) { let scope_addr = old.scope.get().unwrap(); - log::trace!( - "diff_component_nodes. old: {:#?} new: {:#?}", - old_node, - new_node - ); if std::ptr::eq(old, new) { - log::trace!("skipping component diff - component is the sames"); return; } @@ -397,7 +390,6 @@ impl<'b> DiffState<'b> { self.leave_scope(); } else { - log::debug!("scope stack is {:#?}", self.scope_stack); self.replace_node(old_node, new_node); } } @@ -405,14 +397,12 @@ impl<'b> DiffState<'b> { fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) { // This is the case where options or direct vnodes might be used. // In this case, it's faster to just skip ahead to their diff - // if old.children.len() == 1 && new.children.len() == 1 { - // if std::ptr::eq(old, new) { - // log::debug!("skipping fragment diff - fragment is the same"); - // return; - // } - // self.diff_node(&old.children[0], &new.children[0]); - // return; - // } + if old.children.len() == 1 && new.children.len() == 1 { + if !std::ptr::eq(old, new) { + self.diff_node(&old.children[0], &new.children[0]); + } + return; + } debug_assert!(!old.children.is_empty()); debug_assert!(!new.children.is_empty()); @@ -441,7 +431,6 @@ impl<'b> DiffState<'b> { // to an element, and appending makes sense. fn diff_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { if std::ptr::eq(old, new) { - log::debug!("skipping fragment diff - fragment is the same {:?}", old); return; } @@ -481,24 +470,19 @@ impl<'b> DiffState<'b> { // // the change list stack is in the same state when this function returns. fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + use std::cmp::Ordering; + // Handled these cases in `diff_children` before calling this function. debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); - use std::cmp::Ordering; match old.len().cmp(&new.len()) { Ordering::Greater => self.remove_nodes(&old[new.len()..], true), Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), Ordering::Equal => {} } - // panic!( - // "diff_children: new_is_keyed: {:#?}, old_is_keyed: {:#?}. stack: {:#?}, oldptr: {:#?}, newptr: {:#?}", - // old, new, self.scope_stack, old as *const _, new as *const _ - // ); - for (new, old) in new.iter().zip(old.iter()) { - // log::debug!("diffing nonkeyed {:#?} {:#?}", old, new); self.diff_node(old, new); } } @@ -650,6 +634,7 @@ impl<'b> DiffState<'b> { // This function will load the appropriate nodes onto the stack and do diffing in place. // // Upon exit from this function, it will be restored to that same self. + #[allow(clippy::too_many_lines)] fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { /* 1. Map the old keys into a numerical ordering based on indices. @@ -675,8 +660,8 @@ impl<'b> DiffState<'b> { // 0. Debug sanity checks // Should have already diffed the shared-key prefixes and suffixes. - debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); - debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); + debug_assert_ne!(new.first().map(VNode::key), old.first().map(VNode::key)); + debug_assert_ne!(new.last().map(VNode::key), old.last().map(VNode::key)); // 1. Map the old keys into a numerical ordering based on indices. // 2. Create a map of old key to its index @@ -741,7 +726,7 @@ impl<'b> DiffState<'b> { lis_sequence.pop(); } - for idx in lis_sequence.iter() { + for idx in &lis_sequence { self.diff_node(&old[new_index_to_old_index[*idx]], &new[*idx]); } @@ -775,7 +760,6 @@ impl<'b> DiffState<'b> { if last - next > 1 { for (idx, new_node) in new[(next + 1)..last].iter().enumerate() { let new_idx = idx + next + 1; - let old_index = new_index_to_old_index[new_idx]; if old_index == u32::MAX as usize { nodes_created += self.create_node(new_node); @@ -816,7 +800,6 @@ impl<'b> DiffState<'b> { } fn replace_node(&mut self, old: &'b VNode<'b>, new: &'b VNode<'b>) { - log::debug!("Replacing node\n old: {:?}\n new: {:?}", old, new); let nodes_created = self.create_node(new); self.replace_inner(old, nodes_created); } @@ -848,7 +831,7 @@ impl<'b> DiffState<'b> { } VNode::Component(c) => { - log::info!("Replacing component {:?}", old); + log::trace!("Replacing component {:?}", old); let scope_id = c.scope.get().unwrap(); let node = self.scopes.fin_head(scope_id); @@ -856,11 +839,11 @@ impl<'b> DiffState<'b> { self.replace_inner(node, nodes_created); - log::info!("Replacing component x2 {:?}", old); + log::trace!("Replacing component x2 {:?}", old); // we can only remove components if they are actively being diffed if self.scope_stack.contains(&c.originator) { - log::debug!("Removing component {:?}", old); + log::trace!("Removing component {:?}", old); self.scopes.try_remove(scope_id).unwrap(); } @@ -913,7 +896,7 @@ impl<'b> DiffState<'b> { let root = self.scopes.root_node(scope_id); self.remove_nodes([root], gen_muts); - log::info!( + log::trace!( "trying to remove scope {:?}, stack is {:#?}, originator is {:?}", scope_id, self.scope_stack, @@ -922,7 +905,7 @@ impl<'b> DiffState<'b> { // we can only remove this node if the originator is actively if self.scope_stack.contains(&c.originator) { - log::debug!("I am allowed to remove component because scope stack contains originator. Scope stack: {:#?} {:#?} {:#?}", self.scope_stack, c.originator, c.scope); + log::trace!("I am allowed to remove component because scope stack contains originator. Scope stack: {:#?} {:#?} {:#?}", self.scope_stack, c.originator, c.scope); self.scopes.try_remove(scope_id).unwrap(); } self.leave_scope(); @@ -970,15 +953,12 @@ impl<'b> DiffState<'b> { fn find_last_element(&self, vnode: &'b VNode<'b>) -> Option { let mut search_node = Some(vnode); - loop { match &search_node.take().unwrap() { VNode::Text(t) => break t.id.get(), VNode::Element(t) => break t.id.get(), VNode::Placeholder(t) => break t.id.get(), - VNode::Fragment(frag) => { - search_node = frag.children.last(); - } + VNode::Fragment(frag) => search_node = frag.children.last(), VNode::Component(el) => { let scope_id = el.scope.get().unwrap(); search_node = Some(self.scopes.root_node(scope_id)); @@ -989,20 +969,16 @@ impl<'b> DiffState<'b> { fn find_first_element(&self, vnode: &'b VNode<'b>) -> Option { let mut search_node = Some(vnode); - loop { - match &search_node.take().unwrap() { - // the ones that have a direct id - VNode::Fragment(frag) => { - search_node = Some(&frag.children[0]); - } - VNode::Component(el) => { - let scope_id = el.scope.get().unwrap(); - search_node = Some(self.scopes.root_node(scope_id)); - } + match &search_node.take().expect("search node to have an ID") { VNode::Text(t) => break t.id.get(), VNode::Element(t) => break t.id.get(), VNode::Placeholder(t) => break t.id.get(), + VNode::Fragment(frag) => search_node = Some(&frag.children[0]), + VNode::Component(el) => { + let scope = el.scope.get().expect("element to have a scope assigned"); + search_node = Some(self.scopes.root_node(scope)); + } } } } @@ -1015,20 +991,26 @@ impl<'b> DiffState<'b> { 1 } - VNode::Fragment(_) | VNode::Component(_) => { - // + VNode::Fragment(frag) => { let mut added = 0; - for child in node.children() { + for child in frag.children { added += self.push_all_nodes(child); } added } + VNode::Component(c) => { + let scope_id = c.scope.get().unwrap(); + let root = self.scopes.root_node(scope_id); + self.push_all_nodes(root) + } + VNode::Element(el) => { let mut num_on_stack = 0; for child in el.children.iter() { num_on_stack += self.push_all_nodes(child); } + self.mutations.push_root(el.id.get().unwrap()); num_on_stack + 1 diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index b849819f0..7a36f58ce 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -147,13 +147,6 @@ impl<'src> VNode<'src> { } } - pub(crate) fn children(&self) -> &[VNode<'src>] { - match &self { - VNode::Fragment(f) => f.children, - _ => &[], - } - } - // Create an "owned" version of the vnode. pub fn decouple(&self) -> VNode<'src> { match *self { From fb75948363fdcc182292431e2c8706d883cc07c1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 14:33:25 -0500 Subject: [PATCH 082/256] fix: usestate modify panic --- packages/hooks/src/usestate.rs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index bce5b2b88..d77d7ceaa 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -39,16 +39,17 @@ pub fn use_state<'a, T: 'static>( let setter = Rc::new({ crate::to_owned![update_callback, slot]; move |new| { - let mut slot = slot.borrow_mut(); + { + let mut slot = slot.borrow_mut(); - // if there's only one reference (weak or otherwise), we can just swap the values - // Typically happens when the state is set multiple times - we don't want to create a new Rc for each new value - if let Some(val) = Rc::get_mut(&mut slot) { - *val = new; - } else { - *slot = Rc::new(new); + // if there's only one reference (weak or otherwise), we can just swap the values + // Typically happens when the state is set multiple times - we don't want to create a new Rc for each new value + if let Some(val) = Rc::get_mut(&mut slot) { + *val = new; + } else { + *slot = Rc::new(new); + } } - update_callback(); } }); @@ -92,7 +93,7 @@ impl UseState { /// } /// }) /// } - /// ``` + /// ``` #[must_use] pub fn current(&self) -> Rc { self.slot.borrow().clone() @@ -137,10 +138,10 @@ impl UseState { /// # use dioxus_hooks::*; /// fn component(cx: Scope) -> Element { /// let (value, set_value) = use_state(&cx, || 0); - /// + /// /// // to increment the value /// set_value.modify(|v| v + 1); - /// + /// /// // usage in async /// cx.spawn({ /// let set_value = set_value.to_owned(); @@ -153,8 +154,10 @@ impl UseState { /// } /// ``` pub fn modify(&self, f: impl FnOnce(&T) -> T) { - let current = self.slot.borrow(); - let new_val = f(current.as_ref()); + let new_val = { + let current = self.slot.borrow(); + f(current.as_ref()) + }; (self.setter)(new_val); } @@ -176,7 +179,7 @@ impl UseState { /// # use dioxus_hooks::*; /// fn component(cx: Scope) -> Element { /// let (value, set_value) = use_state(&cx, || 0); - /// + /// /// let as_rc = set_value.get(); /// assert_eq!(as_rc.as_ref(), &0); /// @@ -201,7 +204,7 @@ impl UseState { /// } /// }) /// } - /// ``` + /// ``` pub fn needs_update(&self) { (self.update_callback)(); } From 8badf90a03b1fba883bbca598a685e7d1ce367c2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 15:53:06 -0500 Subject: [PATCH 083/256] fix: dioxus web prevents default on in nested targets this commit fixes an issue where we used the event target to prevent default even if the target element wasn't an event handler. --- packages/web/src/dom.rs | 100 +++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 53 deletions(-) diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index ffbef9f4b..733e50e4b 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -28,17 +28,54 @@ impl WebsysDom { pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { // eventually, we just want to let the interpreter do all the work of decoding events into our event type let callback: Box = Box::new(move |event: &web_sys::Event| { - if let Ok(synthetic_event) = decode_trigger(event) { + let mut target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + + let typ = event.type_(); + + let decoded: anyhow::Result = loop { + match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { + Some(Ok(id)) => { + break Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: Some(ElementId(id)), + scope_id: None, + priority: dioxus_core::EventPriority::Medium, + }); + } + Some(Err(e)) => { + break Err(e.into()); + } + None => { + // walk the tree upwards until we actually find an event target + if let Some(parent) = target.parent_element() { + target = parent; + } else { + break Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: None, + scope_id: None, + priority: dioxus_core::EventPriority::Low, + }); + } + } + } + }; + + if let Ok(synthetic_event) = decoded { // Try to prevent default if the attribute is set - if let Some(target) = event.target() { - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); } } } @@ -274,49 +311,6 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc anyhow::Result { - let mut target = event - .target() - .expect("missing target") - .dyn_into::() - .expect("not a valid element"); - - let typ = event.type_(); - - loop { - match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { - Some(Ok(id)) => { - return Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: Some(ElementId(id)), - scope_id: None, - priority: dioxus_core::EventPriority::Medium, - }); - } - Some(Err(e)) => { - return Err(e.into()); - } - None => { - // walk the tree upwards until we actually find an event target - if let Some(parent) = target.parent_element() { - target = parent; - } else { - return Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: None, - scope_id: None, - priority: dioxus_core::EventPriority::Low, - }); - } - } - } - } -} - pub(crate) fn load_document() -> Document { web_sys::window() .expect("should have access to the Window") From f1fe8f1d2a02b53919ef0d107fca0f85619b89fc Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Tue, 1 Feb 2022 08:39:31 +0800 Subject: [PATCH 084/256] fix: change hooks name --- examples/borderless.rs | 8 ++++---- packages/desktop/src/desktop_context.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/borderless.rs b/examples/borderless.rs index 15756503e..2a1c007f2 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -7,13 +7,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let desktop = dioxus::desktop::desktop_context::use_desktop_context(&cx); + let window = dioxus::desktop::desktop_context::use_window(&cx); cx.render(rsx!( link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } header { class: "text-gray-400 bg-gray-900 body-font", - onmousedown: move |_| desktop.drag_window(), + onmousedown: move |_| window.drag(), div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", @@ -23,13 +23,13 @@ fn app(cx: Scope) -> Element { button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| desktop.minimize(true), + onclick: move |_| window.minimize(true), "Minimize" } button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| desktop.close(), + onclick: move |_| window.close(), "Close" } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index a41712904..9c0404477 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -34,7 +34,7 @@ impl DesktopContext { /// ```rust /// onmousedown: move |_| { desktop.drag_window(); } /// ``` - pub fn drag_window(&self) { + pub fn drag(&self) { let _ = self.proxy.send_event(UserWindowEvent::DragWindow); } @@ -60,7 +60,7 @@ impl DesktopContext { } /// use this function can get the `DesktopContext` context. -pub fn use_desktop_context(cx: &ScopeState) -> &Rc { +pub fn use_window(cx: &ScopeState) -> &Rc { cx.use_hook(|_| cx.consume_context::()) .as_ref() .unwrap() From 6905bf98d75f90e5fb7cab863f150dd9aee189b1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 Jan 2022 19:40:48 -0500 Subject: [PATCH 085/256] chore: clean up examples and fix link opening code --- examples/disabled.rs | 5 ---- examples/dog_app.rs | 4 +-- examples/file_explorer.rs | 1 - examples/filedragdrop.rs | 2 +- examples/framework_benchmark.rs | 4 +-- packages/desktop/src/interpreter.js | 42 +++++++++++++++-------------- packages/desktop/src/lib.rs | 1 + 7 files changed, 28 insertions(+), 31 deletions(-) diff --git a/examples/disabled.rs b/examples/disabled.rs index f72a8ff77..949c25369 100644 --- a/examples/disabled.rs +++ b/examples/disabled.rs @@ -18,11 +18,6 @@ fn app(cx: Scope) -> Element { disabled: "{disabled}", "lower button" } - - input { - value: "false", - } - } }) } diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 331574a75..a30f3e5e8 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -16,7 +16,7 @@ struct ListBreeds { } fn app(cx: Scope) -> Element { - let fut = use_future(&cx, || async move { + let breeds = use_future(&cx, || async move { reqwest::get("https://dog.ceo/api/breeds/list/all") .await .unwrap() @@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element { let (breed, set_breed) = use_state(&cx, || None); - match fut.value() { + match breeds.value() { Some(Ok(breeds)) => cx.render(rsx! { div { h1 {"Select a dog breed!"} diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index fe5ab23bb..728812027 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -56,7 +56,6 @@ fn app(cx: Scope) -> Element { ) }) } - }) } diff --git a/examples/filedragdrop.rs b/examples/filedragdrop.rs index 523405b39..e5644e2a0 100644 --- a/examples/filedragdrop.rs +++ b/examples/filedragdrop.rs @@ -4,7 +4,7 @@ fn main() { dioxus::desktop::launch_with_props(app, (), |c| { c.with_file_drop_handler(|_w, e| { println!("{:?}", e); - false + true }) }); } diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index 7cea4f5f7..8dba1cb91 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -17,9 +17,9 @@ impl Label { fn new_list(num: usize) -> Vec { let mut rng = SmallRng::from_entropy(); let mut labels = Vec::with_capacity(num); - for _ in 0..num { + for x in 0..num { labels.push(Label { - key: 0, + key: x, labels: [ ADJECTIVES.choose(&mut rng).unwrap(), COLOURS.choose(&mut rng).unwrap(), diff --git a/packages/desktop/src/interpreter.js b/packages/desktop/src/interpreter.js index 61ba649c7..503ea7247 100644 --- a/packages/desktop/src/interpreter.js +++ b/packages/desktop/src/interpreter.js @@ -200,14 +200,35 @@ export class Interpreter { let target = event.target; if (target != null) { let realId = target.getAttribute(`data-dioxus-id`); + let shouldPreventDefault = target.getAttribute( + `dioxus-prevent-default` + ); + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + console.log("click", event); + console.log("clickeded", event.target); + console.log("clickeded", event.target.tagName); + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.rpc.call("browser_open", { href }); + } + } + } + } + // walk the tree to find the real element while (realId == null && target.parentElement != null) { target = target.parentElement; realId = target.getAttribute(`data-dioxus-id`); } - const shouldPreventDefault = target.getAttribute( + + shouldPreventDefault = target.getAttribute( `dioxus-prevent-default` ); + let contents = serialize_event(event); if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); @@ -215,25 +236,6 @@ export class Interpreter { if (event.type == "submit") { event.preventDefault(); } - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href"); - if ( - href !== "" && - href !== null && - href !== undefined && - realId != null - ) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href, - }); - } - } - } - } if (realId == null) { return; } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index f3e370230..86928ef24 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -188,6 +188,7 @@ pub fn launch_with_props( let _ = proxy.send_event(UserWindowEvent::Update); } "browser_open" => { + println!("browser_open"); let data = req.params.unwrap(); log::trace!("Open browser: {:?}", data); if let Some(arr) = data.as_array() { From bead03596942089862b63d83e6d7514ddada946a Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Tue, 1 Feb 2022 08:43:54 +0800 Subject: [PATCH 086/256] feat: export `use_window` --- examples/borderless.rs | 2 +- packages/desktop/src/lib.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/borderless.rs b/examples/borderless.rs index 2a1c007f2..d52e9fb5a 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -7,7 +7,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let window = dioxus::desktop::desktop_context::use_window(&cx); + let window = dioxus::desktop::use_window(&cx); cx.render(rsx!( link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index eb76ce63d..524e42da4 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -56,6 +56,7 @@ pub mod escape; pub mod events; use cfg::DesktopConfig; +pub use desktop_context::use_window; use desktop_context::DesktopContext; use dioxus_core::*; use std::{ From fa020c53e48b7770afbff8f24bc25a70d19ed58f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 1 Feb 2022 01:57:21 -0500 Subject: [PATCH 087/256] tweak: use 2018 rust syntax instead of 2022 for formatting --- packages/desktop/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index eb4657c52..22acc4f85 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -248,7 +248,7 @@ pub fn launch_with_props( // do not let path searching to go two layers beyond the caller level let data = read(path_buf)?; - let meta = format!("{mime}"); + let meta = format!("{}", mime); wry::http::ResponseBuilder::new().mimetype(&meta).body(data) } From e02dfc3324fdb808f6390d2d6942fb727220a329 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 1 Feb 2022 02:05:54 -0500 Subject: [PATCH 088/256] fix: change use_state to be clone --- packages/hooks/src/usestate.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index d77d7ceaa..7e8892be9 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -275,10 +275,8 @@ impl UseState { } } -impl ToOwned for UseState { - type Owned = UseState; - - fn to_owned(&self) -> Self::Owned { +impl Clone for UseState { + fn clone(&self) -> Self { UseState { current_val: self.current_val.clone(), update_callback: self.update_callback.clone(), From d7968c987ff07134f480e9ab3869d518c1ecd554 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Tue, 1 Feb 2022 20:40:29 +0100 Subject: [PATCH 089/256] wip: Makefile setup --- Makefile.toml | 42 +++++++++++++++++++++ packages/router/Cargo.toml | 56 ++++++++++++++-------------- packages/router/Makefile.toml | 10 +++++ packages/router/tests/route.rs | 68 ++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 28 deletions(-) create mode 100644 Makefile.toml create mode 100644 packages/router/Makefile.toml create mode 100644 packages/router/tests/route.rs diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 000000000..23eeacea2 --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,42 @@ +[config] +default_to_workspace = false +min_version = "0.32.4" + +[env] +CARGO_MAKE_CLIPPY_ARGS = "-- --deny=warnings" +CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true + +[config.modify_core_tasks] +namespace = "core" +private = true + +[tasks.tests] +category = "Testing" +dependencies = ["tests-setup"] +description = "Run all tests" +env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*", "**/packages/changelog"]} +run_task = {name = ["test-flow"], fork = true} + +[tasks.tests-setup] +private = true +script = [ + """ + test_flags = array --headless --firefox + dioxus_test_features = set wasm_test + dioxus_test_flags = array_join ${test_flags} " " + echo "running tests with flags: ${dioxus_test_flags} and features: ${dioxus_test_features}" + set_env DIOXUS_TEST_FLAGS ${dioxus_test_flags} + set_env DIOXUS_TEST_FEATURES ${dioxus_test_features} + """, +] +script_runner = "@duckscript" + +[tasks.test-flow] +dependencies = ["test"] +private = true +workspace = true + +[tasks.test] +args = ["test", "--all-targets"] +command = "cargo" +private = true diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 1c6ed24d2..3bcb6a348 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -1,51 +1,51 @@ [package] -name = "dioxus-router" -version = "0.1.1" -edition = "2018" description = "Dioxus VirtualDOM renderer for the web browser using websys" -license = "MIT/Apache-2.0" -repository = "https://github.com/DioxusLabs/dioxus/" -homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" +edition = "2018" +homepage = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] +license = "MIT/Apache-2.0" +name = "dioxus-router" +repository = "https://github.com/DioxusLabs/dioxus/" +version = "0.1.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version = "^0.1.9", default-features = false } -dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } -dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } +dioxus-core = {path = "../core", version = "^0.1.9", default-features = false} +dioxus-core-macro = {path = "../core-macro", version = "^0.1.7"} +dioxus-html = {path = "../html", version = "^0.1.6", default-features = false} serde = "1" -url = "2.2.2" serde_urlencoded = "0.7" +url = "2.2.2" # for wasm -web-sys = { version = "0.3", features = [ - "Attr", - "Document", - "History", - "HtmlBaseElement", - "Event", - "NamedNodeMap", - "Url", - "UrlSearchParams", - "Window", -], optional = true } -wasm-bindgen = { version = "0.2", optional = true } -js-sys = { version = "0.3", optional = true } -gloo = { version = "0.5", optional = true } +gloo = {version = "0.5", optional = true} +js-sys = {version = "0.3", optional = true} log = "0.4.14" - +wasm-bindgen = {version = "0.2", optional = true} +web-sys = {version = "0.3", features = [ + "Attr", + "Document", + "History", + "HtmlBaseElement", + "Event", + "NamedNodeMap", + "Url", + "UrlSearchParams", + "Window", +], optional = true} [features] default = ["derive", "web"] -web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"] +derive = [] desktop = [] mobile = [] -derive = [] +web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"] [dev-dependencies] console_error_panic_hook = "0.1.7" -dioxus-web = { path = "../web" } +dioxus-web = {path = "../web"} log = "0.4.14" +wasm-bindgen-test = "0.3" wasm-logger = "0.2.0" diff --git a/packages/router/Makefile.toml b/packages/router/Makefile.toml new file mode 100644 index 000000000..27a6c9244 --- /dev/null +++ b/packages/router/Makefile.toml @@ -0,0 +1,10 @@ +[tasks.test] +args = [ + "test", + "@@split(DIOXUS_TEST_FLAGS, )", + "--", + "--features", + "${DIOXUS_TEST_FEATURES}", +] +command = "wasm-pack" +extend = "core::wasm-pack-base" diff --git a/packages/router/tests/route.rs b/packages/router/tests/route.rs new file mode 100644 index 000000000..08a5b0aff --- /dev/null +++ b/packages/router/tests/route.rs @@ -0,0 +1,68 @@ +#![cfg(target_arch = "wasm32")] + +use dioxus_core::prelude::*; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; +use dioxus_router::*; +use gloo_utils::document; +use serde::{Deserialize, Serialize}; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn simple_test() { + fn main() { + console_error_panic_hook::set_once(); + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + dioxus_web::launch(APP); + } + + static APP: Component = |cx| { + cx.render(rsx! { + Router { + onchange: move |route| log::info!("route changed to {}", route), + Route { to: "/", Home {} } + Route { to: "blog" + Route { to: "/", BlogList {} } + Route { to: ":id", BlogPost {} } + } + } + }) + }; + + fn Home(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Home" } + } + }) + } + + fn BlogList(cx: Scope) -> Element { + cx.render(rsx! { + div { + + } + }) + } + + fn BlogPost(cx: Scope) -> Element { + let id = use_route(&cx).segment::("id")?; + + cx.render(rsx! { + div { + id: "test1", + id + } + }) + } + + main(); + + let element = gloo_utils::document() + .get_element_by_id("test1") + .expect("No result found. Most likely, the application crashed") + .inner_html(); + assert!(element, ""); +} From 94c1da82644c6f4dfa824c75ba8c0b30c4bf3f9f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 1 Feb 2022 15:00:36 -0500 Subject: [PATCH 090/256] chore: clean up documentation in diffing algorithm --- packages/core/src/diff.rs | 357 ++++++++++++++++++++++++-------------- 1 file changed, 222 insertions(+), 135 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index d40f49934..3f3629c10 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,6 +1,96 @@ #![warn(clippy::pedantic)] #![allow(clippy::cast_possible_truncation)] +//! This module contains the stateful [`DiffState`] and all methods to diff [`VNodes`], their properties, and their children. +//! +//! The [`DiffState`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set +//! of mutations for the [`RealDom`] to apply. +//! +//! ## Notice: +//! +//! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support +//! Components, Fragments, Suspense, [`SubTree`] memoization, incremental diffing, cancellation, [`NodeRefs`], pausing, priority +//! scheduling, and additional batching operations. +//! +//! ## Implementation Details: +//! +//! ### IDs for elements +//! -------------------- +//! All nodes are addressed by their IDs. The [`RealDom`] provides an imperative interface for making changes to these nodes. +//! We don't necessarily require that DOM changes happen instantly during the diffing process, so the implementor may choose +//! to batch nodes if it is more performant for their application. The element IDs are indices into the internal element +//! array. The expectation is that implementors will use the ID as an index into a Vec of real nodes, allowing for passive +//! garbage collection as the [`VirtualDOM`] replaces old nodes. +//! +//! When new vnodes are created through `cx.render`, they won't know which real node they correspond to. During diffing, +//! we always make sure to copy over the ID. If we don't do this properly, the [`ElementId`] will be populated incorrectly +//! and brick the user's page. +//! +//! ### Fragment Support +//! -------------------- +//! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments +//! can be particularly challenging when they are empty, so the anchor node lets us "reserve" a spot for the empty +//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the [`NodeFactory`] - it is +//! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. Adding +//! "dummy" nodes _is_ inefficient, but it makes our diffing algorithm faster and the implementation is completely up to +//! the platform. +//! +//! Other implementations either don't support fragments or use a "child + sibling" pattern to represent them. Our code is +//! vastly simpler and more performant when we can just create a placeholder element while the fragment has no children. +//! +//! ### Suspense +//! ------------ +//! Dioxus implements Suspense slightly differently than React. In React, each fiber is manually progressed until it runs +//! into a promise-like value. React will then work on the next "ready" fiber, checking back on the previous fiber once +//! it has finished its new work. In Dioxus, we use a similar approach, but try to completely render the tree before +//! switching sub-fibers. Instead, each future is submitted into a futures-queue and the node is manually loaded later on. +//! Due to the frequent calls to [`yield_now`] we can get the pure "fetch-as-you-render" behavior of React Fiber. +//! +//! We're able to use this approach because we use placeholder nodes - futures that aren't ready still get submitted to +//! DOM, but as a placeholder. +//! +//! Right now, the "suspense" queue is intertwined with hooks. In the future, we should allow any future to drive attributes +//! and contents, without the need for the [`use_suspense`] hook. In the interim, this is the quickest way to get Suspense working. +//! +//! ## Subtree Memoization +//! ----------------------- +//! We also employ "subtree memoization" which saves us from having to check trees which hold no dynamic content. We can +//! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the +//! calls to "create" propagate this information upwards. Structures like the one below are entirely static: +//! ```rust, ignore +//! rsx!( div { class: "hello world", "this node is entirely static" } ) +//! ``` +//! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so it's up to the reconciler to +//! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP and depends on comp-time +//! hashing of the subtree from the rsx! macro. We do a very limited form of static analysis via static string pointers as +//! a way of short-circuiting the most expensive checks. +//! +//! ## Bloom Filter and Heuristics +//! ------------------------------ +//! For all components, we employ some basic heuristics to speed up allocations and pre-size bump arenas. The heuristics are +//! currently very rough, but will get better as time goes on. The information currently tracked includes the size of a +//! bump arena after first render, the number of hooks, and the number of nodes in the tree. +//! +//! ## Garbage Collection +//! --------------------- +//! Dioxus uses a passive garbage collection system to clean up old nodes once the work has been completed. This garbage +//! collection is done internally once the main diffing work is complete. After the "garbage" is collected, Dioxus will then +//! start to re-use old keys for new nodes. This results in a passive memory management system that is very efficient. +//! +//! The IDs used by the key/map are just an index into a Vec. This means that Dioxus will drive the key allocation strategy +//! so the client only needs to maintain a simple list of nodes. By default, Dioxus will not manually clean up old nodes +//! for the client. As new nodes are created, old nodes will be over-written. +//! +//! ## Further Reading and Thoughts +//! ---------------------------- +//! There are more ways of increasing diff performance here that are currently not implemented. +//! - Strong memoization of subtrees. +//! - Guided diffing. +//! - Certain web-dom-specific optimizations. +//! +//! More info on how to improve this diffing algorithm: +//! - + use crate::innerlude::{ AnyProps, ElementId, Mutations, ScopeArena, ScopeId, VComponent, VElement, VFragment, VNode, VPlaceholder, VText, @@ -45,35 +135,38 @@ impl<'b> DiffState<'b> { // these are *actual* elements, not wrappers around lists (Text(old), Text(new)) => { if std::ptr::eq(old, new) { - log::trace!("skipping node diff - text are the sames"); return; } - if let Some(root) = old.id.get() { - if old.text != new.text { - self.mutations.set_text(new.text, root.as_u64()); - } - self.scopes.update_node(new_node, root); + let root = old + .id + .get() + .expect("existing text nodes should have an ElementId"); - new.id.set(Some(root)); + if old.text != new.text { + self.mutations.set_text(new.text, root.as_u64()); } + self.scopes.update_node(new_node, root); + + new.id.set(Some(root)); } (Placeholder(old), Placeholder(new)) => { if std::ptr::eq(old, new) { - log::trace!("skipping node diff - placeholder are the sames"); return; } - if let Some(root) = old.id.get() { - self.scopes.update_node(new_node, root); - new.id.set(Some(root)); - } + let root = old + .id + .get() + .expect("existing placeholder nodes should have an ElementId"); + + self.scopes.update_node(new_node, root); + new.id.set(Some(root)); } (Element(old), Element(new)) => { if std::ptr::eq(old, new) { - log::trace!("skipping node diff - element are the sames"); return; } self.diff_element_nodes(old, new, old_node, new_node); @@ -82,7 +175,6 @@ impl<'b> DiffState<'b> { // These two sets are pointers to nodes but are not actually nodes themselves (Component(old), Component(new)) => { if std::ptr::eq(old, new) { - log::trace!("skipping node diff - placeholder are the sames"); return; } self.diff_component_nodes(old_node, new_node, *old, *new); @@ -90,7 +182,6 @@ impl<'b> DiffState<'b> { (Fragment(old), Fragment(new)) => { if std::ptr::eq(old, new) { - log::trace!("skipping node diff - fragment are the sames"); return; } self.diff_fragment_nodes(old, new); @@ -114,19 +205,17 @@ impl<'b> DiffState<'b> { } } - fn create_text_node(&mut self, vtext: &'b VText<'b>, node: &'b VNode<'b>) -> usize { + fn create_text_node(&mut self, text: &'b VText<'b>, node: &'b VNode<'b>) -> usize { let real_id = self.scopes.reserve_node(node); - self.mutations.create_text_node(vtext.text, real_id); - vtext.id.set(Some(real_id)); - + text.id.set(Some(real_id)); + self.mutations.create_text_node(text.text, real_id); 1 } fn create_anchor_node(&mut self, anchor: &'b VPlaceholder, node: &'b VNode<'b>) -> usize { let real_id = self.scopes.reserve_node(node); - self.mutations.create_placeholder(real_id); anchor.id.set(Some(real_id)); - + self.mutations.create_placeholder(real_id); 1 } @@ -140,36 +229,35 @@ impl<'b> DiffState<'b> { id: dom_id, parent: parent_id, .. - } = element; + } = &element; - let parent = self.element_stack.last().unwrap(); - parent_id.set(Some(*parent)); + parent_id.set(self.element_stack.last().copied()); - // set the id of the element let real_id = self.scopes.reserve_node(node); + dom_id.set(Some(real_id)); self.element_stack.push(real_id); + { + self.mutations.create_element(tag_name, *namespace, real_id); - self.mutations.create_element(tag_name, *namespace, real_id); + let cur_scope_id = self + .current_scope() + .expect("diffing should always have a scope"); - if let Some(cur_scope_id) = self.current_scope() { - for listener in *listeners { + for listener in listeners.iter() { listener.mounted_node.set(Some(real_id)); self.mutations.new_event_listener(listener, cur_scope_id); } - } else { - log::trace!("create element called with no scope on the stack - this is an error for a live dom"); - } - for attr in *attributes { - self.mutations.set_attribute(attr, real_id.as_u64()); - } + for attr in attributes.iter() { + self.mutations.set_attribute(attr, real_id.as_u64()); + } - if !children.is_empty() { - self.create_and_append_children(children); + if !children.is_empty() { + self.create_and_append_children(children); + } } - self.element_stack.pop(); 1 @@ -182,25 +270,29 @@ impl<'b> DiffState<'b> { fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize { let parent_idx = self.current_scope().unwrap(); - // the component might already exist - if it does, we need to reuse it - // this makes figure out when to drop the component more complicated - let new_idx = if let Some(idx) = vcomponent.scope.get() { - assert!(self.scopes.get_scope(idx).is_some()); - idx - } else { - // Insert a new scope into our component list - let props: Box = vcomponent.props.borrow_mut().take().unwrap(); - let props: Box = unsafe { std::mem::transmute(props) }; - let new_idx = self.scopes.new_with_key( - vcomponent.user_fc, - props, - Some(parent_idx), - self.element_stack.last().copied().unwrap(), - 0, - ); + // ensure this scope doesn't already exist if we're trying to create it + debug_assert!( + vcomponent + .scope + .get() + .and_then(|f| self.scopes.get_scope(f)) + .is_none(), + "component scope already exists" + ); - new_idx - }; + // Insert a new scope into our component list + let props: Box = vcomponent.props.borrow_mut().take().unwrap(); + let props: Box = unsafe { std::mem::transmute(props) }; + let new_idx = self.scopes.new_with_key( + vcomponent.user_fc, + props, + Some(parent_idx), + self.element_stack.last().copied().unwrap(), + 0, + ); + + // Actually initialize the caller's slot with the right address + vcomponent.scope.set(Some(new_idx)); log::trace!( "created component \"{}\", id: {:?} parent {:?} orig: {:?}", @@ -210,9 +302,6 @@ impl<'b> DiffState<'b> { vcomponent.originator ); - // Actually initialize the caller's slot with the right address - vcomponent.scope.set(Some(new_idx)); - // if vcomponent.can_memoize { // // todo: implement promotion logic. save us from boxing props that we don't need // } else { @@ -221,13 +310,15 @@ impl<'b> DiffState<'b> { self.enter_scope(new_idx); - // Run the scope for one iteration to initialize it - self.scopes.run_scope(new_idx); - self.mutations.mark_dirty_scope(new_idx); + let created = { + // Run the scope for one iteration to initialize it + self.scopes.run_scope(new_idx); + self.mutations.mark_dirty_scope(new_idx); - // Take the node that was just generated from running the component - let nextnode = self.scopes.fin_head(new_idx); - let created = self.create_node(nextnode); + // Take the node that was just generated from running the component + let nextnode = self.scopes.fin_head(new_idx); + self.create_node(nextnode) + }; self.leave_scope(); @@ -241,7 +332,10 @@ impl<'b> DiffState<'b> { old_node: &'b VNode<'b>, new_node: &'b VNode<'b>, ) { - let root = old.id.get().unwrap(); + let root = old + .id + .get() + .expect("existing element nodes should have an ElementId"); // If the element type is completely different, the element needs to be re-rendered completely // This is an optimization React makes due to how users structure their code @@ -330,7 +424,10 @@ impl<'b> DiffState<'b> { old: &'b VComponent<'b>, new: &'b VComponent<'b>, ) { - let scope_addr = old.scope.get().unwrap(); + let scope_addr = old + .scope + .get() + .expect("existing component nodes should have a scope"); if std::ptr::eq(old, new) { return; @@ -339,55 +436,55 @@ impl<'b> DiffState<'b> { // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { self.enter_scope(scope_addr); + { + // Make sure the new component vnode is referencing the right scope id + new.scope.set(Some(scope_addr)); - // Make sure the new component vnode is referencing the right scope id - new.scope.set(Some(scope_addr)); + // make sure the component's caller function is up to date + let scope = self + .scopes + .get_scope(scope_addr) + .unwrap_or_else(|| panic!("could not find {:?}", scope_addr)); - // make sure the component's caller function is up to date - let scope = self - .scopes - .get_scope(scope_addr) - .unwrap_or_else(|| panic!("could not find {:?}", scope_addr)); - - // take the new props out regardless - // when memoizing, push to the existing scope if memoization happens - let new_props = new.props.borrow_mut().take().unwrap(); - - let should_run = { - if old.can_memoize { - let props_are_the_same = unsafe { - scope - .props - .borrow() - .as_ref() - .unwrap() - .memoize(new_props.as_ref()) - }; - !props_are_the_same || self.force_diff - } else { - true - } - }; - - if should_run { - let _old_props = scope + // take the new props out regardless + // when memoizing, push to the existing scope if memoization happens + let new_props = new .props - .replace(unsafe { std::mem::transmute(Some(new_props)) }); + .borrow_mut() + .take() + .expect("new component props should exist"); - // this should auto drop the previous props - self.scopes.run_scope(scope_addr); - self.mutations.mark_dirty_scope(scope_addr); + let should_diff = { + if old.can_memoize { + // safety: we trust the implementation of "memoize" + let props_are_the_same = unsafe { + let new_ref = new_props.as_ref(); + scope.props.borrow().as_ref().unwrap().memoize(new_ref) + }; + !props_are_the_same || self.force_diff + } else { + true + } + }; - self.diff_node( - self.scopes.wip_head(scope_addr), - self.scopes.fin_head(scope_addr), - ); - } else { - log::trace!("memoized"); - // memoization has taken place - drop(new_props); - }; + if should_diff { + let _old_props = scope + .props + .replace(unsafe { std::mem::transmute(Some(new_props)) }); + // this should auto drop the previous props + self.scopes.run_scope(scope_addr); + self.mutations.mark_dirty_scope(scope_addr); + + self.diff_node( + self.scopes.wip_head(scope_addr), + self.scopes.fin_head(scope_addr), + ); + } else { + // memoization has taken place + drop(new_props); + }; + } self.leave_scope(); } else { self.replace_node(old_node, new_node); @@ -410,10 +507,6 @@ impl<'b> DiffState<'b> { self.diff_children(old.children, new.children); } - // ============================================= - // Utilities for creating new diff instructions - // ============================================= - // Diff the given set of old and new children. // // The parent must be on top of the change list stack when this function is @@ -836,16 +929,17 @@ impl<'b> DiffState<'b> { let node = self.scopes.fin_head(scope_id); self.enter_scope(scope_id); + { + self.replace_inner(node, nodes_created); - self.replace_inner(node, nodes_created); + log::trace!("Replacing component x2 {:?}", old); - log::trace!("Replacing component x2 {:?}", old); + // we can only remove components if they are actively being diffed + if self.scope_stack.contains(&c.originator) { + log::trace!("Removing component {:?}", old); - // we can only remove components if they are actively being diffed - if self.scope_stack.contains(&c.originator) { - log::trace!("Removing component {:?}", old); - - self.scopes.try_remove(scope_id).unwrap(); + self.scopes.try_remove(scope_id).unwrap(); + } } self.leave_scope(); } @@ -891,22 +985,15 @@ impl<'b> DiffState<'b> { VNode::Component(c) => { self.enter_scope(c.scope.get().unwrap()); + { + let scope_id = c.scope.get().unwrap(); + let root = self.scopes.root_node(scope_id); + self.remove_nodes([root], gen_muts); - let scope_id = c.scope.get().unwrap(); - let root = self.scopes.root_node(scope_id); - self.remove_nodes([root], gen_muts); - - log::trace!( - "trying to remove scope {:?}, stack is {:#?}, originator is {:?}", - scope_id, - self.scope_stack, - c.originator - ); - - // we can only remove this node if the originator is actively - if self.scope_stack.contains(&c.originator) { - log::trace!("I am allowed to remove component because scope stack contains originator. Scope stack: {:#?} {:#?} {:#?}", self.scope_stack, c.originator, c.scope); - self.scopes.try_remove(scope_id).unwrap(); + // we can only remove this node if the originator is actively in our stackß + if self.scope_stack.contains(&c.originator) { + self.scopes.try_remove(scope_id).unwrap(); + } } self.leave_scope(); } From 8ad3f85872f77f3fc2a586dd18f980937a34a25d Mon Sep 17 00:00:00 2001 From: t1m0t Date: Tue, 1 Feb 2022 21:13:18 +0100 Subject: [PATCH 091/256] fix ordering to match original one --- .vscode/settings.json | 1 + packages/router/Cargo.toml | 59 +++++++++++++++++++------------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ac2195ccd..221164c7c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,4 +4,5 @@ "desktop", "router" ], + "editor.formatOnSave": false } diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 3bcb6a348..af701d2f2 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -1,51 +1,52 @@ [package] -description = "Dioxus VirtualDOM renderer for the web browser using websys" -documentation = "https://dioxuslabs.com" -edition = "2018" -homepage = "https://dioxuslabs.com" -keywords = ["dom", "ui", "gui", "react", "wasm"] -license = "MIT/Apache-2.0" name = "dioxus-router" -repository = "https://github.com/DioxusLabs/dioxus/" version = "0.1.1" +edition = "2018" +description = "Dioxus VirtualDOM renderer for the web browser using websys" +license = "MIT/Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = {path = "../core", version = "^0.1.9", default-features = false} -dioxus-core-macro = {path = "../core-macro", version = "^0.1.7"} -dioxus-html = {path = "../html", version = "^0.1.6", default-features = false} +dioxus-core = { path = "../core", version = "^0.1.9", default-features = false } +dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } +dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } serde = "1" -serde_urlencoded = "0.7" url = "2.2.2" +serde_urlencoded = "0.7" # for wasm -gloo = {version = "0.5", optional = true} -js-sys = {version = "0.3", optional = true} +web-sys = { version = "0.3", features = [ + "Attr", + "Document", + "History", + "HtmlBaseElement", + "Event", + "NamedNodeMap", + "Url", + "UrlSearchParams", + "Window", +], optional = true } +wasm-bindgen = { version = "0.2", optional = true } +js-sys = { version = "0.3", optional = true } +gloo = { version = "0.5", optional = true } log = "0.4.14" -wasm-bindgen = {version = "0.2", optional = true} -web-sys = {version = "0.3", features = [ - "Attr", - "Document", - "History", - "HtmlBaseElement", - "Event", - "NamedNodeMap", - "Url", - "UrlSearchParams", - "Window", -], optional = true} + [features] default = ["derive", "web"] -derive = [] +web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"] desktop = [] mobile = [] -web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"] +derive = [] [dev-dependencies] console_error_panic_hook = "0.1.7" -dioxus-web = {path = "../web"} +dioxus-web = { path = "../web" } log = "0.4.14" -wasm-bindgen-test = "0.3" wasm-logger = "0.2.0" +wasm-bindgen-test = "0.3" From 81ca1a001402a415458e79f2e2a8e6891d85c843 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Tue, 1 Feb 2022 21:34:57 +0100 Subject: [PATCH 092/256] updated workflow to include make tests --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b941960b7..ce7ffecbd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,8 +35,8 @@ jobs: - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev - uses: actions-rs/cargo@v1 with: - command: test - args: --features "desktop, ssr, router" + command: make + args: tests fmt: name: Rustfmt From ee67a041d2991fea2e867f14a4c8b4949421de6f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 1 Feb 2022 15:38:48 -0500 Subject: [PATCH 093/256] feat: turn interpreter into crate --- Cargo.toml | 2 ++ packages/interpreter/Cargo.toml | 8 ++++++++ packages/{jsinterpreter => interpreter}/README.md | 0 packages/interpreter/build.rs | 6 ++++++ packages/{jsinterpreter => interpreter}/interpreter.js | 2 +- packages/{jsinterpreter => interpreter}/interpreter.ts | 6 +----- packages/interpreter/src/lib.rs | 1 + packages/{jsinterpreter => interpreter}/tsconfig.json | 0 8 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 packages/interpreter/Cargo.toml rename packages/{jsinterpreter => interpreter}/README.md (100%) create mode 100644 packages/interpreter/build.rs rename packages/{jsinterpreter => interpreter}/interpreter.js (99%) rename packages/{jsinterpreter => interpreter}/interpreter.ts (99%) create mode 100644 packages/interpreter/src/lib.rs rename packages/{jsinterpreter => interpreter}/tsconfig.json (100%) diff --git a/Cargo.toml b/Cargo.toml index 468518f5f..5e3a0a032 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ dioxus-ssr = { path = "./packages/ssr", version = "^0.1.3", optional = true } dioxus-router = { path = "./packages/router", version = "^0.1.1", optional = true } dioxus-mobile = { path = "./packages/mobile", version = "^0.0.3", optional = true } +dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.0.0", optional = true } # dioxus-liveview = { path = "./packages/liveview", optional = true } [features] @@ -52,6 +53,7 @@ members = [ "packages/ssr", "packages/desktop", "packages/mobile", + "packages/interpreter", ] [dev-dependencies] diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml new file mode 100644 index 000000000..0934c08df --- /dev/null +++ b/packages/interpreter/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "dioxus-interpreter-js" +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/packages/jsinterpreter/README.md b/packages/interpreter/README.md similarity index 100% rename from packages/jsinterpreter/README.md rename to packages/interpreter/README.md diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs new file mode 100644 index 000000000..1ef8b6548 --- /dev/null +++ b/packages/interpreter/build.rs @@ -0,0 +1,6 @@ +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-changed=interpreter.ts"); + Command::new("tsc").spawn().unwrap(); +} diff --git a/packages/jsinterpreter/interpreter.js b/packages/interpreter/interpreter.js similarity index 99% rename from packages/jsinterpreter/interpreter.js rename to packages/interpreter/interpreter.js index ea50ec3d3..a70177252 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/interpreter/interpreter.js @@ -198,7 +198,7 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": - // this handler is only provided on desktop implementations since this + // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event) => { let target = event.target; diff --git a/packages/jsinterpreter/interpreter.ts b/packages/interpreter/interpreter.ts similarity index 99% rename from packages/jsinterpreter/interpreter.ts rename to packages/interpreter/interpreter.ts index fdd8c671d..44d53cda8 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/interpreter/interpreter.ts @@ -1,7 +1,3 @@ - - - - export function main() { let root = window.document.getElementById("main"); if (root != null) { @@ -245,7 +241,7 @@ export class Interpreter { case "NewEventListener": - // this handler is only provided on desktop implementations since this + // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event: Event) => { let target = event.target as Element | null; diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs new file mode 100644 index 000000000..9c0229aa5 --- /dev/null +++ b/packages/interpreter/src/lib.rs @@ -0,0 +1 @@ +pub static INTERPRTER_JS: &str = include_str!("../interpreter.js"); diff --git a/packages/jsinterpreter/tsconfig.json b/packages/interpreter/tsconfig.json similarity index 100% rename from packages/jsinterpreter/tsconfig.json rename to packages/interpreter/tsconfig.json From 527434b9f93b9146153d9d94ee0b2f7eaca54bab Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 1 Feb 2022 15:44:08 -0500 Subject: [PATCH 094/256] feat: make the interpreter as its own crate --- packages/desktop/Cargo.toml | 2 + packages/desktop/examples/async.rs | 8 +- packages/desktop/src/interpreter.js | 550 -------------------------- packages/desktop/src/lib.rs | 2 +- packages/web/Cargo.toml | 3 + packages/web/build.rs | 8 + packages/web/{src => }/interpreter.js | 2 +- packages/web/src/bindings.rs | 2 +- 8 files changed, 20 insertions(+), 557 deletions(-) delete mode 100644 packages/desktop/src/interpreter.js create mode 100644 packages/web/build.rs rename packages/web/{src => }/interpreter.js (99%) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 0d393e1ef..fa1bc88dc 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -31,6 +31,8 @@ dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" +dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } + [features] default = ["tokio_runtime"] diff --git a/packages/desktop/examples/async.rs b/packages/desktop/examples/async.rs index 056e7b591..064ef1960 100644 --- a/packages/desktop/examples/async.rs +++ b/packages/desktop/examples/async.rs @@ -10,14 +10,14 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); use_future(&cx, || { - let count = count.for_async(); + let set_count = set_count.clone(); async move { loop { tokio::time::sleep(Duration::from_millis(1000)).await; - *count.modify() += 1; + set_count.modify(|f| f + 1) } } }); @@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element { div { h1 { "High-Five counter: {count}" } button { - onclick: move |_| count.set(0), + onclick: move |_| set_count(0), "Click me!" } } diff --git a/packages/desktop/src/interpreter.js b/packages/desktop/src/interpreter.js deleted file mode 100644 index 503ea7247..000000000 --- a/packages/desktop/src/interpreter.js +++ /dev/null @@ -1,550 +0,0 @@ -export function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} -export class Interpreter { - root; - stack; - listeners; - handlers; - lastNodeWasText; - nodes; - constructor(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - top() { - return this.stack[this.stack.length - 1]; - } - pop() { - return this.stack.pop(); - } - PushRoot(root) { - const node = this.nodes[root]; - this.stack.push(node); - } - AppendChildren(many) { - let root = this.stack[this.stack.length - (1 + many)]; - let to_add = this.stack.splice(this.stack.length - many); - for (let i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - } - ReplaceWith(root_id, m) { - let root = this.nodes[root_id]; - let els = this.stack.splice(this.stack.length - m); - root.replaceWith(...els); - } - InsertAfter(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.after(...new_nodes); - } - InsertBefore(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.before(...new_nodes); - } - Remove(root) { - let node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - } - CreateTextNode(text, root) { - // todo: make it so the types are okay - const node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - } - CreateElement(tag, root) { - const el = document.createElement(tag); - // el.setAttribute("data-dioxus-id", `${root}`); - this.nodes[root] = el; - this.stack.push(el); - } - CreateElementNs(tag, root, ns) { - let el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - } - CreatePlaceholder(root) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - NewEventListener(event_name, root, handler) { - const element = this.nodes[root]; - element.setAttribute("data-dioxus-id", `${root}`); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } else { - this.listeners[event_name]++; - } - } - RemoveEventListener(root, event_name) { - const element = this.nodes[root]; - element.removeAttribute(`data-dioxus-id`); - this.listeners[event_name]--; - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; - } - } - SetText(root, text) { - this.nodes[root].textContent = text; - } - SetAttribute(root, field, value, ns) { - const name = field; - const node = this.nodes[root]; - if (ns == "style") { - // @ts-ignore - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root, name) { - const node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - } - handleEdits(edits) { - this.stack.push(this.root); - for (let edit of edits) { - this.handleEdit(edit); - } - } - handleEdit(edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event) => { - let target = event.target; - if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); - let shouldPreventDefault = target.getAttribute( - `dioxus-prevent-default` - ); - - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - console.log("click", event); - console.log("clickeded", event.target); - console.log("clickeded", event.target.tagName); - if (target.tagName == "A") { - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.rpc.call("browser_open", { href }); - } - } - } - } - - // walk the tree to find the real element - while (realId == null && target.parentElement != null) { - target = target.parentElement; - realId = target.getAttribute(`data-dioxus-id`); - } - - shouldPreventDefault = target.getAttribute( - `dioxus-prevent-default` - ); - - let contents = serialize_event(event); - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - if (event.type == "submit") { - event.preventDefault(); - } - if (realId == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - } -} -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { - charCode, - key, - altKey, - ctrlKey, - metaKey, - keyCode, - shiftKey, - location, - repeat, - which, - } = event; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - locale: "locale", - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - return { - value: value, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - return { - value: value, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - pointerId, - width, - height, - pressure, - tangentialPressure, - tiltX, - tiltY, - twist, - pointerType, - isPrimary, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 22acc4f85..9181d04dc 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -224,7 +224,7 @@ pub fn launch_with_props( } else if trimmed == "index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") - .body(include_bytes!("./interpreter.js").to_vec()) + .body(dioxus_interpreter_js::INTERPRTER_JS.as_bytes().to_vec()) } else { // Read the file content from file path use std::fs::read; diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index a14d6cb0a..9ac508a8c 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -29,6 +29,9 @@ gloo-timers = { version = "0.2.1", features = ["futures"] } futures-util = "0.3.15" smallstr = "0.2.0" +[build-dependencies] +dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } + [dependencies.web-sys] version = "0.3.51" features = [ diff --git a/packages/web/build.rs b/packages/web/build.rs new file mode 100644 index 000000000..bdccab341 --- /dev/null +++ b/packages/web/build.rs @@ -0,0 +1,8 @@ +use std::{fs::File, io::Write}; + +fn main() { + // write the interpreter code to a local file + let mut file = File::create("interpreter.js").unwrap(); + file.write_all(dioxus_interpreter_js::INTERPRTER_JS.as_bytes()) + .unwrap(); +} diff --git a/packages/web/src/interpreter.js b/packages/web/interpreter.js similarity index 99% rename from packages/web/src/interpreter.js rename to packages/web/interpreter.js index ea50ec3d3..a70177252 100644 --- a/packages/web/src/interpreter.js +++ b/packages/web/interpreter.js @@ -198,7 +198,7 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": - // this handler is only provided on desktop implementations since this + // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event) => { let target = event.target; diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index fa96e3230..55506b73c 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -2,7 +2,7 @@ use js_sys::Function; use wasm_bindgen::prelude::*; use web_sys::{Element, Node}; -#[wasm_bindgen(module = "/src/interpreter.js")] +#[wasm_bindgen(module = "/interpreter.js")] extern "C" { pub type Interpreter; From 3f97cbdaa5347bbcb2e32f68366b32245edd7839 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Tue, 1 Feb 2022 21:44:21 +0100 Subject: [PATCH 095/256] format on save: toml excluded --- .vscode/settings.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 221164c7c..44bcb08e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "desktop", "router" ], - "editor.formatOnSave": false + "editor.formatOnSave": true, + "[toml]": { + "editor.formatOnSave": false + } } From 5b69f9b68611d109cbc4dcc4ead2cdf2c82c01c4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 1 Feb 2022 15:45:03 -0500 Subject: [PATCH 096/256] release: make interpreter crate publishable --- packages/interpreter/Cargo.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 0934c08df..fe150e1f9 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -2,6 +2,14 @@ name = "dioxus-interpreter-js" version = "0.0.0" edition = "2018" +authors = ["Jonathan Kelley"] +description = "JS Intepreter for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://docs.rs/dioxus" +keywords = ["dom", "ui", "gui", "react", "wasm"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 8568678f501c84d679c2fdf44be2a1423db2f1d2 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Tue, 1 Feb 2022 21:50:55 +0100 Subject: [PATCH 097/256] install cargo make --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ce7ffecbd..9a6a7e143 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,6 +33,11 @@ jobs: - uses: Swatinem/rust-cache@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev + - name: Install cargo-make + uses: actions-rs/cargo@v1 + with: + command: install + args: --debug cargo-make - uses: actions-rs/cargo@v1 with: command: make From 71656adc897dc1e3fb50010e10b3488059d12dfe Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 1 Feb 2022 15:54:32 -0500 Subject: [PATCH 098/256] publish: generate our bindings.rs file from an inlinejs snippet --- packages/interpreter/build.rs | 11 +- packages/web/build.rs | 72 ++++- packages/web/src/bindings.rs | 502 +++++++++++++++++++++++++++++++++- 3 files changed, 579 insertions(+), 6 deletions(-) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 1ef8b6548..f6047e356 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -2,5 +2,14 @@ use std::process::Command; fn main() { println!("cargo:rerun-if-changed=interpreter.ts"); - Command::new("tsc").spawn().unwrap(); + match Command::new("tsc").spawn() { + Ok(_) => println!("Was spawned :)"), + Err(e) => { + if let std::io::ErrorKind::NotFound = e.kind() { + println!("`tsc` was not found! Not going to generate new interpreter") + } else { + println!("Some strange error occurred :("); + } + } + } } diff --git a/packages/web/build.rs b/packages/web/build.rs index bdccab341..85c8087c6 100644 --- a/packages/web/build.rs +++ b/packages/web/build.rs @@ -1,8 +1,72 @@ use std::{fs::File, io::Write}; fn main() { - // write the interpreter code to a local file - let mut file = File::create("interpreter.js").unwrap(); - file.write_all(dioxus_interpreter_js::INTERPRTER_JS.as_bytes()) - .unwrap(); + let src = format!( + r###" +use js_sys::Function; +use wasm_bindgen::prelude::*; +use web_sys::{{Element, Node}}; + +#[wasm_bindgen(inline_js = r##"{}"##)] +extern "C" {{ + pub type Interpreter; + + #[wasm_bindgen(constructor)] + pub fn new(arg: Element) -> Interpreter; + + #[wasm_bindgen(method)] + pub fn set_node(this: &Interpreter, id: usize, node: Node); + + #[wasm_bindgen(method)] + pub fn PushRoot(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn AppendChildren(this: &Interpreter, many: u32); + + #[wasm_bindgen(method)] + pub fn ReplaceWith(this: &Interpreter, root: u64, m: u32); + + #[wasm_bindgen(method)] + pub fn InsertAfter(this: &Interpreter, root: u64, n: u32); + + #[wasm_bindgen(method)] + pub fn InsertBefore(this: &Interpreter, root: u64, n: u32); + + #[wasm_bindgen(method)] + pub fn Remove(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateElement(this: &Interpreter, tag: &str, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateElementNs(this: &Interpreter, tag: &str, root: u64, ns: &str); + + #[wasm_bindgen(method)] + pub fn CreatePlaceholder(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function); + + #[wasm_bindgen(method)] + pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); + + #[wasm_bindgen(method)] + pub fn SetText(this: &Interpreter, root: u64, text: &str); + + #[wasm_bindgen(method)] + pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>); + + #[wasm_bindgen(method)] + pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str); +}} +"###, + dioxus_interpreter_js::INTERPRTER_JS + ); + + // write the bindings to a local file + let mut file = File::create("src/bindings.rs").unwrap(); + file.write_all(src.as_bytes()).unwrap(); } diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index 55506b73c..5369708f1 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -1,8 +1,508 @@ + use js_sys::Function; use wasm_bindgen::prelude::*; use web_sys::{Element, Node}; -#[wasm_bindgen(module = "/interpreter.js")] +#[wasm_bindgen(inline_js = r##"export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +export class Interpreter { + root; + stack; + listeners; + handlers; + lastNodeWasText; + nodes; + constructor(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + top() { + return this.stack[this.stack.length - 1]; + } + pop() { + return this.stack.pop(); + } + PushRoot(root) { + const node = this.nodes[root]; + this.stack.push(node); + } + AppendChildren(many) { + let root = this.stack[this.stack.length - (1 + many)]; + let to_add = this.stack.splice(this.stack.length - many); + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + } + ReplaceWith(root_id, m) { + let root = this.nodes[root_id]; + let els = this.stack.splice(this.stack.length - m); + root.replaceWith(...els); + } + InsertAfter(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + InsertBefore(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + Remove(root) { + let node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + } + CreateTextNode(text, root) { + // todo: make it so the types are okay + const node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + } + CreateElement(tag, root) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + this.nodes[root] = el; + this.stack.push(el); + } + CreateElementNs(tag, root, ns) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + CreatePlaceholder(root) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + NewEventListener(event_name, root, handler) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + else { + this.listeners[event_name]++; + } + } + RemoveEventListener(root, event_name) { + const element = this.nodes[root]; + element.removeAttribute(`data-dioxus-id`); + this.listeners[event_name]--; + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; + } + } + SetText(root, text) { + this.nodes[root].textContent = text; + } + SetAttribute(root, field, value, ns) { + const name = field; + const node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } + else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } + else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } + else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root, name) { + const node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + } + handleEdits(edits) { + this.stack.push(this.root); + for (let edit of edits) { + this.handleEdit(edit); + } + } + handleEdit(edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event) => { + let target = event.target; + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + let contents = serialize_event(event); + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + if (event.type == "submit") { + event.preventDefault(); + } + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + if (realId == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} +function serialize_event(event) { + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; + } + case "compositionend": + case "compositionstart": + case "compositionupdate": { + let { data } = event; + return { + data, + }; + } + case "keydown": + case "keypress": + case "keyup": { + let { charCode, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location, + repeat: repeat, + which: which, + locale: "locale", + }; + } + case "focus": + case "blur": { + return {}; + } + case "change": { + let target = event.target; + let value; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } + else { + value = target.value ?? target.textContent; + } + return { + value: value, + }; + } + case "input": + case "invalid": + case "reset": + case "submit": { + let target = event.target; + let value = target.value ?? target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, pointerId, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, pointerType, isPrimary, } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary, + }; + } + case "select": { + return {}; + } + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + const { altKey, ctrlKey, metaKey, shiftKey, } = event; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, + }; + } + case "scroll": { + return {}; + } + case "wheel": { + const { deltaX, deltaY, deltaZ, deltaMode, } = event; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + const { animationName, elapsedTime, pseudoElement, } = event; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + const { propertyName, elapsedTime, pseudoElement, } = event; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": { + return {}; + } + case "toggle": { + return {}; + } + default: { + return {}; + } + } +} +const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, +}; +"##)] extern "C" { pub type Interpreter; From 86bac80040f500c2581fee536eff3a04666bc18c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 1 Feb 2022 15:57:07 -0500 Subject: [PATCH 099/256] fmt: make bindings obey formatting --- packages/web/build.rs | 8 ++++++-- packages/web/src/bindings.rs | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/web/build.rs b/packages/web/build.rs index 85c8087c6..3c9baa7c8 100644 --- a/packages/web/build.rs +++ b/packages/web/build.rs @@ -2,11 +2,15 @@ use std::{fs::File, io::Write}; fn main() { let src = format!( - r###" -use js_sys::Function; + r###"use js_sys::Function; use wasm_bindgen::prelude::*; use web_sys::{{Element, Node}}; +/* +This is an autogenerated file from build.rs. +Do not edit this file directly. +*/ + #[wasm_bindgen(inline_js = r##"{}"##)] extern "C" {{ pub type Interpreter; diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index 5369708f1..4b25260a3 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -1,8 +1,12 @@ - use js_sys::Function; use wasm_bindgen::prelude::*; use web_sys::{Element, Node}; +/* +This is an autogenerated file from build.rs. +Do not edit this file directly. +*/ + #[wasm_bindgen(inline_js = r##"export function main() { let root = window.document.getElementById("main"); if (root != null) { From 12cfd79d51a54e2da6aecd437559a563a7233b8b Mon Sep 17 00:00:00 2001 From: t1m0t Date: Tue, 1 Feb 2022 22:04:21 +0100 Subject: [PATCH 100/256] commented out failing example --- packages/desktop/examples/async.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/desktop/examples/async.rs b/packages/desktop/examples/async.rs index 056e7b591..47b17b1b4 100644 --- a/packages/desktop/examples/async.rs +++ b/packages/desktop/examples/async.rs @@ -1,4 +1,4 @@ -use dioxus::prelude::*; +/* use dioxus::prelude::*; use dioxus_core as dioxus; use dioxus_core_macro::*; use dioxus_hooks::*; @@ -32,3 +32,4 @@ fn app(cx: Scope) -> Element { } }) } + */ From b4923b2b81108630d37a239f31eb5cda6d4626e2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 1 Feb 2022 16:07:50 -0500 Subject: [PATCH 101/256] ci: move gen folder out of src --- packages/interpreter/build.rs | 1 + packages/interpreter/{ => gen}/interpreter.js | 0 packages/interpreter/{ => src}/interpreter.ts | 0 packages/interpreter/src/lib.rs | 2 +- packages/interpreter/tsconfig.json | 2 ++ 5 files changed, 4 insertions(+), 1 deletion(-) rename packages/interpreter/{ => gen}/interpreter.js (100%) rename packages/interpreter/{ => src}/interpreter.ts (100%) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index f6047e356..610e3c3c3 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -2,6 +2,7 @@ use std::process::Command; fn main() { println!("cargo:rerun-if-changed=interpreter.ts"); + match Command::new("tsc").spawn() { Ok(_) => println!("Was spawned :)"), Err(e) => { diff --git a/packages/interpreter/interpreter.js b/packages/interpreter/gen/interpreter.js similarity index 100% rename from packages/interpreter/interpreter.js rename to packages/interpreter/gen/interpreter.js diff --git a/packages/interpreter/interpreter.ts b/packages/interpreter/src/interpreter.ts similarity index 100% rename from packages/interpreter/interpreter.ts rename to packages/interpreter/src/interpreter.ts diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index 9c0229aa5..7d680ae25 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -1 +1 @@ -pub static INTERPRTER_JS: &str = include_str!("../interpreter.js"); +pub static INTERPRTER_JS: &str = include_str!("../gen/interpreter.js"); diff --git a/packages/interpreter/tsconfig.json b/packages/interpreter/tsconfig.json index f63034044..1c1ed1ef9 100644 --- a/packages/interpreter/tsconfig.json +++ b/packages/interpreter/tsconfig.json @@ -8,6 +8,8 @@ "es6", "dom" ], + "rootDir": "src", "strict": true, + "outDir": "gen", } } From 022d6d7850dba2e6f68edab3a89e383c8181321c Mon Sep 17 00:00:00 2001 From: t1m0t Date: Tue, 1 Feb 2022 22:37:34 +0100 Subject: [PATCH 102/256] exclude dioxus-mobile in tests (future feature) --- Makefile.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.toml b/Makefile.toml index 23eeacea2..95abec9de 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -37,6 +37,6 @@ private = true workspace = true [tasks.test] -args = ["test", "--all-targets"] +args = ["test", "--all-targets", "--workspace", "--exclude", "dioxus-mobile"] command = "cargo" private = true From 84959a78262f20a09e39bc4c79fffc383dd29877 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Feb 2022 01:08:59 -0500 Subject: [PATCH 103/256] feat: it works everywhere --- packages/interpreter/README.md | 1 - packages/interpreter/gen/interpreter.js | 28 +- packages/interpreter/src/interpreter.ts | 29 +- packages/web/interpreter.js | 499 ------------------------ packages/web/src/bindings.rs | 28 +- 5 files changed, 40 insertions(+), 545 deletions(-) delete mode 100644 packages/web/interpreter.js diff --git a/packages/interpreter/README.md b/packages/interpreter/README.md index bab57ac16..767fe31c0 100644 --- a/packages/interpreter/README.md +++ b/packages/interpreter/README.md @@ -4,5 +4,4 @@ After diffing old and new trees, the Dioxus VirtualDom produces patches that are In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets. - For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks. diff --git a/packages/interpreter/gen/interpreter.js b/packages/interpreter/gen/interpreter.js index a70177252..0d89efa85 100644 --- a/packages/interpreter/gen/interpreter.js +++ b/packages/interpreter/gen/interpreter.js @@ -204,12 +204,24 @@ export class Interpreter { let target = event.target; if (target != null) { let realId = target.getAttribute(`data-dioxus-id`); + let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.rpc.call("browser_open", { href }); + } + } + } + } // walk the tree to find the real element while (realId == null && target.parentElement != null) { target = target.parentElement; realId = target.getAttribute(`data-dioxus-id`); } - const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); let contents = serialize_event(event); if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); @@ -217,20 +229,6 @@ export class Interpreter { if (event.type == "submit") { event.preventDefault(); } - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined && realId != null) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href - }); - } - } - } - } if (realId == null) { return; } diff --git a/packages/interpreter/src/interpreter.ts b/packages/interpreter/src/interpreter.ts index 44d53cda8..f150410f4 100644 --- a/packages/interpreter/src/interpreter.ts +++ b/packages/interpreter/src/interpreter.ts @@ -248,6 +248,19 @@ export class Interpreter { if (target != null) { let realId = target.getAttribute(`data-dioxus-id`); + let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href") + if (href !== "" && href !== null && href !== undefined) { + window.rpc.call("browser_open", { href }); + } + } + } + } // walk the tree to find the real element while (realId == null && target.parentElement != null) { @@ -255,7 +268,7 @@ export class Interpreter { realId = target.getAttribute(`data-dioxus-id`); } - const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); let contents = serialize_event(event); @@ -267,20 +280,6 @@ export class Interpreter { event.preventDefault(); } - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href") - if (href !== "" && href !== null && href !== undefined && realId != null) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href - }); - } - } - } - } if (realId == null) { return; diff --git a/packages/web/interpreter.js b/packages/web/interpreter.js deleted file mode 100644 index a70177252..000000000 --- a/packages/web/interpreter.js +++ /dev/null @@ -1,499 +0,0 @@ -export function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} -export class Interpreter { - root; - stack; - listeners; - handlers; - lastNodeWasText; - nodes; - constructor(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - top() { - return this.stack[this.stack.length - 1]; - } - pop() { - return this.stack.pop(); - } - PushRoot(root) { - const node = this.nodes[root]; - this.stack.push(node); - } - AppendChildren(many) { - let root = this.stack[this.stack.length - (1 + many)]; - let to_add = this.stack.splice(this.stack.length - many); - for (let i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - } - ReplaceWith(root_id, m) { - let root = this.nodes[root_id]; - let els = this.stack.splice(this.stack.length - m); - root.replaceWith(...els); - } - InsertAfter(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.after(...new_nodes); - } - InsertBefore(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.before(...new_nodes); - } - Remove(root) { - let node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - } - CreateTextNode(text, root) { - // todo: make it so the types are okay - const node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - } - CreateElement(tag, root) { - const el = document.createElement(tag); - // el.setAttribute("data-dioxus-id", `${root}`); - this.nodes[root] = el; - this.stack.push(el); - } - CreateElementNs(tag, root, ns) { - let el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - } - CreatePlaceholder(root) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - NewEventListener(event_name, root, handler) { - const element = this.nodes[root]; - element.setAttribute("data-dioxus-id", `${root}`); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } - else { - this.listeners[event_name]++; - } - } - RemoveEventListener(root, event_name) { - const element = this.nodes[root]; - element.removeAttribute(`data-dioxus-id`); - this.listeners[event_name]--; - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; - } - } - SetText(root, text) { - this.nodes[root].textContent = text; - } - SetAttribute(root, field, value, ns) { - const name = field; - const node = this.nodes[root]; - if (ns == "style") { - // @ts-ignore - node.style[name] = value; - } - else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } - else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } - else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root, name) { - const node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - } - handleEdits(edits) { - this.stack.push(this.root); - for (let edit of edits) { - this.handleEdit(edit); - } - } - handleEdit(edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event) => { - let target = event.target; - if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); - // walk the tree to find the real element - while (realId == null && target.parentElement != null) { - target = target.parentElement; - realId = target.getAttribute(`data-dioxus-id`); - } - const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - let contents = serialize_event(event); - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - if (event.type == "submit") { - event.preventDefault(); - } - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined && realId != null) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href - }); - } - } - } - } - if (realId == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - } -} -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { charCode, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - locale: "locale", - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } - else { - value = target.value ?? target.textContent; - } - return { - value: value, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - return { - value: value, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, pointerId, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, pointerType, isPrimary, } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey, } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode, } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement, } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement, } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index 4b25260a3..4834146af 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -213,12 +213,24 @@ export class Interpreter { let target = event.target; if (target != null) { let realId = target.getAttribute(`data-dioxus-id`); + let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.rpc.call("browser_open", { href }); + } + } + } + } // walk the tree to find the real element while (realId == null && target.parentElement != null) { target = target.parentElement; realId = target.getAttribute(`data-dioxus-id`); } - const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); let contents = serialize_event(event); if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); @@ -226,20 +238,6 @@ export class Interpreter { if (event.type == "submit") { event.preventDefault(); } - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined && realId != null) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href - }); - } - } - } - } if (realId == null) { return; } From 8f9d15db4aa2edcec65a478ce68de27b2264a5f1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Feb 2022 02:00:53 -0500 Subject: [PATCH 104/256] feat: enable form elements in web --- packages/interpreter/gen/interpreter.js | 2 ++ packages/web/src/bindings.rs | 2 ++ packages/web/src/dom.rs | 31 ++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/interpreter/gen/interpreter.js b/packages/interpreter/gen/interpreter.js index c582bdf66..62cf3dd13 100644 --- a/packages/interpreter/gen/interpreter.js +++ b/packages/interpreter/gen/interpreter.js @@ -198,6 +198,8 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation let handler = (event) => { let target = event.target; if (target != null) { diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index 2b773ecd5..fe25fcec0 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -207,6 +207,8 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation let handler = (event) => { let target = event.target; if (target != null) { diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 112154938..67acf7890 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -215,7 +215,36 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc() { + let elements = form.elements(); + for x in 0..elements.length() { + let element = elements.item(x).unwrap(); + if let Some(name) = element.get_attribute("name") { + let value: String = (&element) + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => input.value() + } + }) + .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| input.value())) + .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlSelectElement| input.value())) + .or_else(|| target.dyn_ref::().unwrap().text_content()) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + values.insert(name, value); + } + } + } + Arc::new(FormData { value, values }) } "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" From 2c4e7beae80a7f36a26f2684063afa76e0bd86b6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Feb 2022 11:32:00 -0500 Subject: [PATCH 105/256] wip: update interpreter --- packages/interpreter/src/interpreter.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/interpreter/src/interpreter.ts b/packages/interpreter/src/interpreter.ts index ec48f9c26..41036905b 100644 --- a/packages/interpreter/src/interpreter.ts +++ b/packages/interpreter/src/interpreter.ts @@ -263,7 +263,12 @@ export class Interpreter { } // walk the tree to find the real element - while (realId == null && target.parentElement != null) { + while (realId == null) { + // we've reached the root we don't want to send an event + if (target.parentElement === null) { + return; + } + target = target.parentElement; realId = target.getAttribute(`data-dioxus-id`); } @@ -326,7 +331,7 @@ export class Interpreter { -function serialize_event(event: Event) { +export function serialize_event(event: Event) { switch (event.type) { case "copy": case "cut": From b2b2dae7e611cb9eb0a0f5864759921151919655 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Feb 2022 11:33:02 -0500 Subject: [PATCH 106/256] fix: diffing allows component reuse --- packages/core/src/diff.rs | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 3f3629c10..48163ccf7 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -270,26 +270,23 @@ impl<'b> DiffState<'b> { fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize { let parent_idx = self.current_scope().unwrap(); - // ensure this scope doesn't already exist if we're trying to create it - debug_assert!( - vcomponent - .scope - .get() - .and_then(|f| self.scopes.get_scope(f)) - .is_none(), - "component scope already exists" - ); - - // Insert a new scope into our component list - let props: Box = vcomponent.props.borrow_mut().take().unwrap(); - let props: Box = unsafe { std::mem::transmute(props) }; - let new_idx = self.scopes.new_with_key( - vcomponent.user_fc, - props, - Some(parent_idx), - self.element_stack.last().copied().unwrap(), - 0, - ); + // the component might already exist - if it does, we need to reuse it + // this makes figure out when to drop the component more complicated + let new_idx = if let Some(idx) = vcomponent.scope.get() { + assert!(self.scopes.get_scope(idx).is_some()); + idx + } else { + // Insert a new scope into our component list + let props: Box = vcomponent.props.borrow_mut().take().unwrap(); + let props: Box = unsafe { std::mem::transmute(props) }; + self.scopes.new_with_key( + vcomponent.user_fc, + props, + Some(parent_idx), + self.element_stack.last().copied().unwrap(), + 0, + ) + }; // Actually initialize the caller's slot with the right address vcomponent.scope.set(Some(new_idx)); From 24d78682ad7dfe72297844929b7d79a321e73d1e Mon Sep 17 00:00:00 2001 From: t1m0t Date: Wed, 2 Feb 2022 21:35:57 +0100 Subject: [PATCH 107/256] add docker folder for handling local tests --- docker | 1 + 1 file changed, 1 insertion(+) create mode 160000 docker diff --git a/docker b/docker new file mode 160000 index 000000000..022d6d785 --- /dev/null +++ b/docker @@ -0,0 +1 @@ +Subproject commit 022d6d7850dba2e6f68edab3a89e383c8181321c From f1865faef790754ca3192a5a9a387e2f225e72a0 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Thu, 3 Feb 2022 00:11:38 +0100 Subject: [PATCH 108/256] setup done, local tests work, check workflow --- .github/workflows/main.yml | 2 +- packages/mobile/Cargo.toml | 5 +++++ packages/mobile/Makefile.toml | 7 +++++++ packages/router/Cargo.toml | 8 ++++++++ packages/router/Makefile.toml | 6 +++--- packages/router/tests/route.rs | 9 ++------- packages/router/webdriver.json | 10 ++++++++++ 7 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 packages/mobile/Makefile.toml create mode 100644 packages/router/webdriver.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9a6a7e143..d649d47ab 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: override: true - uses: Swatinem/rust-cache@v1 - run: sudo apt-get update - - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev + - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev firefox-csr - name: Install cargo-make uses: actions-rs/cargo@v1 with: diff --git a/packages/mobile/Cargo.toml b/packages/mobile/Cargo.toml index 9da440343..50647a693 100644 --- a/packages/mobile/Cargo.toml +++ b/packages/mobile/Cargo.toml @@ -13,3 +13,8 @@ license = "MIT/Apache-2.0" [dependencies] dioxus-desktop = { path = "../desktop", version = "^0.1.6" } + +[lib] +doctest = false +# tests suspended until package ready +test = false \ No newline at end of file diff --git a/packages/mobile/Makefile.toml b/packages/mobile/Makefile.toml new file mode 100644 index 000000000..90f08a170 --- /dev/null +++ b/packages/mobile/Makefile.toml @@ -0,0 +1,7 @@ +[tasks.test] +command = "cargo" +args = [ + "test", + "--no-run", +] + diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index af701d2f2..b35760889 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -43,6 +43,7 @@ web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"] desktop = [] mobile = [] derive = [] +wasm_test = [] [dev-dependencies] console_error_panic_hook = "0.1.7" @@ -50,3 +51,10 @@ dioxus-web = { path = "../web" } log = "0.4.14" wasm-logger = "0.2.0" wasm-bindgen-test = "0.3" +gloo-utils = "0.1.2" + +[dev-dependencies.web-sys] +version = "0.3" +features = [ + "Document", +] \ No newline at end of file diff --git a/packages/router/Makefile.toml b/packages/router/Makefile.toml index 27a6c9244..989b1cc7b 100644 --- a/packages/router/Makefile.toml +++ b/packages/router/Makefile.toml @@ -1,10 +1,10 @@ [tasks.test] +extend = "core::wasm-pack-base" +command = "wasm-pack" args = [ "test", "@@split(DIOXUS_TEST_FLAGS, )", "--", "--features", "${DIOXUS_TEST_FEATURES}", -] -command = "wasm-pack" -extend = "core::wasm-pack-base" +] \ No newline at end of file diff --git a/packages/router/tests/route.rs b/packages/router/tests/route.rs index 08a5b0aff..89b7a64fa 100644 --- a/packages/router/tests/route.rs +++ b/packages/router/tests/route.rs @@ -52,17 +52,12 @@ fn simple_test() { cx.render(rsx! { div { - id: "test1", - id + } }) } main(); - let element = gloo_utils::document() - .get_element_by_id("test1") - .expect("No result found. Most likely, the application crashed") - .inner_html(); - assert!(element, ""); + let element = gloo_utils::document(); } diff --git a/packages/router/webdriver.json b/packages/router/webdriver.json new file mode 100644 index 000000000..5f6013c9d --- /dev/null +++ b/packages/router/webdriver.json @@ -0,0 +1,10 @@ +{ + "moz:firefoxOptions": { + "binary": "/usr/bin/firefox", + "prefs": { + "media.navigator.streams.fake": true, + "media.navigator.permission.disabled": true + }, + "args": [] + } + } \ No newline at end of file From 3b55669373a7cc23eda53b996d574a1f4aac32b1 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Thu, 3 Feb 2022 00:16:56 +0100 Subject: [PATCH 109/256] try fix CI --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d649d47ab..4ab05df1c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,13 +32,14 @@ jobs: override: true - uses: Swatinem/rust-cache@v1 - run: sudo apt-get update - - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev firefox-csr + - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev - name: Install cargo-make uses: actions-rs/cargo@v1 with: command: install args: --debug cargo-make - uses: actions-rs/cargo@v1 + - uses: browser-actions/setup-firefox@latest with: command: make args: tests From bc43069884d74fe01f77cdfbc2b492db5f9a328e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Feb 2022 19:04:37 -0500 Subject: [PATCH 110/256] fix: allow scopes and nodes to be missing --- packages/core/src/diff.rs | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 48163ccf7..2c8a66ca8 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -329,10 +329,11 @@ impl<'b> DiffState<'b> { old_node: &'b VNode<'b>, new_node: &'b VNode<'b>, ) { - let root = old - .id - .get() - .expect("existing element nodes should have an ElementId"); + // if the node is comming back not assigned, that means it was borrowed but removed + let root = match old.id.get() { + Some(id) => id, + None => self.scopes.reserve_node(new_node), + }; // If the element type is completely different, the element needs to be re-rendered completely // This is an optimization React makes due to how users structure their code @@ -931,12 +932,18 @@ impl<'b> DiffState<'b> { log::trace!("Replacing component x2 {:?}", old); - // we can only remove components if they are actively being diffed - if self.scope_stack.contains(&c.originator) { - log::trace!("Removing component {:?}", old); + let scope = self.scopes.get_scope(scope_id).unwrap(); + c.scope.set(None); + let props = scope.props.take().unwrap(); + c.props.borrow_mut().replace(props); + self.scopes.try_remove(scope_id).unwrap(); - self.scopes.try_remove(scope_id).unwrap(); - } + // // we can only remove components if they are actively being diffed + // if self.scope_stack.contains(&c.originator) { + // log::trace!("Removing component {:?}", old); + + // self.scopes.try_remove(scope_id).unwrap(); + // } } self.leave_scope(); } @@ -950,6 +957,7 @@ impl<'b> DiffState<'b> { // this check exists because our null node will be removed but does not have an ID if let Some(id) = t.id.get() { self.scopes.collect_garbage(id); + t.id.set(None); if gen_muts { self.mutations.remove(id.as_u64()); @@ -959,6 +967,7 @@ impl<'b> DiffState<'b> { VNode::Placeholder(a) => { let id = a.id.get().unwrap(); self.scopes.collect_garbage(id); + a.id.set(None); if gen_muts { self.mutations.remove(id.as_u64()); @@ -972,6 +981,7 @@ impl<'b> DiffState<'b> { } self.scopes.collect_garbage(id); + e.id.set(None); self.remove_nodes(e.children, false); } @@ -987,10 +997,12 @@ impl<'b> DiffState<'b> { let root = self.scopes.root_node(scope_id); self.remove_nodes([root], gen_muts); - // we can only remove this node if the originator is actively in our stackß - if self.scope_stack.contains(&c.originator) { - self.scopes.try_remove(scope_id).unwrap(); - } + let scope = self.scopes.get_scope(scope_id).unwrap(); + c.scope.set(None); + + let props = scope.props.take().unwrap(); + c.props.borrow_mut().replace(props); + self.scopes.try_remove(scope_id).unwrap(); } self.leave_scope(); } From f7b3228ab2a3bc199834b478001d37022ffd05c4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 2 Feb 2022 20:04:55 -0500 Subject: [PATCH 111/256] fix: enable resume node for text and placeholder --- packages/core/src/diff.rs | 108 ++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 2c8a66ca8..06d8346b4 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -119,11 +119,15 @@ impl<'b> DiffState<'b> { pub fn diff_scope(&mut self, scopeid: ScopeId) { let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); - self.scope_stack.push(scopeid); let scope = self.scopes.get_scope(scopeid).unwrap(); - self.element_stack.push(scope.container); - self.diff_node(old, new); + self.scope_stack.push(scopeid); + self.element_stack.push(scope.container); + { + self.diff_node(old, new); + } + self.element_stack.pop(); + self.scope_stack.pop(); self.mutations.mark_dirty_scope(scopeid); } @@ -131,63 +135,26 @@ impl<'b> DiffState<'b> { pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) { use VNode::{Component, Element, Fragment, Placeholder, Text}; match (old_node, new_node) { - // Check the most common cases first - // these are *actual* elements, not wrappers around lists (Text(old), Text(new)) => { - if std::ptr::eq(old, new) { - return; - } - - let root = old - .id - .get() - .expect("existing text nodes should have an ElementId"); - - if old.text != new.text { - self.mutations.set_text(new.text, root.as_u64()); - } - self.scopes.update_node(new_node, root); - - new.id.set(Some(root)); + self.diff_text_nodes(old, new, old_node, new_node); } (Placeholder(old), Placeholder(new)) => { - if std::ptr::eq(old, new) { - return; - } - - let root = old - .id - .get() - .expect("existing placeholder nodes should have an ElementId"); - - self.scopes.update_node(new_node, root); - new.id.set(Some(root)); + self.diff_placeholder_nodes(old, new, old_node, new_node); } (Element(old), Element(new)) => { - if std::ptr::eq(old, new) { - return; - } self.diff_element_nodes(old, new, old_node, new_node); } - // These two sets are pointers to nodes but are not actually nodes themselves (Component(old), Component(new)) => { - if std::ptr::eq(old, new) { - return; - } self.diff_component_nodes(old_node, new_node, *old, *new); } (Fragment(old), Fragment(new)) => { - if std::ptr::eq(old, new) { - return; - } self.diff_fragment_nodes(old, new); } - // Anything else is just a basic replace and create ( Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), @@ -322,6 +289,53 @@ impl<'b> DiffState<'b> { created } + pub(crate) fn diff_text_nodes( + &mut self, + old: &'b VText<'b>, + new: &'b VText<'b>, + _old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + ) { + if std::ptr::eq(old, new) { + return; + } + + // if the node is comming back not assigned, that means it was borrowed but removed + let root = match old.id.get() { + Some(id) => id, + None => self.scopes.reserve_node(new_node), + }; + + if old.text != new.text { + self.mutations.set_text(new.text, root.as_u64()); + } + + self.scopes.update_node(new_node, root); + + new.id.set(Some(root)); + } + + pub(crate) fn diff_placeholder_nodes( + &mut self, + old: &'b VPlaceholder, + new: &'b VPlaceholder, + _old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + ) { + if std::ptr::eq(old, new) { + return; + } + + // if the node is comming back not assigned, that means it was borrowed but removed + let root = match old.id.get() { + Some(id) => id, + None => self.scopes.reserve_node(new_node), + }; + + self.scopes.update_node(new_node, root); + new.id.set(Some(root)); + } + fn diff_element_nodes( &mut self, old: &'b VElement<'b>, @@ -329,6 +343,10 @@ impl<'b> DiffState<'b> { old_node: &'b VNode<'b>, new_node: &'b VNode<'b>, ) { + if std::ptr::eq(old, new) { + return; + } + // if the node is comming back not assigned, that means it was borrowed but removed let root = match old.id.get() { Some(id) => id, @@ -490,6 +508,10 @@ impl<'b> DiffState<'b> { } fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) { + if std::ptr::eq(old, new) { + return; + } + // This is the case where options or direct vnodes might be used. // In this case, it's faster to just skip ahead to their diff if old.children.len() == 1 && new.children.len() == 1 { @@ -1106,9 +1128,7 @@ impl<'b> DiffState<'b> { for child in el.children.iter() { num_on_stack += self.push_all_nodes(child); } - self.mutations.push_root(el.id.get().unwrap()); - num_on_stack + 1 } } From 1913ccd61f24d3b23aecc0fe3151716fec2ea1ec Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 3 Feb 2022 00:35:10 -0500 Subject: [PATCH 112/256] chore: current_scope always panics --- packages/core/src/diff.rs | 44 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 06d8346b4..d7189a710 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -208,9 +208,7 @@ impl<'b> DiffState<'b> { { self.mutations.create_element(tag_name, *namespace, real_id); - let cur_scope_id = self - .current_scope() - .expect("diffing should always have a scope"); + let cur_scope_id = self.current_scope(); for listener in listeners.iter() { listener.mounted_node.set(Some(real_id)); @@ -235,7 +233,7 @@ impl<'b> DiffState<'b> { } fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize { - let parent_idx = self.current_scope().unwrap(); + let parent_idx = self.current_scope(); // the component might already exist - if it does, we need to reuse it // this makes figure out when to drop the component more complicated @@ -401,25 +399,25 @@ impl<'b> DiffState<'b> { // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) // // TODO: take a more efficient path than this - if let Some(cur_scope_id) = self.current_scope() { - if old.listeners.len() == new.listeners.len() { - for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { - if old_l.event != new_l.event { - self.mutations - .remove_event_listener(old_l.event, root.as_u64()); - self.mutations.new_event_listener(new_l, cur_scope_id); - } - new_l.mounted_node.set(old_l.mounted_node.get()); - } - } else { - for listener in old.listeners { + let cur_scope_id = self.current_scope(); + + if old.listeners.len() == new.listeners.len() { + for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { + if old_l.event != new_l.event { self.mutations - .remove_event_listener(listener.event, root.as_u64()); - } - for listener in new.listeners { - listener.mounted_node.set(Some(root)); - self.mutations.new_event_listener(listener, cur_scope_id); + .remove_event_listener(old_l.event, root.as_u64()); + self.mutations.new_event_listener(new_l, cur_scope_id); } + new_l.mounted_node.set(old_l.mounted_node.get()); + } + } else { + for listener in old.listeners { + self.mutations + .remove_event_listener(listener.event, root.as_u64()); + } + for listener in new.listeners { + listener.mounted_node.set(Some(root)); + self.mutations.new_event_listener(listener, cur_scope_id); } } @@ -1057,8 +1055,8 @@ impl<'b> DiffState<'b> { self.mutations.insert_before(first, created as u32); } - fn current_scope(&self) -> Option { - self.scope_stack.last().copied() + fn current_scope(&self) -> ScopeId { + self.scope_stack.last().copied().expect("no current scope") } fn enter_scope(&mut self, scope: ScopeId) { From 4135fabb8d496bd059b9733b910014976086672f Mon Sep 17 00:00:00 2001 From: t1m0t Date: Thu, 3 Feb 2022 09:24:35 +0100 Subject: [PATCH 113/256] try fix docker folder --- docker | 1 - 1 file changed, 1 deletion(-) delete mode 160000 docker diff --git a/docker b/docker deleted file mode 160000 index 022d6d785..000000000 --- a/docker +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 022d6d7850dba2e6f68edab3a89e383c8181321c From 74cc2eee813430ff0e521f70f31e710107e17ea1 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Thu, 3 Feb 2022 09:26:01 +0100 Subject: [PATCH 114/256] adding docker folder --- docker/Dockerfile_pre_test | 9 +++++++++ docker/Dockerfile_test | 8 ++++++++ docker/README.md | 27 +++++++++++++++++++++++++++ docker/run_local_tests.sh | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 docker/Dockerfile_pre_test create mode 100644 docker/Dockerfile_test create mode 100644 docker/README.md create mode 100644 docker/run_local_tests.sh diff --git a/docker/Dockerfile_pre_test b/docker/Dockerfile_pre_test new file mode 100644 index 000000000..6e0af4987 --- /dev/null +++ b/docker/Dockerfile_pre_test @@ -0,0 +1,9 @@ +FROM rust:1.58-buster + +RUN apt update +RUN apt install -y libglib2.0-dev libgtk-3-dev libsoup2.4-dev libappindicator3-dev libwebkit2gtk-4.0-dev firefox-esr + +RUN cargo install cargo-make --debug +RUN cargo install cargo-cache && cargo cache -a + +CMD ["echo","\"base image built\""] diff --git a/docker/Dockerfile_test b/docker/Dockerfile_test new file mode 100644 index 000000000..f6265c2bb --- /dev/null +++ b/docker/Dockerfile_test @@ -0,0 +1,8 @@ +FROM dioxus-base-test-image + +RUN mkdir run_test +COPY tmp /run_test +WORKDIR /run_test +RUN cargo make tests + +CMD ["exit"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..57578f405 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,27 @@ +# Why this? + +This part is used to test whole package before pushing it + +# How to use it? + +Just run in the folder: +`bash run_local_tests.sh`. If nothing fails, then you can push your code to the repo. +or run: +`bash run_local_tests.sh --with-full-docker-cleanup` +for cleaning up images as well + +# How is it composed of? + + 1. `Dockerfile_pre_test` will build the base image for the tests to be run into + 2. `Dockerfile_test` will run the actual tests based on 1. + 3. `run_local_tests.sh` to wrap this up + +# Warning + +The task requires some amount of CPU work and disk space (5GB per tests). Some clean up is included in the script. + +# Requirements + + * [docker](https://docs.docker.com/engine/install/) + * bash + * rsync \ No newline at end of file diff --git a/docker/run_local_tests.sh b/docker/run_local_tests.sh new file mode 100644 index 000000000..0d6c1fb8b --- /dev/null +++ b/docker/run_local_tests.sh @@ -0,0 +1,36 @@ +set -eux + +echo "Test script started" + +function run_script { + if [[ -d tmp ]] + then + rm -rf tmp + mkdir tmp + else + mkdir tmp + fi + + # copy files first + rsync -a --progress ../ tmp --exclude target --exclude docker + + # build base image + docker build -f Dockerfile_pre_test -t dioxus-base-test-image . + # run test + docker build -f Dockerfile_test -t dioxus-test-image . + + # clean up + rm -rf tmp + if [ $1 = "--with-full-docker-cleanup" ] + then + docker image rm dioxus-base-test-image + docker image rm dioxus-test-image + docker system prune -a --force + fi +} + +run_script || echo "Error occured.. cleaning a bit." && + docker system prune -a --force && \ + rm -rf tmp; + +echo "Script finished to execute" From ee49fc27ffd6977e0ac62dc81d2e4604c975415d Mon Sep 17 00:00:00 2001 From: t1m0t Date: Thu, 3 Feb 2022 09:28:06 +0100 Subject: [PATCH 115/256] fix some newlines --- packages/mobile/Cargo.toml | 2 +- packages/router/Cargo.toml | 2 +- packages/router/Makefile.toml | 2 +- packages/router/webdriver.json | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/mobile/Cargo.toml b/packages/mobile/Cargo.toml index 50647a693..cdf0a5c05 100644 --- a/packages/mobile/Cargo.toml +++ b/packages/mobile/Cargo.toml @@ -17,4 +17,4 @@ dioxus-desktop = { path = "../desktop", version = "^0.1.6" } [lib] doctest = false # tests suspended until package ready -test = false \ No newline at end of file +test = false diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index b35760889..239a3c5d8 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -57,4 +57,4 @@ gloo-utils = "0.1.2" version = "0.3" features = [ "Document", -] \ No newline at end of file +] diff --git a/packages/router/Makefile.toml b/packages/router/Makefile.toml index 989b1cc7b..063585c98 100644 --- a/packages/router/Makefile.toml +++ b/packages/router/Makefile.toml @@ -7,4 +7,4 @@ args = [ "--", "--features", "${DIOXUS_TEST_FEATURES}", -] \ No newline at end of file +] diff --git a/packages/router/webdriver.json b/packages/router/webdriver.json index 5f6013c9d..81413f24b 100644 --- a/packages/router/webdriver.json +++ b/packages/router/webdriver.json @@ -7,4 +7,5 @@ }, "args": [] } - } \ No newline at end of file + } + \ No newline at end of file From fed4a68650082d6dedefee0cf26efea3cffba276 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Thu, 3 Feb 2022 10:01:45 +0100 Subject: [PATCH 116/256] improve script --- docker/run_local_tests.sh | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/docker/run_local_tests.sh b/docker/run_local_tests.sh index 0d6c1fb8b..804d87afb 100644 --- a/docker/run_local_tests.sh +++ b/docker/run_local_tests.sh @@ -6,11 +6,8 @@ function run_script { if [[ -d tmp ]] then rm -rf tmp - mkdir tmp - else - mkdir tmp fi - + mkdir tmp # copy files first rsync -a --progress ../ tmp --exclude target --exclude docker @@ -21,16 +18,21 @@ function run_script { # clean up rm -rf tmp - if [ $1 = "--with-full-docker-cleanup" ] + if [ $# -ge 1 ] then - docker image rm dioxus-base-test-image - docker image rm dioxus-test-image - docker system prune -a --force + echo "Got some parameter" + if [ $1 = "--with-full-docker-cleanup" ] + then + docker image rm dioxus-base-test-image + docker image rm dioxus-test-image + docker system prune -af + fi fi } -run_script || echo "Error occured.. cleaning a bit." && - docker system prune -a --force && \ - rm -rf tmp; +run_script || echo "Error occured.. cleaning a bit." && \ + docker system prune -af; + +docker system prune -af echo "Script finished to execute" From 474fd8453855adba3657132948f6bf6ba47bd98d Mon Sep 17 00:00:00 2001 From: t1m0t Date: Thu, 3 Feb 2022 18:14:50 +0100 Subject: [PATCH 117/256] modify folder structure --- {docker => .docker}/Dockerfile_pre_test | 0 {docker => .docker}/Dockerfile_test | 0 {docker => .docker}/README.md | 0 {docker => .docker}/run_local_tests.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {docker => .docker}/Dockerfile_pre_test (100%) rename {docker => .docker}/Dockerfile_test (100%) rename {docker => .docker}/README.md (100%) rename {docker => .docker}/run_local_tests.sh (100%) diff --git a/docker/Dockerfile_pre_test b/.docker/Dockerfile_pre_test similarity index 100% rename from docker/Dockerfile_pre_test rename to .docker/Dockerfile_pre_test diff --git a/docker/Dockerfile_test b/.docker/Dockerfile_test similarity index 100% rename from docker/Dockerfile_test rename to .docker/Dockerfile_test diff --git a/docker/README.md b/.docker/README.md similarity index 100% rename from docker/README.md rename to .docker/README.md diff --git a/docker/run_local_tests.sh b/.docker/run_local_tests.sh similarity index 100% rename from docker/run_local_tests.sh rename to .docker/run_local_tests.sh From add21d5f9d440b0f923cafdb4393e0b587a84b43 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 3 Feb 2022 12:52:05 -0500 Subject: [PATCH 118/256] feat: update bindings and interpreter with new tsc code --- packages/interpreter/gen/interpreter.js | 8 ++++++-- packages/web/src/bindings.rs | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/interpreter/gen/interpreter.js b/packages/interpreter/gen/interpreter.js index 62cf3dd13..429b5c95a 100644 --- a/packages/interpreter/gen/interpreter.js +++ b/packages/interpreter/gen/interpreter.js @@ -217,7 +217,11 @@ export class Interpreter { } } // walk the tree to find the real element - while (realId == null && target.parentElement != null) { + while (realId == null) { + // we've reached the root we don't want to send an event + if (target.parentElement === null) { + return; + } target = target.parentElement; realId = target.getAttribute(`data-dioxus-id`); } @@ -270,7 +274,7 @@ export class Interpreter { } } } -function serialize_event(event) { +export function serialize_event(event) { switch (event.type) { case "copy": case "cut": diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index fe25fcec0..356f5b8eb 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -226,7 +226,11 @@ export class Interpreter { } } // walk the tree to find the real element - while (realId == null && target.parentElement != null) { + while (realId == null) { + // we've reached the root we don't want to send an event + if (target.parentElement === null) { + return; + } target = target.parentElement; realId = target.getAttribute(`data-dioxus-id`); } @@ -279,7 +283,7 @@ export class Interpreter { } } } -function serialize_event(event) { +export function serialize_event(event) { switch (event.type) { case "copy": case "cut": From a9ac0568e24d1ddaa4a896bedec1cc2b32bb25c6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 3 Feb 2022 13:19:41 -0500 Subject: [PATCH 119/256] fix: allow prevent_default on svg --- examples/svg.rs | 99 ++++++++++++++++++++++++++ packages/html/src/elements.rs | 7 +- packages/html/src/global_attributes.rs | 16 +++++ 3 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 examples/svg.rs diff --git a/examples/svg.rs b/examples/svg.rs new file mode 100644 index 000000000..781b35f2c --- /dev/null +++ b/examples/svg.rs @@ -0,0 +1,99 @@ +use dioxus::{events::MouseEvent, prelude::*}; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let (val, set_val) = use_state(&cx, || 5); + + cx.render(rsx! { + div { + user_select: "none", + webkit_user_select: "none", + margin_left: "10%", + margin_right: "10%", + h1 { "Click die to generate a new value" } + div { + cursor: "pointer", + height: "80%", + width: "80%", + Die { + value: *val, + keep: true, + onclick: move |_| { + use rand::Rng; + let mut rng = rand::thread_rng(); + set_val(rng.gen_range(1..6)); + } + } + } + } + }) +} + +#[derive(Props)] +pub struct DieProps<'a> { + pub value: u64, + pub keep: bool, + pub onclick: EventHandler<'a, MouseEvent>, +} + +const DOTS: [(i64, i64); 7] = [(-1, -1), (-1, -0), (-1, 1), (1, -1), (1, 0), (1, 1), (0, 0)]; +const DOTS_FOR_VALUE: [[bool; 7]; 6] = [ + [false, false, false, false, false, false, true], + [false, false, true, true, false, false, false], + [false, false, true, true, false, false, true], + [true, false, true, true, false, true, false], + [true, false, true, true, false, true, true], + [true, true, true, true, true, true, false], +]; + +const OFFSET: i64 = 600; +const DOT_RADIUS: &str = "200"; +const HELD_COLOR: &str = "#aaa"; +const UNHELD_COLOR: &str = "#ddd"; + +// A six-sided die (D6) with dots. +#[allow(non_snake_case)] +pub fn Die<'a>(cx: Scope<'a, DieProps<'a>>) -> Element { + let &DieProps { value, keep, .. } = cx.props; + + let active_dots = &DOTS_FOR_VALUE[(value - 1) as usize]; + let fill = if keep { HELD_COLOR } else { UNHELD_COLOR }; + let dots = DOTS + .iter() + .zip(active_dots.iter()) + .filter(|(_, &active)| active) + .map(|((x, y), _)| { + let dcx = x * OFFSET; + let dcy = y * OFFSET; + rsx!(circle { + cx: "{dcx}", + cy: "{dcy}", + r: "{DOT_RADIUS}", + fill: "#333" + }) + }); + + rsx!(cx, + svg { + onclick: move |e| cx.props.onclick.call(e), + prevent_default: "onclick", + "dioxus-prevent-default": "onclick", + class: "die", + view_box: "-1000 -1000 2000 2000", + + rect { + x: "-1000", + y: "-1000", + width: "2000", + height: "2000", + rx: "{DOT_RADIUS}", + fill: "{fill}", + } + + dots + } + ) +} diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 1a2f36e57..c6b1cb7e0 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1047,7 +1047,7 @@ impl input { /// - `text` /// - `time` /// - `url` - /// - `week` + /// - `week` pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { cx.attr("type", val, None, false) } @@ -1100,11 +1100,6 @@ impl label { cx.attr("for", val, None, false) } } -impl a { - pub fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { - cx.attr("dioxus-prevent-default", val, None, false) - } -} builder_constructors! { // SVG components diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 1368f87e3..151a1539b 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -49,6 +49,10 @@ macro_rules! aria_trait_methods { } pub trait GlobalAttributes { + /// Prevent the default action for this element. + /// + /// For more information, see the MDN docs: + /// fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { cx.attr("dioxus-prevent-default", val, None, false) } @@ -603,6 +607,11 @@ pub trait GlobalAttributes { /// Specifies the speed curve of the transition effect. transition_timing_function: "transition-timing-function", + /// The user-select CSS property controls whether the user can select text. + /// This doesn't have any effect on content loaded as part of a browser's user interface (its chrome), except in textboxes. + user_select: "user-select", + webkit_user_select: "-webkit-user-select", + /// Sets the vertical positioning of an element relative to the current text baseline. vertical_align: "vertical-align", @@ -685,6 +694,13 @@ pub trait GlobalAttributes { } pub trait SvgAttributes { + /// Prevent the default action for this element. + /// + /// For more information, see the MDN docs: + /// + fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { + cx.attr("dioxus-prevent-default", val, None, false) + } aria_trait_methods! { accent_height: "accent-height", accumulate: "accumulate", From 89cfda4bf941269879eab4ea630b063e5fda2152 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 3 Feb 2022 13:21:20 -0500 Subject: [PATCH 120/256] fix: add attributions --- examples/svg.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/svg.rs b/examples/svg.rs index 781b35f2c..6e5676fe3 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -1,3 +1,5 @@ +// Thanks to @japsu and their project https://github.com/japsu/jatsi for the example! + use dioxus::{events::MouseEvent, prelude::*}; fn main() { From ac4faec570999cb135c75b519da7bf4f72d2ec16 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 3 Feb 2022 13:30:24 -0500 Subject: [PATCH 121/256] fix: remove broken example --- packages/desktop/examples/async.rs | 35 ------------------------------ 1 file changed, 35 deletions(-) delete mode 100644 packages/desktop/examples/async.rs diff --git a/packages/desktop/examples/async.rs b/packages/desktop/examples/async.rs deleted file mode 100644 index 0668834f9..000000000 --- a/packages/desktop/examples/async.rs +++ /dev/null @@ -1,35 +0,0 @@ -/* use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_hooks::*; -use dioxus_html as dioxus_elements; -use std::time::Duration; - -fn main() { - dioxus_desktop::launch(app); -} - -fn app(cx: Scope) -> Element { - let (count, set_count) = use_state(&cx, || 0); - - use_future(&cx, || { - let set_count = set_count.clone(); - async move { - loop { - tokio::time::sleep(Duration::from_millis(1000)).await; - set_count.modify(|f| f + 1) - } - } - }); - - cx.render(rsx! { - div { - h1 { "High-Five counter: {count}" } - button { - onclick: move |_| set_count(0), - "Click me!" - } - } - }) -} - */ From ca0d7dc268a6ebb980f790206ebd79ce16e7bad2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 01:56:54 -0500 Subject: [PATCH 122/256] fix: remove code generation --- packages/interpreter/Cargo.toml | 8 + packages/interpreter/build.rs | 16 - .../build.rs => interpreter/src/bindings.rs} | 25 +- .../interpreter/{gen => src}/interpreter.js | 0 packages/interpreter/src/interpreter.ts | 675 ------------------ packages/interpreter/src/lib.rs | 8 +- packages/interpreter/tsconfig.json | 15 - packages/web/Cargo.toml | 24 +- packages/web/src/bindings.rs | 561 --------------- packages/web/src/dom.rs | 2 +- packages/web/src/lib.rs | 1 - 11 files changed, 31 insertions(+), 1304 deletions(-) delete mode 100644 packages/interpreter/build.rs rename packages/{web/build.rs => interpreter/src/bindings.rs} (77%) rename packages/interpreter/{gen => src}/interpreter.js (100%) delete mode 100644 packages/interpreter/src/interpreter.ts delete mode 100644 packages/interpreter/tsconfig.json delete mode 100644 packages/web/src/bindings.rs diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index fe150e1f9..1e0f4bb51 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -14,3 +14,11 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +wasm-bindgen = { version = "0.2.79", optional = true } +js-sys = { version = "0.3.56", optional = true } +web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } + + +[features] +default = [] +web = ["wasm-bindgen", "js-sys", "web-sys"] diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs deleted file mode 100644 index 610e3c3c3..000000000 --- a/packages/interpreter/build.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::process::Command; - -fn main() { - println!("cargo:rerun-if-changed=interpreter.ts"); - - match Command::new("tsc").spawn() { - Ok(_) => println!("Was spawned :)"), - Err(e) => { - if let std::io::ErrorKind::NotFound = e.kind() { - println!("`tsc` was not found! Not going to generate new interpreter") - } else { - println!("Some strange error occurred :("); - } - } - } -} diff --git a/packages/web/build.rs b/packages/interpreter/src/bindings.rs similarity index 77% rename from packages/web/build.rs rename to packages/interpreter/src/bindings.rs index 3c9baa7c8..fa96e3230 100644 --- a/packages/web/build.rs +++ b/packages/interpreter/src/bindings.rs @@ -1,18 +1,9 @@ -use std::{fs::File, io::Write}; - -fn main() { - let src = format!( - r###"use js_sys::Function; +use js_sys::Function; use wasm_bindgen::prelude::*; -use web_sys::{{Element, Node}}; +use web_sys::{Element, Node}; -/* -This is an autogenerated file from build.rs. -Do not edit this file directly. -*/ - -#[wasm_bindgen(inline_js = r##"{}"##)] -extern "C" {{ +#[wasm_bindgen(module = "/src/interpreter.js")] +extern "C" { pub type Interpreter; #[wasm_bindgen(constructor)] @@ -65,12 +56,4 @@ extern "C" {{ #[wasm_bindgen(method)] pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str); -}} -"###, - dioxus_interpreter_js::INTERPRTER_JS - ); - - // write the bindings to a local file - let mut file = File::create("src/bindings.rs").unwrap(); - file.write_all(src.as_bytes()).unwrap(); } diff --git a/packages/interpreter/gen/interpreter.js b/packages/interpreter/src/interpreter.js similarity index 100% rename from packages/interpreter/gen/interpreter.js rename to packages/interpreter/src/interpreter.js diff --git a/packages/interpreter/src/interpreter.ts b/packages/interpreter/src/interpreter.ts deleted file mode 100644 index f150410f4..000000000 --- a/packages/interpreter/src/interpreter.ts +++ /dev/null @@ -1,675 +0,0 @@ -export function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} - -declare global { - interface Window { - interpreter: Interpreter; - rpc: { call: (method: string, args?: any) => void }; - } -} - - -export class Interpreter { - root: Element; - stack: Element[]; - listeners: { [key: string]: number }; - handlers: { [key: string]: (evt: Event) => void }; - lastNodeWasText: boolean; - nodes: Element[]; - - - constructor(root: Element) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - - top() { - return this.stack[this.stack.length - 1]; - } - - pop() { - return this.stack.pop(); - } - - PushRoot(root: number) { - const node = this.nodes[root]; - this.stack.push(node); - } - - AppendChildren(many: number) { - let root = this.stack[this.stack.length - (1 + many)]; - - let to_add = this.stack.splice(this.stack.length - many); - - for (let i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - } - - ReplaceWith(root_id: number, m: number) { - let root = this.nodes[root_id] as Element; - let els = this.stack.splice(this.stack.length - m); - - root.replaceWith(...els); - } - - InsertAfter(root: number, n: number) { - let old = this.nodes[root] as Element; - let new_nodes = this.stack.splice(this.stack.length - n); - old.after(...new_nodes); - } - - InsertBefore(root: number, n: number) { - let old = this.nodes[root] as Element; - let new_nodes = this.stack.splice(this.stack.length - n); - old.before(...new_nodes); - } - - Remove(root: number) { - let node = this.nodes[root] as Element; - if (node !== undefined) { - node.remove(); - } - } - - CreateTextNode(text: string, root: number) { - // todo: make it so the types are okay - const node = document.createTextNode(text) as any as Element; - this.nodes[root] = node; - this.stack.push(node); - } - - CreateElement(tag: string, root: number) { - const el = document.createElement(tag); - // el.setAttribute("data-dioxus-id", `${root}`); - - this.nodes[root] = el; - this.stack.push(el); - } - - CreateElementNs(tag: string, root: number, ns: string) { - let el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - } - - CreatePlaceholder(root: number) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - - NewEventListener(event_name: string, root: number, handler: (evt: Event) => void) { - const element = this.nodes[root]; - element.setAttribute("data-dioxus-id", `${root}`); - - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } else { - this.listeners[event_name]++; - } - } - - RemoveEventListener(root: number, event_name: string) { - const element = this.nodes[root]; - element.removeAttribute(`data-dioxus-id`); - - this.listeners[event_name]--; - - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; - } - } - - - SetText(root: number, text: string) { - this.nodes[root].textContent = text; - } - - SetAttribute(root: number, field: string, value: string, ns: string | undefined) { - const name = field; - const node = this.nodes[root]; - - if (ns == "style") { - - // @ts-ignore - (node as HTMLElement).style[name] = value; - - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value != (node as HTMLInputElement).value) { - (node as HTMLInputElement).value = value; - } - break; - case "checked": - (node as HTMLInputElement).checked = value === "true"; - break; - case "selected": - (node as HTMLOptionElement).selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root: number, name: string) { - - const node = this.nodes[root]; - node.removeAttribute(name); - - if (name === "value") { - (node as HTMLInputElement).value = ""; - } - - if (name === "checked") { - (node as HTMLInputElement).checked = false; - } - - if (name === "selected") { - (node as HTMLOptionElement).selected = false; - } - } - - handleEdits(edits: DomEdit[]) { - this.stack.push(this.root); - - for (let edit of edits) { - this.handleEdit(edit); - } - } - - handleEdit(edit: DomEdit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - - - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event: Event) => { - let target = event.target as Element | null; - - if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); - let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href") - if (href !== "" && href !== null && href !== undefined) { - window.rpc.call("browser_open", { href }); - } - } - } - } - - // walk the tree to find the real element - while (realId == null && target.parentElement != null) { - target = target.parentElement; - realId = target.getAttribute(`data-dioxus-id`); - } - - shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - - let contents = serialize_event(event); - - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - - if (event.type == "submit") { - event.preventDefault(); - } - - - if (realId == null) { - return; - } - - window.rpc.call("user_event", { - event: (edit as NewEventListener).event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - } -} - - - -function serialize_event(event: Event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = (event as CompositionEvent); - return { - data, - }; - } - - case "keydown": - case "keypress": - case "keyup": { - let { - charCode, - key, - altKey, - ctrlKey, - metaKey, - keyCode, - shiftKey, - location, - repeat, - which, - } = (event as KeyboardEvent); - - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - locale: "locale", - }; - } - - case "focus": - case "blur": { - return {}; - } - - case "change": { - let target = event.target as HTMLInputElement; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - - return { - value: value, - }; - } - - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target as HTMLFormElement; - let value = target.value ?? target.textContent; - - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - - return { - value: value, - }; - } - - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - } = event as MouseEvent; - - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; - } - - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - pointerId, - width, - height, - pressure, - tangentialPressure, - tiltX, - tiltY, - twist, - pointerType, - isPrimary, - } = event as PointerEvent; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - - case "select": { - return {}; - } - - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { - altKey, - ctrlKey, - metaKey, - shiftKey, - } = event as TouchEvent; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - - case "scroll": { - return {}; - } - - case "wheel": { - const { - deltaX, - deltaY, - deltaZ, - deltaMode, - } = event as WheelEvent; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - - case "animationstart": - case "animationend": - case "animationiteration": { - const { - animationName, - elapsedTime, - pseudoElement, - } = event as AnimationEvent; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - - case "transitionend": { - const { - propertyName, - elapsedTime, - pseudoElement, - } = event as TransitionEvent; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - - case "toggle": { - return {}; - } - - default: { - return {}; - } - } -} - -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; - - - -type PushRoot = { type: "PushRoot", root: number }; -type AppendChildren = { type: "AppendChildren", many: number }; -type ReplaceWith = { type: "ReplaceWith", root: number, m: number }; -type InsertAfter = { type: "InsertAfter", root: number, n: number }; -type InsertBefore = { type: "InsertBefore", root: number, n: number }; -type Remove = { type: "Remove", root: number }; -type CreateTextNode = { type: "CreateTextNode", text: string, root: number }; -type CreateElement = { type: "CreateElement", tag: string, root: number }; -type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string }; -type CreatePlaceholder = { type: "CreatePlaceholder", root: number }; -type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number }; -type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number }; -type SetText = { type: "SetText", root: number, text: string }; -type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined }; -type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string }; - - -type DomEdit = - PushRoot | - AppendChildren | - ReplaceWith | - InsertAfter | - InsertBefore | - Remove | - CreateTextNode | - CreateElement | - CreateElementNs | - CreatePlaceholder | - NewEventListener | - RemoveEventListener | - SetText | - SetAttribute | - RemoveAttribute; diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index 7d680ae25..ebca3cbf4 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -1 +1,7 @@ -pub static INTERPRTER_JS: &str = include_str!("../gen/interpreter.js"); +pub static INTERPRTER_JS: &str = include_str!("./interpreter.js"); + +#[cfg(feature = "web")] +mod bindings; + +#[cfg(feature = "web")] +pub use bindings::Interpreter; diff --git a/packages/interpreter/tsconfig.json b/packages/interpreter/tsconfig.json deleted file mode 100644 index 1c1ed1ef9..000000000 --- a/packages/interpreter/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ES2015", - "lib": [ - "es2015", - "es5", - "es6", - "dom" - ], - "rootDir": "src", - "strict": true, - "outDir": "gen", - } -} diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 9ac508a8c..9145fed8b 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -13,27 +13,25 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.9" } dioxus-html = { path = "../html", version = "^0.1.6" } -js-sys = "0.3" -wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } +js-sys = "0.3.56" +wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] } lazy_static = "1.4.0" -wasm-bindgen-futures = "0.4.20" +wasm-bindgen-futures = "0.4.29" log = { version = "0.4.14", features = ["release_max_level_off"] } fxhash = "0.2.1" wasm-logger = "0.2.0" console_error_panic_hook = { version = "0.1.7", optional = true } -wasm-bindgen-test = "0.3.21" -once_cell = "1.8" +wasm-bindgen-test = "0.3.29" +once_cell = "1.9.0" async-channel = "1.6.1" -anyhow = "1.0" -gloo-timers = { version = "0.2.1", features = ["futures"] } -futures-util = "0.3.15" +anyhow = "1.0.53" +gloo-timers = { version = "0.2.3", features = ["futures"] } +futures-util = "0.3.19" smallstr = "0.2.0" - -[build-dependencies] -dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } +dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0", features = ["web"] } [dependencies.web-sys] -version = "0.3.51" +version = "0.3.56" features = [ "Comment", "Attr", @@ -78,5 +76,5 @@ panic_hook = ["console_error_panic_hook"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } -wasm-bindgen-test = "0.3.28" +wasm-bindgen-test = "0.3.29" dioxus-ssr = { path = "../ssr" } diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs deleted file mode 100644 index 4834146af..000000000 --- a/packages/web/src/bindings.rs +++ /dev/null @@ -1,561 +0,0 @@ -use js_sys::Function; -use wasm_bindgen::prelude::*; -use web_sys::{Element, Node}; - -/* -This is an autogenerated file from build.rs. -Do not edit this file directly. -*/ - -#[wasm_bindgen(inline_js = r##"export function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} -export class Interpreter { - root; - stack; - listeners; - handlers; - lastNodeWasText; - nodes; - constructor(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - top() { - return this.stack[this.stack.length - 1]; - } - pop() { - return this.stack.pop(); - } - PushRoot(root) { - const node = this.nodes[root]; - this.stack.push(node); - } - AppendChildren(many) { - let root = this.stack[this.stack.length - (1 + many)]; - let to_add = this.stack.splice(this.stack.length - many); - for (let i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - } - ReplaceWith(root_id, m) { - let root = this.nodes[root_id]; - let els = this.stack.splice(this.stack.length - m); - root.replaceWith(...els); - } - InsertAfter(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.after(...new_nodes); - } - InsertBefore(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.before(...new_nodes); - } - Remove(root) { - let node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - } - CreateTextNode(text, root) { - // todo: make it so the types are okay - const node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - } - CreateElement(tag, root) { - const el = document.createElement(tag); - // el.setAttribute("data-dioxus-id", `${root}`); - this.nodes[root] = el; - this.stack.push(el); - } - CreateElementNs(tag, root, ns) { - let el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - } - CreatePlaceholder(root) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - NewEventListener(event_name, root, handler) { - const element = this.nodes[root]; - element.setAttribute("data-dioxus-id", `${root}`); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } - else { - this.listeners[event_name]++; - } - } - RemoveEventListener(root, event_name) { - const element = this.nodes[root]; - element.removeAttribute(`data-dioxus-id`); - this.listeners[event_name]--; - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; - } - } - SetText(root, text) { - this.nodes[root].textContent = text; - } - SetAttribute(root, field, value, ns) { - const name = field; - const node = this.nodes[root]; - if (ns == "style") { - // @ts-ignore - node.style[name] = value; - } - else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } - else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } - else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root, name) { - const node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - } - handleEdits(edits) { - this.stack.push(this.root); - for (let edit of edits) { - this.handleEdit(edit); - } - } - handleEdit(edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event) => { - let target = event.target; - if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); - let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.rpc.call("browser_open", { href }); - } - } - } - } - // walk the tree to find the real element - while (realId == null && target.parentElement != null) { - target = target.parentElement; - realId = target.getAttribute(`data-dioxus-id`); - } - shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - let contents = serialize_event(event); - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - if (event.type == "submit") { - event.preventDefault(); - } - if (realId == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - } -} -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { charCode, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - locale: "locale", - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } - else { - value = target.value ?? target.textContent; - } - return { - value: value, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - return { - value: value, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, pointerId, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, pointerType, isPrimary, } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey, } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode, } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement, } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement, } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; -"##)] -extern "C" { - pub type Interpreter; - - #[wasm_bindgen(constructor)] - pub fn new(arg: Element) -> Interpreter; - - #[wasm_bindgen(method)] - pub fn set_node(this: &Interpreter, id: usize, node: Node); - - #[wasm_bindgen(method)] - pub fn PushRoot(this: &Interpreter, root: u64); - - #[wasm_bindgen(method)] - pub fn AppendChildren(this: &Interpreter, many: u32); - - #[wasm_bindgen(method)] - pub fn ReplaceWith(this: &Interpreter, root: u64, m: u32); - - #[wasm_bindgen(method)] - pub fn InsertAfter(this: &Interpreter, root: u64, n: u32); - - #[wasm_bindgen(method)] - pub fn InsertBefore(this: &Interpreter, root: u64, n: u32); - - #[wasm_bindgen(method)] - pub fn Remove(this: &Interpreter, root: u64); - - #[wasm_bindgen(method)] - pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64); - - #[wasm_bindgen(method)] - pub fn CreateElement(this: &Interpreter, tag: &str, root: u64); - - #[wasm_bindgen(method)] - pub fn CreateElementNs(this: &Interpreter, tag: &str, root: u64, ns: &str); - - #[wasm_bindgen(method)] - pub fn CreatePlaceholder(this: &Interpreter, root: u64); - - #[wasm_bindgen(method)] - pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function); - - #[wasm_bindgen(method)] - pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); - - #[wasm_bindgen(method)] - pub fn SetText(this: &Interpreter, root: u64, text: &str); - - #[wasm_bindgen(method)] - pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>); - - #[wasm_bindgen(method)] - pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str); -} diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 733e50e4b..6ca689d28 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,8 +7,8 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> -use crate::bindings::Interpreter; use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; +use dioxus_interpreter_js::Interpreter; use js_sys::Function; use std::{any::Any, rc::Rc, sync::Arc}; use wasm_bindgen::{closure::Closure, JsCast}; diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index f4266a7c8..cef1bc14c 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -61,7 +61,6 @@ pub use dioxus_core as dioxus; use dioxus_core::prelude::Component; use futures_util::FutureExt; -pub(crate) mod bindings; mod cache; mod cfg; mod dom; From d758dc6065a4563e7c1e94eb19466b5b4987d62e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 02:13:35 -0500 Subject: [PATCH 123/256] feat: form works in web --- examples/form.rs | 7 +++--- packages/web/src/dom.rs | 49 +++++++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/examples/form.rs b/examples/form.rs index 577658aeb..4fa87c885 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -1,6 +1,7 @@ -//! Example: README.md showcase +//! Forms //! -//! The example from the README.md. +//! Dioxus forms deviate slightly from html, automatically returning all named inputs +//! in the "values" field use dioxus::prelude::*; @@ -13,7 +14,7 @@ fn app(cx: Scope) -> Element { div { h1 { "Form" } form { - oninput: move |ev| println!("{:?}", ev), + oninput: move |ev| println!("{:?}", ev.values), input { r#type: "text", name: "username" } input { r#type: "text", name: "full-name" } input { r#type: "password", name: "password" } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 67acf7890..efa21ebbf 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -41,7 +41,7 @@ impl WebsysDom { Some(Ok(id)) => { break Ok(UserEvent { name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), + data: virtual_event_from_websys_event(event.clone(), target.clone()), element: Some(ElementId(id)), scope_id: None, priority: dioxus_core::EventPriority::Medium, @@ -57,7 +57,10 @@ impl WebsysDom { } else { break Ok(UserEvent { name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), + data: virtual_event_from_websys_event( + event.clone(), + target.clone(), + ), element: None, scope_id: None, priority: dioxus_core::EventPriority::Low, @@ -144,7 +147,10 @@ unsafe impl Sync for DioxusWebsysEvent {} // todo: some of these events are being casted to the wrong event type. // We need tests that simulate clicks/etc and make sure every event type works. -fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { +fn virtual_event_from_websys_event( + event: web_sys::Event, + target: Element, +) -> Arc { use dioxus_html::on::*; use dioxus_html::KeyCode; @@ -177,9 +183,6 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { - let evt: &web_sys::Event = event.dyn_ref().unwrap(); - - let target: web_sys::EventTarget = evt.target().unwrap(); let value: String = (&target) .dyn_ref() .map(|input: &web_sys::HtmlInputElement| { @@ -217,28 +220,29 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc() { let elements = form.elements(); for x in 0..elements.length() { let element = elements.item(x).unwrap(); if let Some(name) = element.get_attribute("name") { let value: String = (&element) - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - match input.type_().as_str() { - "checkbox" => { - match input.checked() { - true => "true".to_string(), - false => "false".to_string(), - } - }, - _ => input.value() - } - }) - .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| input.value())) - .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlSelectElement| input.value())) - .or_else(|| target.dyn_ref::().unwrap().text_content()) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => input.value() + } + }) + .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| input.value())) + .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlSelectElement| input.value())) + .or_else(|| target.dyn_ref::().unwrap().text_content()) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); values.insert(name, value); } @@ -337,6 +341,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc Arc::new(MediaData {}), "toggle" => Arc::new(ToggleData {}), + _ => Arc::new(()), } } From b4391a3eaa4292d96072661107fd7328c23293b2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 02:44:27 -0500 Subject: [PATCH 124/256] fix: tweak the js bindings while we're at it --- examples/todomvc.rs | 9 +- packages/interpreter/src/interpreter.js | 1002 ++++++++++++----------- 2 files changed, 531 insertions(+), 480 deletions(-) diff --git a/examples/todomvc.rs b/examples/todomvc.rs index e5f7d1dfa..937d6992c 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -120,15 +120,18 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { rsx!(cx, li { class: "{completed} {editing}", - onclick: move |_| set_is_editing(true), - onfocusout: move |_| set_is_editing(false), div { class: "view", input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{todo.checked}", onchange: move |evt| { cx.props.set_todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap(); } } - label { r#for: "cbg-{todo.id}", pointer_events: "none", "{todo.contents}" } + label { + r#for: "cbg-{todo.id}", + onclick: move |_| set_is_editing(true), + onfocusout: move |_| set_is_editing(false), + "{todo.contents}" + } } is_editing.then(|| rsx!{ input { diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 0d89efa85..dbcb73a96 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -1,497 +1,545 @@ export function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } } export class Interpreter { - root; - stack; - listeners; - handlers; - lastNodeWasText; - nodes; - constructor(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; + root; + stack; + listeners; + handlers; + lastNodeWasText; + nodes; + constructor(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + top() { + return this.stack[this.stack.length - 1]; + } + pop() { + return this.stack.pop(); + } + PushRoot(root) { + const node = this.nodes[root]; + this.stack.push(node); + } + AppendChildren(many) { + let root = this.stack[this.stack.length - (1 + many)]; + let to_add = this.stack.splice(this.stack.length - many); + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); } - top() { - return this.stack[this.stack.length - 1]; + } + ReplaceWith(root_id, m) { + let root = this.nodes[root_id]; + let els = this.stack.splice(this.stack.length - m); + root.replaceWith(...els); + } + InsertAfter(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + InsertBefore(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + Remove(root) { + let node = this.nodes[root]; + if (node !== undefined) { + node.remove(); } - pop() { - return this.stack.pop(); + } + CreateTextNode(text, root) { + // todo: make it so the types are okay + const node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + } + CreateElement(tag, root) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + this.nodes[root] = el; + this.stack.push(el); + } + CreateElementNs(tag, root, ns) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + CreatePlaceholder(root) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + NewEventListener(event_name, root, handler) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } else { + this.listeners[event_name]++; } - PushRoot(root) { - const node = this.nodes[root]; - this.stack.push(node); + } + RemoveEventListener(root, event_name) { + const element = this.nodes[root]; + element.removeAttribute(`data-dioxus-id`); + this.listeners[event_name]--; + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; } - AppendChildren(many) { - let root = this.stack[this.stack.length - (1 + many)]; - let to_add = this.stack.splice(this.stack.length - many); - for (let i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } + } + SetText(root, text) { + this.nodes[root].textContent = text; + } + SetAttribute(root, field, value, ns) { + const name = field; + const node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } } - ReplaceWith(root_id, m) { - let root = this.nodes[root_id]; - let els = this.stack.splice(this.stack.length - m); - root.replaceWith(...els); + } + RemoveAttribute(root, name) { + const node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; } - InsertAfter(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.after(...new_nodes); + if (name === "checked") { + node.checked = false; } - InsertBefore(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.before(...new_nodes); + if (name === "selected") { + node.selected = false; } - Remove(root) { - let node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } + } + handleEdits(edits) { + this.stack.push(this.root); + for (let edit of edits) { + this.handleEdit(edit); } - CreateTextNode(text, root) { - // todo: make it so the types are okay - const node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - } - CreateElement(tag, root) { - const el = document.createElement(tag); - // el.setAttribute("data-dioxus-id", `${root}`); - this.nodes[root] = el; - this.stack.push(el); - } - CreateElementNs(tag, root, ns) { - let el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - } - CreatePlaceholder(root) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - NewEventListener(event_name, root, handler) { - const element = this.nodes[root]; - element.setAttribute("data-dioxus-id", `${root}`); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } - else { - this.listeners[event_name]++; - } - } - RemoveEventListener(root, event_name) { - const element = this.nodes[root]; - element.removeAttribute(`data-dioxus-id`); - this.listeners[event_name]--; - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; - } - } - SetText(root, text) { - this.nodes[root].textContent = text; - } - SetAttribute(root, field, value, ns) { - const name = field; - const node = this.nodes[root]; - if (ns == "style") { - // @ts-ignore - node.style[name] = value; - } - else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } - else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } - else { - node.setAttribute(name, value); - } + } + handleEdit(edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event) => { + let target = event.target; + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + let shouldPreventDefault = target.getAttribute( + `dioxus-prevent-default` + ); + + if (event.type == "click") { + // todo call prevent default if it's the right type of event + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.rpc.call("browser_open", { href }); + } + } + } } - } - } - RemoveAttribute(root, name) { - const node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - } - handleEdits(edits) { - this.stack.push(this.root); - for (let edit of edits) { - this.handleEdit(edit); - } - } - handleEdit(edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event) => { - let target = event.target; - if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); - let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.rpc.call("browser_open", { href }); - } - } - } - } - // walk the tree to find the real element - while (realId == null && target.parentElement != null) { - target = target.parentElement; - realId = target.getAttribute(`data-dioxus-id`); - } - shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - let contents = serialize_event(event); - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - if (event.type == "submit") { - event.preventDefault(); - } - if (realId == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + + shouldPreventDefault = target.getAttribute( + `dioxus-prevent-default` + ); + let contents = serialize_event(event); + if (shouldPreventDefault === `on${event.type}`) { + // event.preventDefault(); + } + if (event.type == "submit") { + // event.preventDefault(); + } + if (realId == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; } + } } function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { charCode, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - locale: "locale", - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } - else { - value = target.value ?? target.textContent; - } - return { - value: value, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - return { - value: value, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, pointerId, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, pointerType, isPrimary, } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey, } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode, } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement, } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement, } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; } + case "compositionend": + case "compositionstart": + case "compositionupdate": { + let { data } = event; + return { + data, + }; + } + case "keydown": + case "keypress": + case "keyup": { + let { + charCode, + key, + altKey, + ctrlKey, + metaKey, + keyCode, + shiftKey, + location, + repeat, + which, + } = event; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location, + repeat: repeat, + which: which, + locale: "locale", + }; + } + case "focus": + case "blur": { + return {}; + } + case "change": { + let target = event.target; + let value; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } else { + value = target.value ?? target.textContent; + } + return { + value: value, + }; + } + case "input": + case "invalid": + case "reset": + case "submit": { + let target = event.target; + let value = target.value ?? target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + const { + altKey, + button, + buttons, + clientX, + clientY, + ctrlKey, + metaKey, + pageX, + pageY, + screenX, + screenY, + shiftKey, + } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + const { + altKey, + button, + buttons, + clientX, + clientY, + ctrlKey, + metaKey, + pageX, + pageY, + screenX, + screenY, + shiftKey, + pointerId, + width, + height, + pressure, + tangentialPressure, + tiltX, + tiltY, + twist, + pointerType, + isPrimary, + } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary, + }; + } + case "select": { + return {}; + } + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + const { altKey, ctrlKey, metaKey, shiftKey } = event; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, + }; + } + case "scroll": { + return {}; + } + case "wheel": { + const { deltaX, deltaY, deltaZ, deltaMode } = event; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + const { animationName, elapsedTime, pseudoElement } = event; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + const { propertyName, elapsedTime, pseudoElement } = event; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": { + return {}; + } + case "toggle": { + return {}; + } + default: { + return {}; + } + } } const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, }; From ca9700a84dc01afcfa9e3e17e94143d1f33a335c Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 4 Feb 2022 08:47:09 +0100 Subject: [PATCH 125/256] fix ci test --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ab05df1c..29319ef30 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,8 +38,8 @@ jobs: with: command: install args: --debug cargo-make - - uses: actions-rs/cargo@v1 - uses: browser-actions/setup-firefox@latest + - uses: actions-rs/cargo@v1 with: command: make args: tests From bd565bb65f7cef7aab263cb28994181a7f069730 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 4 Feb 2022 17:18:31 +0100 Subject: [PATCH 126/256] improve Makefile tests --- .docker/Dockerfile_pre_test | 15 +++++++++++++-- .docker/run_local_tests.sh | 5 ++--- Makefile.toml | 27 ++++++++++++++++++--------- packages/router/Makefile.toml | 4 ++-- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/.docker/Dockerfile_pre_test b/.docker/Dockerfile_pre_test index 6e0af4987..380bb5773 100644 --- a/.docker/Dockerfile_pre_test +++ b/.docker/Dockerfile_pre_test @@ -1,9 +1,20 @@ FROM rust:1.58-buster RUN apt update -RUN apt install -y libglib2.0-dev libgtk-3-dev libsoup2.4-dev libappindicator3-dev libwebkit2gtk-4.0-dev firefox-esr +RUN apt install -y \ + libglib2.0-dev \ + libgtk-3-dev \ + libsoup2.4-dev \ + libappindicator3-dev \ + libwebkit2gtk-4.0-dev \ + firefox-esr +# for kcov and Tarpaulin +#liblzma-dev binutils-dev libcurl4-openssl-dev libdw-dev libelf-dev RUN cargo install cargo-make --debug +# for test coverage +#RUN cargo install cargo-tarpaulin +# clean up a bit RUN cargo install cargo-cache && cargo cache -a -CMD ["echo","\"base image built\""] +CMD ["exit"] diff --git a/.docker/run_local_tests.sh b/.docker/run_local_tests.sh index 804d87afb..4684625d4 100644 --- a/.docker/run_local_tests.sh +++ b/.docker/run_local_tests.sh @@ -25,14 +25,13 @@ function run_script { then docker image rm dioxus-base-test-image docker image rm dioxus-test-image - docker system prune -af fi fi } run_script || echo "Error occured.. cleaning a bit." && \ - docker system prune -af; + docker system prune -f; -docker system prune -af +docker system prune -f echo "Script finished to execute" diff --git a/Makefile.toml b/Makefile.toml index 95abec9de..17c32fd15 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -10,13 +10,6 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true namespace = "core" private = true -[tasks.tests] -category = "Testing" -dependencies = ["tests-setup"] -description = "Run all tests" -env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*", "**/packages/changelog"]} -run_task = {name = ["test-flow"], fork = true} - [tasks.tests-setup] private = true script = [ @@ -31,12 +24,28 @@ script = [ ] script_runner = "@duckscript" +[tasks.tests] +category = "Testing" +dependencies = ["tests-setup"] +description = "Run all tests" +env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"]} +run_task = {name = ["test-flow", "test-with-browser"], fork = true} + +[tasks.build] +command = "cargo" +args = ["build"] + [tasks.test-flow] dependencies = ["test"] private = true -workspace = true [tasks.test] -args = ["test", "--all-targets", "--workspace", "--exclude", "dioxus-mobile"] +dependencies = ["build"] command = "cargo" +args = ["test", "--all-targets", "--workspace", "--exclude", "dioxus-router"] private = true + +[tasks.test-with-browser] +env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router"] } +private = true +workspace = true diff --git a/packages/router/Makefile.toml b/packages/router/Makefile.toml index 063585c98..42b7f5c9a 100644 --- a/packages/router/Makefile.toml +++ b/packages/router/Makefile.toml @@ -1,4 +1,4 @@ -[tasks.test] +[tasks.test-with-browser] extend = "core::wasm-pack-base" command = "wasm-pack" args = [ @@ -7,4 +7,4 @@ args = [ "--", "--features", "${DIOXUS_TEST_FEATURES}", -] +] \ No newline at end of file From c18670c068d9029e123f0daa450f579f0b7e9632 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 4 Feb 2022 17:52:47 +0100 Subject: [PATCH 127/256] adding cargo-make and wasm-pack to CI worklow --- .github/workflows/main.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 29319ef30..244313ef9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,12 +33,9 @@ jobs: - uses: Swatinem/rust-cache@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev - - name: Install cargo-make - uses: actions-rs/cargo@v1 - with: - command: install - args: --debug cargo-make + - uses: davidB/rust-cargo-make@v1 - uses: browser-actions/setup-firefox@latest + - uses: jetli/wasm-pack-action@v0.3.0 - uses: actions-rs/cargo@v1 with: command: make From 841c0c40159972614d37a7bce6efba1e3c22928d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 18:06:36 -0500 Subject: [PATCH 128/256] docs: combine icons --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 52493b119..cc39ddf82 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,7 @@ CI status - -

Awesome Page From 28e9e4373ea923917b7c29d50a5bee0e943bf21b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 18:11:58 -0500 Subject: [PATCH 129/256] fix: tweak js code --- packages/interpreter/src/interpreter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 4b2f3b701..300d12001 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -229,12 +229,14 @@ export class Interpreter { shouldPreventDefault = target.getAttribute( `dioxus-prevent-default` ); + let contents = serialize_event(event); + if (shouldPreventDefault === `on${event.type}`) { - // event.preventDefault(); + event.preventDefault(); } if (event.type == "submit") { - // event.preventDefault(); + event.preventDefault(); } if (target.tagName == "FORM") { From a270ccd81efaf3261c3182a9273f1f0baa2d8aa8 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Sat, 5 Feb 2022 08:50:15 +0800 Subject: [PATCH 130/256] trans `Projects in the ecosystem` --- notes/README/ZH_CN.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 28bd07ed0..9bf0d394c 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -160,6 +160,15 @@ Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但 - 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 - 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。 +### 项目生态 + +想要加入我们一起为 Dioxus 生态努力吗?有很多项目都能在您的帮助下获得改变: + +- [TUI 渲染器](https://github.com/dioxusLabs/rink) +- [CLI 开发工具](https://github.com/dioxusLabs/cli) +- [官网及文档](https://github.com/dioxusLabs/docsite) +- 动态网站 及 Web 服务器 +- 资源系统 ## 协议 From c5fe8afe46d424cfc3846e8c7ff775c6bf312883 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Sat, 5 Feb 2022 08:52:40 +0800 Subject: [PATCH 131/256] link `docs` to chinese version --- notes/README/ZH_CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 9bf0d394c..ef1176586 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -44,7 +44,7 @@

官网 | - 手册 + 手册 | 示例

From 8012fded19214037cd63d1bcb099223891e2c758 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 20:57:32 -0500 Subject: [PATCH 132/256] docs: remove emojis --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc39ddf82..3a44bf3c1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
-

🌗🚀 Dioxus

+

Dioxus

Frontend that scales.

From a939df677af9ffd3a684b1c9754fd629a70007e7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 21:02:53 -0500 Subject: [PATCH 133/256] example: use onsubmit to showcase entry --- examples/form.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/form.rs b/examples/form.rs index 4fa87c885..5f0c992f5 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -14,10 +14,12 @@ fn app(cx: Scope) -> Element { div { h1 { "Form" } form { - oninput: move |ev| println!("{:?}", ev.values), + onsubmit: move |ev| println!("Submitted {:?}", ev.values), + oninput: move |ev| println!("Input {:?}", ev.values), input { r#type: "text", name: "username" } input { r#type: "text", name: "full-name" } input { r#type: "password", name: "password" } + button { "Submit the form" } } } }) From 4cc3369241b7458c02ff3adbb579ebd52f79c7e8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 5 Feb 2022 20:16:52 -0500 Subject: [PATCH 134/256] fix: add a check for dangerousinnerhtml in interpreter --- packages/interpreter/src/interpreter.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 300d12001..546679e97 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -141,15 +141,17 @@ export class Interpreter { } RemoveAttribute(root, name) { const node = this.nodes[root]; - node.removeAttribute(name); + if (name === "value") { node.value = ""; - } - if (name === "checked") { + } else if (name === "checked") { node.checked = false; - } - if (name === "selected") { + } else if (name === "selected") { node.selected = false; + } else if (name == "dangerous_innter_html") { + node.innerHTML = ""; + } else { + node.removeAttribute(name); } } handleEdits(edits) { From 15b074f60be7f11db72024729aa2e9108bc0357c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 5 Feb 2022 20:41:15 -0500 Subject: [PATCH 135/256] fix: type --- packages/interpreter/src/interpreter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 546679e97..01a1d3dd5 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -148,7 +148,7 @@ export class Interpreter { node.checked = false; } else if (name === "selected") { node.selected = false; - } else if (name == "dangerous_innter_html") { + } else if (name == "dangerous_inner_html") { node.innerHTML = ""; } else { node.removeAttribute(name); From 5ba2c43274d8123de9adada0040eacbb134ba834 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sun, 6 Feb 2022 17:51:04 +0100 Subject: [PATCH 136/256] local code coverage --- .docker/Dockerfile_base_test_image | 14 ++++++++++++++ .docker/Dockerfile_code_coverage | 7 +++++++ .docker/Dockerfile_pre_test | 20 ++++---------------- .docker/Dockerfile_test | 3 ++- .docker/run_local_tests.sh | 11 ++++++++++- .gitignore | 1 + 6 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 .docker/Dockerfile_base_test_image create mode 100644 .docker/Dockerfile_code_coverage diff --git a/.docker/Dockerfile_base_test_image b/.docker/Dockerfile_base_test_image new file mode 100644 index 000000000..a29eac927 --- /dev/null +++ b/.docker/Dockerfile_base_test_image @@ -0,0 +1,14 @@ +FROM rust:1.58-buster + +RUN apt update +RUN apt install -y \ + libglib2.0-dev \ + libgtk-3-dev \ + libsoup2.4-dev \ + libappindicator3-dev \ + libwebkit2gtk-4.0-dev \ + firefox-esr \ + # for Tarpaulin code coverage + liblzma-dev binutils-dev libcurl4-openssl-dev libdw-dev libelf-dev + +CMD ["exit"] diff --git a/.docker/Dockerfile_code_coverage b/.docker/Dockerfile_code_coverage new file mode 100644 index 000000000..abcccb56d --- /dev/null +++ b/.docker/Dockerfile_code_coverage @@ -0,0 +1,7 @@ +FROM dioxus-test-image + +WORKDIR /run_test +RUN cargo install cargo-tarpaulin +RUN cargo cache -a + +ENTRYPOINT [ "bash" ] diff --git a/.docker/Dockerfile_pre_test b/.docker/Dockerfile_pre_test index 380bb5773..cc6f8810e 100644 --- a/.docker/Dockerfile_pre_test +++ b/.docker/Dockerfile_pre_test @@ -1,20 +1,8 @@ -FROM rust:1.58-buster +FROM dioxus-base-test-image -RUN apt update -RUN apt install -y \ - libglib2.0-dev \ - libgtk-3-dev \ - libsoup2.4-dev \ - libappindicator3-dev \ - libwebkit2gtk-4.0-dev \ - firefox-esr -# for kcov and Tarpaulin -#liblzma-dev binutils-dev libcurl4-openssl-dev libdw-dev libelf-dev - -RUN cargo install cargo-make --debug -# for test coverage -#RUN cargo install cargo-tarpaulin -# clean up a bit +RUN cargo install cargo-binstall +RUN cargo install cargo-make +RUN cargo install wasm-pack RUN cargo install cargo-cache && cargo cache -a CMD ["exit"] diff --git a/.docker/Dockerfile_test b/.docker/Dockerfile_test index f6265c2bb..4cd644051 100644 --- a/.docker/Dockerfile_test +++ b/.docker/Dockerfile_test @@ -1,8 +1,9 @@ -FROM dioxus-base-test-image +FROM dioxus-pre-test RUN mkdir run_test COPY tmp /run_test WORKDIR /run_test RUN cargo make tests +RUN cargo cache -a CMD ["exit"] diff --git a/.docker/run_local_tests.sh b/.docker/run_local_tests.sh index 4684625d4..ea37b5087 100644 --- a/.docker/run_local_tests.sh +++ b/.docker/run_local_tests.sh @@ -12,9 +12,18 @@ function run_script { rsync -a --progress ../ tmp --exclude target --exclude docker # build base image - docker build -f Dockerfile_pre_test -t dioxus-base-test-image . + docker build -f Dockerfile_base_test_image -t dioxus-base-test-image . + docker build -f Dockerfile_pre_test -t dioxus-pre-test . # run test docker build -f Dockerfile_test -t dioxus-test-image . + # code coverage + docker build -f Dockerfile_code_coverage -t dioxus-code-coverage . + + # exec test coverage + cd .. && \ + echo "rustup default nightly && cargo +nightly tarpaulin --verbose --all-features --tests --workspace --timeout 120 --out Html" | docker run -i --rm --security-opt seccomp=unconfined -v "/home/elios/project/prs/dioxus/:/run_test" dioxus-code-coverage + + firefox tarpaulin-report.html # clean up rm -rf tmp diff --git a/.gitignore b/.gitignore index 9df7e9f93..854f5cb26 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ Cargo.lock !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +tarpaulin-report.html \ No newline at end of file From ed0e03a036c5a1a8f72339b7e32a252cb169b4ba Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sun, 6 Feb 2022 18:14:19 +0100 Subject: [PATCH 137/256] new workflow --- .github/workflows/main.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 244313ef9..3d6687ae3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -76,3 +76,20 @@ jobs: with: command: clippy args: -- -D warnings + + coverage: + name: coverage + runs-on: ubuntu-latest + container: + image: xd009642/tarpaulin:develop-nightly + options: --security-opt seccomp=unconfined + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Generate code coverage + run: | + apt-get update &&\ + apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev &&\ + cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + - name: Upload to codecov.io + uses: codecov/codecov-action@v2 From 0ca1dcc32a99bf4c7b6c7fbb6e9bad9de922eea2 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sun, 6 Feb 2022 18:15:54 +0100 Subject: [PATCH 138/256] fix workflow --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d6687ae3..390cce21e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,7 +89,7 @@ jobs: - name: Generate code coverage run: | apt-get update &&\ - apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev &&\ + apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v2 From e295cc0b72fce451d4d122f0b5451a9255152443 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sun, 6 Feb 2022 19:41:44 +0100 Subject: [PATCH 139/256] add wasm target --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 390cce21e..07664ba35 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -78,7 +78,7 @@ jobs: args: -- -D warnings coverage: - name: coverage + name: Coverage runs-on: ubuntu-latest container: image: xd009642/tarpaulin:develop-nightly @@ -90,6 +90,6 @@ jobs: run: | apt-get update &&\ apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ - cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + cargo +nightly tarpaulin --target wasm32-unknown-unknown --verbose --tests --all-features --workspace --timeout 120 --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v2 From 04bccef04b763b82f7c17e739345d67ae48ce328 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sun, 6 Feb 2022 19:55:15 +0100 Subject: [PATCH 140/256] try fix --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 07664ba35..46137cb75 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -86,6 +86,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 + - uses: jetli/wasm-pack-action@v0.3.0 - name: Generate code coverage run: | apt-get update &&\ From d8765464852413fef95c18e17733511475345743 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sun, 6 Feb 2022 23:08:26 +0100 Subject: [PATCH 141/256] revert worklfow --- .docker/run_local_tests.sh | 4 ++-- .github/workflows/main.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.docker/run_local_tests.sh b/.docker/run_local_tests.sh index ea37b5087..396b7740b 100644 --- a/.docker/run_local_tests.sh +++ b/.docker/run_local_tests.sh @@ -21,9 +21,9 @@ function run_script { # exec test coverage cd .. && \ - echo "rustup default nightly && cargo +nightly tarpaulin --verbose --all-features --tests --workspace --timeout 120 --out Html" | docker run -i --rm --security-opt seccomp=unconfined -v "/home/elios/project/prs/dioxus/:/run_test" dioxus-code-coverage + echo "rustup default nightly && cargo +nightly tarpaulin --verbose --all-features --tests --workspace --exclude core-macro --timeout 120 --out Html" | docker run -i --rm --security-opt seccomp=unconfined -v "/home/elios/project/prs/dioxus/:/run_test" dioxus-code-coverage - firefox tarpaulin-report.html + #firefox tarpaulin-report.html # clean up rm -rf tmp diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 46137cb75..83c79d61d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -91,6 +91,6 @@ jobs: run: | apt-get update &&\ apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ - cargo +nightly tarpaulin --target wasm32-unknown-unknown --verbose --tests --all-features --workspace --timeout 120 --out Xml + cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v2 From fc0a093466ae3cdfd36f0e00f89c602010d9116e Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sun, 6 Feb 2022 23:27:24 +0100 Subject: [PATCH 142/256] try fix --- .docker/run_local_tests.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/.docker/run_local_tests.sh b/.docker/run_local_tests.sh index 396b7740b..b6da68541 100644 --- a/.docker/run_local_tests.sh +++ b/.docker/run_local_tests.sh @@ -23,8 +23,6 @@ function run_script { cd .. && \ echo "rustup default nightly && cargo +nightly tarpaulin --verbose --all-features --tests --workspace --exclude core-macro --timeout 120 --out Html" | docker run -i --rm --security-opt seccomp=unconfined -v "/home/elios/project/prs/dioxus/:/run_test" dioxus-code-coverage - #firefox tarpaulin-report.html - # clean up rm -rf tmp if [ $# -ge 1 ] From ad2e39fb99e90af05b6230126f9aa662362b7c0e Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sun, 6 Feb 2022 23:35:47 +0100 Subject: [PATCH 143/256] try fix --- .github/workflows/main.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 83c79d61d..3d6687ae3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -78,7 +78,7 @@ jobs: args: -- -D warnings coverage: - name: Coverage + name: coverage runs-on: ubuntu-latest container: image: xd009642/tarpaulin:develop-nightly @@ -86,11 +86,10 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - - uses: jetli/wasm-pack-action@v0.3.0 - name: Generate code coverage run: | apt-get update &&\ - apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ + apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev &&\ cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v2 From 9e4dce282d6698fb75e84a7f28cce402090b135f Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sun, 6 Feb 2022 23:43:33 +0100 Subject: [PATCH 144/256] try fix --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d6687ae3..6b6f9be21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -78,7 +78,7 @@ jobs: args: -- -D warnings coverage: - name: coverage + name: Coverage runs-on: ubuntu-latest container: image: xd009642/tarpaulin:develop-nightly @@ -89,7 +89,7 @@ jobs: - name: Generate code coverage run: | apt-get update &&\ - apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev &&\ + apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v2 From 4f46089fce018d6c594338d7fb2a6719744ec1a1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 7 Feb 2022 10:17:16 -0500 Subject: [PATCH 145/256] feat: use serde-wasm-bindgen crate for speedup --- packages/interpreter/src/bindings.rs | 12 +++++++++--- packages/web/Cargo.toml | 1 + packages/web/src/dom.rs | 24 +++++++++++++++++------- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index fa96e3230..e5cd4480c 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -31,7 +31,7 @@ extern "C" { pub fn Remove(this: &Interpreter, root: u64); #[wasm_bindgen(method)] - pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64); + pub fn CreateTextNode(this: &Interpreter, text: JsValue, root: u64); #[wasm_bindgen(method)] pub fn CreateElement(this: &Interpreter, tag: &str, root: u64); @@ -49,10 +49,16 @@ extern "C" { pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); #[wasm_bindgen(method)] - pub fn SetText(this: &Interpreter, root: u64, text: &str); + pub fn SetText(this: &Interpreter, root: u64, text: JsValue); #[wasm_bindgen(method)] - pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>); + pub fn SetAttribute( + this: &Interpreter, + root: u64, + field: &str, + value: JsValue, + ns: Option<&str>, + ); #[wasm_bindgen(method)] pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str); diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 57fc797df..7a3baa955 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -29,6 +29,7 @@ gloo-timers = { version = "0.2.3", features = ["futures"] } futures-util = "0.3.19" smallstr = "0.2.0" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0", features = ["web"] } +serde-wasm-bindgen = "0.4.2" [dependencies.web-sys] version = "0.3.56" diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 555e04e43..c722ab737 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -105,9 +105,7 @@ impl WebsysDom { DomEdit::InsertAfter { root, n } => self.interpreter.InsertAfter(root, n), DomEdit::InsertBefore { root, n } => self.interpreter.InsertBefore(root, n), DomEdit::Remove { root } => self.interpreter.Remove(root), - DomEdit::CreateTextNode { text, root } => { - self.interpreter.CreateTextNode(text, root) - } + DomEdit::CreateElement { tag, root } => self.interpreter.CreateElement(tag, root), DomEdit::CreateElementNs { tag, root, ns } => { self.interpreter.CreateElementNs(tag, root, ns) @@ -123,15 +121,27 @@ impl WebsysDom { DomEdit::RemoveEventListener { root, event } => { self.interpreter.RemoveEventListener(root, event) } - DomEdit::SetText { root, text } => self.interpreter.SetText(root, text), + + DomEdit::RemoveAttribute { root, name } => { + self.interpreter.RemoveAttribute(root, name) + } + + DomEdit::CreateTextNode { text, root } => { + let text = serde_wasm_bindgen::to_value(text).unwrap(); + self.interpreter.CreateTextNode(text, root) + } + DomEdit::SetText { root, text } => { + let text = serde_wasm_bindgen::to_value(text).unwrap(); + self.interpreter.SetText(root, text) + } DomEdit::SetAttribute { root, field, value, ns, - } => self.interpreter.SetAttribute(root, field, value, ns), - DomEdit::RemoveAttribute { root, name } => { - self.interpreter.RemoveAttribute(root, name) + } => { + let value = serde_wasm_bindgen::to_value(value).unwrap(); + self.interpreter.SetAttribute(root, field, value, ns) } } } From abbd0b8a39727c05300bc52e9a7d83522b0b797f Mon Sep 17 00:00:00 2001 From: j1ngzoue Date: Tue, 8 Feb 2022 10:25:25 +0900 Subject: [PATCH 146/256] fix: update the sample --- docs/guide/src/README.md | 8 ++++---- notes/README/ZH_CN.md | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index c00dcd274..32943cd7b 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -5,13 +5,13 @@ **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: Scope) -> Element { - let mut count = use_state(&cx, || 0); +fn app(cx: Scope) -> Element { + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } )) }; ``` diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index ef1176586..79f2b51aa 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -65,12 +65,12 @@ Dioxus 是一个可移植、高性能的框架,用于在 Rust 中构建跨平 ```rust fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } )) } ``` From 95a6abbfc5d8b5485b896e67043f384949afed1e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 8 Feb 2022 00:35:06 -0500 Subject: [PATCH 147/256] fix: class attributes don't adhere to es6 spec --- packages/interpreter/src/interpreter.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 01a1d3dd5..17dc879e2 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -6,12 +6,6 @@ export function main() { } } export class Interpreter { - root; - stack; - listeners; - handlers; - lastNodeWasText; - nodes; constructor(root) { this.root = root; this.stack = [root]; From fc5dd8f56283f3c8c3b75f7d8655456defd76a55 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 8 Feb 2022 00:40:48 -0500 Subject: [PATCH 148/256] fix: always prevent default on a tags --- packages/interpreter/src/interpreter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 01a1d3dd5..4720382c7 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -210,6 +210,7 @@ export class Interpreter { // todo call prevent default if it's the right type of event if (shouldPreventDefault !== `onclick`) { if (target.tagName == "A") { + event.preventDefault(); const href = target.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { window.rpc.call("browser_open", { href }); From 4818c44c7c4d960c89e44fd2e2bc1cbccbfd0ab2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 8 Feb 2022 00:43:05 -0500 Subject: [PATCH 149/256] fix: also prevent default on buttons --- packages/interpreter/src/interpreter.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 4720382c7..a9f00b2a1 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -217,6 +217,11 @@ export class Interpreter { } } } + + // also prevent buttons from submitting + if (target.tagName == "BUTTON") { + event.preventDefault(); + } } // walk the tree to find the real element while (realId == null) { From 6727de447b638b21b231be8f9b7b083f82953768 Mon Sep 17 00:00:00 2001 From: Reinis Mazeiks Date: Wed, 9 Feb 2022 19:35:56 +0200 Subject: [PATCH 150/256] Proofread & rephrase chapters 1-5 for readability --- docs/guide/src/README.md | 2 +- .../src/elements/conditional_rendering.md | 64 ++++++------- docs/guide/src/elements/index.md | 4 +- docs/guide/src/elements/lists.md | 94 +++++-------------- docs/guide/src/elements/special_attributes.md | 26 ++--- docs/guide/src/elements/vnodes.md | 14 +-- 6 files changed, 76 insertions(+), 128 deletions(-) diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index 32943cd7b..bbbd9f85f 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -22,7 +22,7 @@ In general, Dioxus and React share many functional similarities. If this guide i ## Multiplatform -Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to Web-Sys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `html` feature enabled, but this can be disabled depending on your target renderer. +Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to WebSys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `html` feature enabled, but this can be disabled depending on your target renderer. Right now, we have several 1st-party renderers: - WebSys (for WASM) diff --git a/docs/guide/src/elements/conditional_rendering.md b/docs/guide/src/elements/conditional_rendering.md index c13263cf6..5c19de237 100644 --- a/docs/guide/src/elements/conditional_rendering.md +++ b/docs/guide/src/elements/conditional_rendering.md @@ -67,7 +67,7 @@ fn App(cx: Scope)-> Element { } ``` -Do note: the `rsx!` macro returns a `Closure`, an anonymous function that has a unique type. To turn our `rsx!` into Elements, we need to call `cx.render`. +Do note: the `rsx!` macro does not return an Element, but rather a wrapper struct for a `Closure` (an anonymous function). To turn our `rsx!` into an Element, we need to call `cx.render`. 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: @@ -136,34 +136,6 @@ cx.render(rsx!{ }) ``` - -## Boolean Mapping - -In the spirit of highly-functional apps, we suggest using the "boolean mapping" pattern when trying to conditionally hide/show an Element. - -By default, Rust lets you convert any `boolean` into any other type by calling `and_then()`. We can exploit this functionality in components by mapping to some Element. - -```rust -let show_title = true; -rsx!( - div { - show_title.and_then(|| rsx!{ - "This is the title" - }) - } -) -``` - -We can use this pattern for many things, including options: -```rust -let user_name = Some("bob"); -rsx!( - div { - user_name.map(|name| rsx!("Hello {name}")) - } -) -``` - ## Rendering Nothing Sometimes, you don't want your component to return anything at all. Under the hood, the `Element` type is just an alias for `Option`, so you can simply return `None`. @@ -176,11 +148,37 @@ fn demo(cx: Scope) -> Element { } ``` +## Boolean Mapping + +In the spirit of highly-functional apps, we suggest using the "boolean mapping" pattern when trying to conditionally hide/show an Element. + +By default, Rust lets you convert any `boolean` into an Option of any other type with [`then()`](https://doc.rust-lang.org/std/primitive.bool.html#method.then). We can use this in Components by mapping to some Element. + +```rust +let show_title = true; +rsx!( + div { + // Renders nothing by returning None when show_title is false + show_title.then(|| rsx!{ + "This is the title" + }) + } +) +``` + +We can use this pattern for many things, including options: +```rust +let user_name = Some("bob"); +rsx!( + div { + // Renders nothing if user_name is None + user_name.map(|name| rsx!("Hello {name}")) + } +) +``` + ## Moving Forward: -In this chapter, we learned how to render different Elements from a Component depending on a condition. This is a very powerful building block to assemble complex User Interfaces! +In this chapter, we learned how to render different Elements from a Component depending on a condition. This is a very powerful building block to assemble complex user interfaces! In the next chapter, we'll cover how to renderer lists inside your `rsx!`. - -Related Reading: -- [RSX in Depth]() diff --git a/docs/guide/src/elements/index.md b/docs/guide/src/elements/index.md index aedc39e34..c814945d1 100644 --- a/docs/guide/src/elements/index.md +++ b/docs/guide/src/elements/index.md @@ -68,6 +68,6 @@ Remember: this concept is not new! Many frameworks are declarative - with React Here's some reading about declaring UI in React: -- [https://stackoverflow.com/questions/33655534/difference-between-declarative-and-imperative-in-react-js](https://stackoverflow.com/questions/33655534/difference-between-declarative-and-imperative-in-react-js) +- [Difference between declarative and imperative in React.js](https://stackoverflow.com/questions/33655534/difference-between-declarative-and-imperative-in-react-js), a StackOverflow thread -- [https://medium.com/@myung.kim287/declarative-vs-imperative-251ce99c6c44](https://medium.com/@myung.kim287/declarative-vs-imperative-251ce99c6c44) +- [Declarative vs Imperative](https://medium.com/@myung.kim287/declarative-vs-imperative-251ce99c6c44), a blog post by Myung Kim diff --git a/docs/guide/src/elements/lists.md b/docs/guide/src/elements/lists.md index 41c6a892d..40628c164 100644 --- a/docs/guide/src/elements/lists.md +++ b/docs/guide/src/elements/lists.md @@ -42,7 +42,7 @@ As a simple example, let's render a list of names. First, start with our input d let names = ["jim", "bob", "jane", "doe"]; ``` -Then, we create a new iterator by calling `iter` and then `map`. In our `map` function, we'll place render our template. +Then, we create a new iterator by calling `iter` and then [`map`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map). In our `map` function, we'll render our template. ```rust let name_list = names.iter().map(|name| rsx!( @@ -50,7 +50,7 @@ let name_list = names.iter().map(|name| rsx!( )); ``` -Finally, we can include this list in the final structure: +We can include this list in the final Element: ```rust rsx!( @@ -59,7 +59,8 @@ rsx!( } ) ``` -Or, we can include the iterator inline: + +Rather than storing `name_list` in a temporary variable, we could also include the iterator inline: ```rust rsx!( ul { @@ -70,7 +71,7 @@ rsx!( ) ``` -The HTML-rendered version of this list would follow what you would expect: +The rendered HTML list is what you would expect: ```html
  • jim
  • @@ -80,54 +81,13 @@ The HTML-rendered version of this list would follow what you would expect:
``` -### Rendering our posts with a PostList component - -Let's start by modeling this problem with a component and some properties. - -For this example, we're going to use the borrowed component syntax since we probably have a large list of posts that we don't want to clone every time we render the Post List. - -```rust -#[derive(Props, PartialEq)] -struct PostListProps<'a> { - posts: &'a [PostData] -} -``` -Next, we're going to define our component: - -```rust -fn App(cx: Scope) -> Element { - // First, we create a new iterator by mapping the post array - let posts = cx.props.posts.iter().map(|post| rsx!{ - Post { - title: post.title, - age: post.age, - original_poster: post.original_poster - } - }); - - // Finally, we render the post list inside of a container - cx.render(rsx!{ - ul { class: "post-list" - {posts} - } - }) -} -``` - - ## Filtering Iterators Rust's iterators are extremely powerful, especially when used for filtering tasks. When building user interfaces, you might want to display a list of items filtered by some arbitrary check. -As a very simple example, let's set up a filter where we only list names that begin with the letter "J". +As a very simple example, let's set up a filter where we only list names that begin with the letter "j". -Let's make our list of names: - -```rust -let names = ["jim", "bob", "jane", "doe"]; -``` - -Then, we create a new iterator by calling `iter`, then `filter`, then `map`. In our `filter` function, we'll only allow "j" names, and in our `map` function, we'll render our template. +Using the list from above, let's create a new iterator. Before we render the list with `map` as in the previous example, we'll [`filter`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter) the names to only allow those that start with "j". ```rust let name_list = names @@ -136,53 +96,43 @@ let name_list = names .map(|name| rsx!( li { "{name}" })); ``` -Rust's iterators provide us tons of functionality and are significantly easier to work with than JavaScript's map/filter/reduce. +Rust's Iterators are very versatile – check out [their documentation](https://doc.rust-lang.org/std/iter/trait.Iterator.html) for more things you can do with them! -For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collected` our filtered list into new Vec, then we would need to make an allocation to store these new elements. Instead, we create an entirely new _lazy_ iterator which will then be consumed by Dioxus in the `render` call. +For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collect`ed our filtered list into new Vec, we would need to make an allocation to store these new elements, which slows down rendering. Instead, we create an entirely new _lazy_ iterator which Dioxus will consume in the `render` call. The `render` method is extraordinarily efficient, so it's best practice to let it do most of the allocations for us. -The `render` method is extraordinarily efficient, so it's best practice to let it do most of the allocations for us. +## Keeping list items in order with `key` -## Keeping list items in order with key +The examples above demonstrate the power of iterators in `rsx!` but all share the same issue: if your array items move (e.g. due to sorting), get inserted, or get deleted, Dioxus has no way of knowing what happened. This can cause Elements to be unnecessarily removed, changed and rebuilt when all that was needed was to change their position – this is inneficient. - -The examples above demonstrate the power of iterators in `rsx!` but all share the same issue: a lack of "keys". Whenever you render a list of elements, each item in the list must be **uniquely identifiable**. To make each item unique, you need to give it a "key". - -In Dioxus, keys are strings that uniquely identifies it among other items in that array: +To solve this problem, each item in the list must be **uniquely identifiable**. You can achieve this by giving it a unique, fixed "key". In Dioxus, a key is a string that identifies an item among others in the list. ```rust rsx!( li { key: "a" } ) ``` -Keys tell Dioxus which array item each component corresponds to, so that it can match them up later. This becomes important if your array items can move (e.g. due to sorting), get inserted, or get deleted. A well-chosen key helps Dioxus infer what exactly has happened, and make the correct updates to the screen - +Now, if an item has already been rendered once, Dioxus can use the key to match it up later to make the correct updates – and avoid unnecessary work. NB: the language from this section is strongly borrowed from [React's guide on keys](https://reactjs.org/docs/lists-and-keys.html). + ### Where to get your key Different sources of data provide different sources of keys: - _Data from a database_: If your data is coming from a database, you can use the database keys/IDs, which are unique by nature. -- _Locally generated data_: If your data is generated and persisted locally (e.g. notes in a note-taking app), use an incrementing counter or a package like `uuid` when creating items. +- _Locally generated data_: If your data is generated and persisted locally (e.g. notes in a note-taking app), keep track of keys along with your data. You can use an incrementing counter or a package like `uuid` to generate keys for new items – but make sure they stay the same for the item's lifetime. + +Remember: keys let Dioxus uniquely identify an item among its siblings. A well-chosen key provides more information than the position within the array. Even if the position changes due to reordering, the key lets Dioxus identify the item throughout its lifetime. ### Rules of keys - Keys must be unique among siblings. However, it’s okay to use the same keys for Elements in different arrays. -- Keys must not change or that defeats their purpose! Don’t generate them while rendering. +- An item's key must not change – **don’t generate them on the fly** while rendering. Otherwise, Dioxus will be unable to keep track of which item is which, and we're back to square one. -### Why does Dioxus need keys? +You might be tempted to use an item's index in the array as its key. In fact, that’s what Dioxus will use if you don’t specify a key at all. This is only acceptable if you can guarantee that the list is constant – i.e., no re-ordering, additions or deletions. In all other cases, do not use the index for the key – it will lead to the performance problems described above. -Imagine that files on your desktop didn’t have names. Instead, you’d refer to them by their order — the first file, the second file, and so on. You could get used to it, but once you delete a file, it would get confusing. The second file would become the first file, the third file would be the second file, and so on. - -File names in a folder and Element keys in an array serve a similar purpose. They let us uniquely identify an item between its siblings. A well-chosen key provides more information than the position within the array. Even if the position changes due to reordering, the key lets Dioxus identify the item throughout its lifetime. - -### Gotcha -You might be tempted to use an item’s index in the array as its key. In fact, that’s what Dioxus will use if you don’t specify a key at all. But the order in which you render items will change over time if an item is inserted, deleted, or if the array gets reordered. Index as a key often leads to subtle and confusing bugs. - -Similarly, do not generate keys on the fly, like `gen_random`. This will cause keys to never match up between renders, leading to all your components and DOM being recreated every time. Not only is this slow, but it will also lose any user input inside the list items. Instead, use a stable ID based on the data. - -Note that your components won’t receive key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop: +Note that if you pass the key to a [custom component](./components.md) you've made, it won't receive the key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop: ```rust -Post { key: "{key}", id: "{id}" } +Post { key: "{key}", id: "{key}" } ``` ## Moving on @@ -192,4 +142,4 @@ In this section, we learned: - How to use iterator tools to filter and transform data - How to use keys to render lists efficiently -Moving forward, we'll finally cover user input and interactivity. +Moving forward, we'll learn more about attributes. diff --git a/docs/guide/src/elements/special_attributes.md b/docs/guide/src/elements/special_attributes.md index 460d1cd69..a09ce0bf7 100644 --- a/docs/guide/src/elements/special_attributes.md +++ b/docs/guide/src/elements/special_attributes.md @@ -15,7 +15,7 @@ In this section, we'll cover special attributes built into Dioxus: One thing you might've missed from React is the ability to render raw HTML directly to the DOM. If you're working with pre-rendered assets, output from templates, or output from a JS library, then you might want to pass HTML directly instead of going through Dioxus. In these instances, reach for `dangerous_inner_html`. -For example, shipping a markdown-to-Dioxus converter might significantly bloat your final application size. Instead, you'll want to pre-render your markdown to HTML and then include the HTML directly in your output. We use this approach for the `http://dioxuslabs.com` site: +For example, shipping a markdown-to-Dioxus converter might significantly bloat your final application size. Instead, you'll want to pre-render your markdown to HTML and then include the HTML directly in your output. We use this approach for the [Dioxus homepage](https://dioxuslabs.com): ```rust @@ -30,14 +30,16 @@ fn BlogPost(cx: Scope) -> Element { } ``` -> Note! This attribute is called "dangerous_inner_html" because it is DANGEROUS. If you're not careful, you can easily expose cross-site-scripting (XSS) attacks to your users. If you're handling untrusted input, make sure to escape your HTML before passing it into `dangerous_inner_html`. +> Note! This attribute is called "dangerous_inner_html" because it is **dangerous** to pass it data you don't trust. If you're not careful, you can easily expose cross-site-scripting (XSS) attacks to your users. +> +> If you're handling untrusted input, make sure to sanitize your HTML before passing it into `dangerous_inner_html` – or just pass it to a Text Element to escape any HTML tags. ## Boolean Attributes Most attributes, when rendered, will be rendered exactly as the input you provided. However, some attributes are considered "boolean" attributes and just their presence determines whether or not they affect the output. For these attributes, a provided value of `"false"` will cause them to be removed from the target element. -So the input of: +So this RSX: ```rust rsx!{ @@ -47,14 +49,12 @@ rsx!{ } } ``` -would actually render an output of +wouldn't actually render the `hidden` attribute: ```html
hello
``` -Notice how `hidden` is not present in the final output? - -Not all attributes work like this however. Only *these specific attributes* are whitelisted to have this behavior: +Not all attributes work like this however. *Only the following attributes* have this behavior: - `allowfullscreen` - `allowpaymentrequest` @@ -131,7 +131,7 @@ pub fn StateInput<'a>(cx: Scope<'a, InputProps<'a>>) -> Element { ## Controlled inputs and `value`, `checked`, and `selected` -In Dioxus, there is a distinction between controlled and uncontrolled inputs. Most inputs you'll use are "controlled," meaning we both drive the `value` of the input and react to the `oninput`. +In Dioxus, there is a distinction between controlled and uncontrolled inputs. Most inputs you'll use are controlled, meaning we both drive the `value` of the input and react to the `oninput`. Controlled components: ```rust @@ -153,7 +153,7 @@ let value = use_ref(&cx, || String::from("hello world")); rsx! { input { oninput: move |evt| *value.write_silent() = evt.value.clone(), - // no "value" is driven + // no "value" is driven here – the input keeps track of its own value, and you can't change it } } ``` @@ -162,7 +162,7 @@ rsx! { For element fields that take a handler like `onclick` or `oninput`, Dioxus will let you attach a closure. Alternatively, you can also pass a string using normal attribute syntax and assign this attribute on the DOM. -This lets you escape into JavaScript (only if your renderer can execute JavaScript). +This lets you use JavaScript (only if your renderer can execute JavaScript). ```rust rsx!{ @@ -179,14 +179,14 @@ rsx!{ ## Wrapping up -We've reached just about the end of what you can do with elements without venturing into "advanced" territory. - In this chapter, we learned: - How to declare elements - How to conditionally render parts of your UI - How to render lists - Which attributes are "special" + diff --git a/docs/guide/src/elements/vnodes.md b/docs/guide/src/elements/vnodes.md index 3c136cfc2..4adbf962d 100644 --- a/docs/guide/src/elements/vnodes.md +++ b/docs/guide/src/elements/vnodes.md @@ -56,7 +56,7 @@ With the default configuration, any Element defined within the `dioxus-html` cra ## Text Elements -Dioxus also supports a special type of Element: Text. Text Elements do not accept children, but rather just string literals denoted with double quotes. +Dioxus also supports a special type of Element: Text. Text Elements do not accept children, just a string literal denoted by double quotes. ```rust rsx! ( @@ -74,20 +74,20 @@ rsx! ( ) ``` -Text can also be formatted with any value that implements `Display`. We use [f-string formatting](https://docs.rs/fstrings/0.2.3/fstrings/) - a "coming soon" feature for stable Rust that is familiar for Python and JavaScript users: +Text can also be formatted with any value that implements `Display`. We use the same syntax as Rust [format strings](https://www.rustnote.com/blog/format_strings.html) – which will already be familiar for Python and JavaScript users: ```rust let name = "Bob"; rsx! ( "hello {name}" ) ``` -Unfortunately, you cannot drop in arbitrary expressions directly into the string literal. In the cases where we need to compute a complex value, we'll want to use `format_args!` directly. Due to specifics of how the `rsx!` macro (we'll cover later), our call to `format_args` must be contained within square braces. +Unfortunately, you cannot drop in arbitrary expressions directly into the string literal. In the cases where we need to compute a complex value, we'll want to use `format_args!` directly. Due to specifics of the `rsx!` macro (which we'll cover later), our call to `format_args` must be contained within square braces. ```rust -rsx!( {format_args!("Hello {}", if enabled { "Jack" } else { "Bob" } )] ) +rsx!( [format_args!("Hello {}", if enabled { "Jack" } else { "Bob" } )] ) ``` -Alternatively, `&str` can be included directly, though it must be inside of square braces: +Alternatively, `&str` can be included directly, though it must also be inside square braces: ```rust rsx!( "Hello ", [if enabled { "Jack" } else { "Bob" }] ) @@ -104,7 +104,7 @@ rsx! ( "hello {name}" ) ## Attributes -Every Element in your User Interface will have some sort of properties that the renderer will use when drawing to the screen. These might inform the renderer if the component should be hidden, what its background color should be, or to give it a specific name or ID. +Every Element in your user interface will have some sort of properties that the renderer will use when drawing to the screen. These might inform the renderer if the component should be hidden, what its background color should be, or to give it a specific name or ID. To do this, we use the familiar struct-style syntax that Rust provides: @@ -123,7 +123,7 @@ Each field is defined as a method on the element in the `dioxus-html` crate. Thi 1) file an issue if the attribute _should_ be enabled 2) add a custom attribute on-the-fly -To use custom attributes, simply put the attribute name in quotes followed by a colon: +To use custom attributes, simply put the attribute name in quotes: ```rust rsx!( From 6eaad850eee7074326c37e93c2e2ecc5fe5b20a7 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Thu, 10 Feb 2022 12:35:17 +0800 Subject: [PATCH 151/256] feat: add window title api --- packages/desktop/src/desktop_context.rs | 7 +++++++ packages/desktop/src/lib.rs | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 9c0404477..d2c25a3e7 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -57,6 +57,13 @@ impl DesktopContext { pub fn focus(&self) { let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } + + /// set window title + pub fn title(&self, title: &str) { + let _ = self + .proxy + .send_event(UserWindowEvent::Title(String::from(title))); + } } /// use this function can get the `DesktopContext` context. diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 9181d04dc..6bd95b6ff 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -321,6 +321,12 @@ pub fn launch_with_props( window.set_focus(); } } + UserWindowEvent::Title(content) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_title(&content); + } + } } } Event::MainEventsCleared => {} @@ -338,6 +344,8 @@ pub enum UserWindowEvent { DragWindow, CloseWindow, FocusWindow, + + Title(String), Minimize(bool), Maximize(bool), } From f28fb7165a0158b8ce93aa768c350f5ea2b6aff6 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Thu, 10 Feb 2022 13:47:45 +0800 Subject: [PATCH 152/256] feat: add window api --- packages/desktop/src/desktop_context.rs | 14 ++++++++++++-- packages/desktop/src/lib.rs | 21 ++++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index d2c25a3e7..294cfa23c 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -58,11 +58,21 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } + /// set resizable state + pub fn resizable(&self, resizable: bool) { + let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); + } + /// set window title - pub fn title(&self, title: &str) { + pub fn set_title(&self, title: &str) { let _ = self .proxy - .send_event(UserWindowEvent::Title(String::from(title))); + .send_event(UserWindowEvent::SetTitle(String::from(title))); + } + + /// hide the menu + pub fn hide_menu(&self) { + let _ = self.proxy.send_event(UserWindowEvent::HideMenu); } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 6bd95b6ff..452bbba96 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -321,12 +321,25 @@ pub fn launch_with_props( window.set_focus(); } } - UserWindowEvent::Title(content) => { + + UserWindowEvent::SetTitle(content) => { for webview in desktop.webviews.values() { let window = webview.window(); window.set_title(&content); } } + UserWindowEvent::Resizable(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_resizable(state); + } + } + UserWindowEvent::HideMenu => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.hide_menu(); + } + } } } Event::MainEventsCleared => {} @@ -344,10 +357,12 @@ pub enum UserWindowEvent { DragWindow, CloseWindow, FocusWindow, - - Title(String), Minimize(bool), Maximize(bool), + Resizable(bool), + + SetTitle(String), + HideMenu, } pub struct DesktopController { From 02d8e66f40e628b7fc1ae616bd36fb6dddbc1c1d Mon Sep 17 00:00:00 2001 From: Reinis Mazeiks Date: Thu, 10 Feb 2022 10:07:33 +0200 Subject: [PATCH 153/256] Refactor: move Component docs into the `components` directory to reflect how they are presented in the guide --- docs/guide/src/SUMMARY.md | 10 +++++----- .../src/{elements => components}/component_children.md | 0 docs/guide/src/{elements => components}/composing.md | 0 .../{elements => components}/exporting_components.md | 0 .../{elements/components.md => components/index.md} | 0 docs/guide/src/{elements => components}/propsmacro.md | 0 docs/guide/src/elements/lists.md | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename docs/guide/src/{elements => components}/component_children.md (100%) rename docs/guide/src/{elements => components}/composing.md (100%) rename docs/guide/src/{elements => components}/exporting_components.md (100%) rename docs/guide/src/{elements/components.md => components/index.md} (100%) rename docs/guide/src/{elements => components}/propsmacro.md (100%) diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 5023eab8e..27d7e227f 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -9,11 +9,11 @@ - [Conditional Rendering](elements/conditional_rendering.md) - [Lists](elements/lists.md) - [Special Attributes](elements/special_attributes.md) -- [Components](elements/components.md) - - [Properties](elements/propsmacro.md) - - [Reusing, Importing, and Exporting](elements/exporting_components.md) - - [Children and Attributes](elements/component_children.md) - - [How Data Flows](elements/composing.md) +- [Components](components/index.md) + - [Properties](components/propsmacro.md) + - [Reusing, Importing, and Exporting](components/exporting_components.md) + - [Children and Attributes](components/component_children.md) + - [How Data Flows](components/composing.md) - [Thinking Reactively]() - [Thinking Reactively]() - [Thinking Reactively]() diff --git a/docs/guide/src/elements/component_children.md b/docs/guide/src/components/component_children.md similarity index 100% rename from docs/guide/src/elements/component_children.md rename to docs/guide/src/components/component_children.md diff --git a/docs/guide/src/elements/composing.md b/docs/guide/src/components/composing.md similarity index 100% rename from docs/guide/src/elements/composing.md rename to docs/guide/src/components/composing.md diff --git a/docs/guide/src/elements/exporting_components.md b/docs/guide/src/components/exporting_components.md similarity index 100% rename from docs/guide/src/elements/exporting_components.md rename to docs/guide/src/components/exporting_components.md diff --git a/docs/guide/src/elements/components.md b/docs/guide/src/components/index.md similarity index 100% rename from docs/guide/src/elements/components.md rename to docs/guide/src/components/index.md diff --git a/docs/guide/src/elements/propsmacro.md b/docs/guide/src/components/propsmacro.md similarity index 100% rename from docs/guide/src/elements/propsmacro.md rename to docs/guide/src/components/propsmacro.md diff --git a/docs/guide/src/elements/lists.md b/docs/guide/src/elements/lists.md index 40628c164..8cd58c841 100644 --- a/docs/guide/src/elements/lists.md +++ b/docs/guide/src/elements/lists.md @@ -130,7 +130,7 @@ Remember: keys let Dioxus uniquely identify an item among its siblings. A well-c You might be tempted to use an item's index in the array as its key. In fact, that’s what Dioxus will use if you don’t specify a key at all. This is only acceptable if you can guarantee that the list is constant – i.e., no re-ordering, additions or deletions. In all other cases, do not use the index for the key – it will lead to the performance problems described above. -Note that if you pass the key to a [custom component](./components.md) you've made, it won't receive the key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop: +Note that if you pass the key to a [custom component](../components/index.md) you've made, it won't receive the key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop: ```rust Post { key: "{key}", id: "{key}" } ``` From 01435d6aff049364e76e34acba29a08d36c18c00 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Thu, 10 Feb 2022 16:59:28 +0800 Subject: [PATCH 154/256] feat: add window api --- packages/desktop/src/desktop_context.rs | 6 ++++-- packages/desktop/src/lib.rs | 27 ++++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 294cfa23c..0b9c323c1 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -71,8 +71,10 @@ impl DesktopContext { } /// hide the menu - pub fn hide_menu(&self) { - let _ = self.proxy.send_event(UserWindowEvent::HideMenu); + pub fn set_decorations(&self, decoration: bool) { + let _ = self + .proxy + .send_event(UserWindowEvent::SetDecorations(decoration)); } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 452bbba96..5d45895a9 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -72,7 +72,7 @@ use tao::{ pub use wry; pub use wry::application as tao; use wry::{ - application::event_loop::EventLoopProxy, + application::{event_loop::EventLoopProxy, window::Fullscreen}, webview::RpcRequest, webview::{WebView, WebViewBuilder}, }; @@ -315,12 +315,24 @@ pub fn launch_with_props( window.set_maximized(state); } } + UserWindowEvent::Fullscreen(fullscreen) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_fullscreen(*fullscreen.clone()); + } + } UserWindowEvent::FocusWindow => { for webview in desktop.webviews.values() { let window = webview.window(); window.set_focus(); } } + UserWindowEvent::Resizable(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_resizable(state); + } + } UserWindowEvent::SetTitle(content) => { for webview in desktop.webviews.values() { @@ -328,16 +340,10 @@ pub fn launch_with_props( window.set_title(&content); } } - UserWindowEvent::Resizable(state) => { + UserWindowEvent::SetDecorations(state) => { for webview in desktop.webviews.values() { let window = webview.window(); - window.set_resizable(state); - } - } - UserWindowEvent::HideMenu => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.hide_menu(); + window.set_decorations(state); } } } @@ -360,9 +366,10 @@ pub enum UserWindowEvent { Minimize(bool), Maximize(bool), Resizable(bool), + Fullscreen(Box>), SetTitle(String), - HideMenu, + SetDecorations(bool), } pub struct DesktopController { From ebc768932d98e6b6edce8bc95a5c6e4b33e27a6a Mon Sep 17 00:00:00 2001 From: Reinis Mazeiks Date: Thu, 10 Feb 2022 12:15:37 +0200 Subject: [PATCH 155/256] Proofread chapter 6 --- docs/guide/src/SUMMARY.md | 5 - .../src/components/component_children.md | 2 - docs/guide/src/components/composing.md | 2 +- .../src/components/exporting_components.md | 14 +- docs/guide/src/components/index.md | 175 +----------------- docs/guide/src/components/propsmacro.md | 137 ++++++-------- docs/guide/src/interactivity/index.md | 26 ++- 7 files changed, 87 insertions(+), 274 deletions(-) diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 27d7e227f..fff21fd02 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -14,11 +14,6 @@ - [Reusing, Importing, and Exporting](components/exporting_components.md) - [Children and Attributes](components/component_children.md) - [How Data Flows](components/composing.md) -- [Thinking Reactively]() - - [Thinking Reactively]() - - [Thinking Reactively]() - - [Thinking Reactively]() - - [Thinking Reactively]() - [Adding Interactivity](interactivity/index.md) - [Hooks and Internal State](interactivity/hooks.md) - [UseState and UseRef](interactivity/importanthooks.md) diff --git a/docs/guide/src/components/component_children.md b/docs/guide/src/components/component_children.md index 52f116881..ea9b1fc45 100644 --- a/docs/guide/src/components/component_children.md +++ b/docs/guide/src/components/component_children.md @@ -215,5 +215,3 @@ In this chapter, we learned: - 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. - diff --git a/docs/guide/src/components/composing.md b/docs/guide/src/components/composing.md index dcd54b18f..785387e1b 100644 --- a/docs/guide/src/components/composing.md +++ b/docs/guide/src/components/composing.md @@ -1,6 +1,6 @@ # Thinking in React -We've finally reached the point in our tutorial where we can talk about the "Theory of React." We've talked about defining a declarative view, but not about the aspects that make our code *reactive*. +We've finally reached the point in our tutorial where we can talk about the theory of Reactive interfaces. We've talked about defining a declarative view, but not about the aspects that make our code *reactive*. Understanding the theory of reactive programming is essential to making sense of Dioxus and writing effective, performant UIs. diff --git a/docs/guide/src/components/exporting_components.md b/docs/guide/src/components/exporting_components.md index 350484c95..727e5b6b6 100644 --- a/docs/guide/src/components/exporting_components.md +++ b/docs/guide/src/components/exporting_components.md @@ -1,4 +1,3 @@ - # Reusing, Importing, and Exporting Components As your application grows in size, you'll want to start breaking your UI into components and, eventually, different files. This is a great idea to encapsulate functionality of your UI and scale your team. @@ -67,6 +66,8 @@ fn ActionCard(Scope) -> Element {} We should also create a `mod.rs` file in the `post` folder so we can use it from our `main.rs`. Our `Post` component and its props will go into this file. ```rust +// src/post/mod.rs + use dioxus::prelude::*; #[derive(PartialEq, Props)] @@ -289,14 +290,3 @@ use dioxus::prelude::*; pub struct ActionCardProps {} pub fn ActionCard(Scope) -> Element {} ``` - -## Moving forward - -Next chapter, we'll start to add use code to hide and show Elements with conditional rendering. - -For more reading on components: - -- [Components in depth]() -- [Lifecycles]() -- [The Context object]() -- [Optional Prop fields]() diff --git a/docs/guide/src/components/index.md b/docs/guide/src/components/index.md index 8b284b793..8db797c21 100644 --- a/docs/guide/src/components/index.md +++ b/docs/guide/src/components/index.md @@ -1,23 +1,22 @@ # Introduction to Components -In the previous chapter, we learned about Elements and how they can be composed to create a basic User Interface. In this chapter, we'll learn how to group Elements together to form Components. +In the previous chapter, we learned about Elements and how they can be composed to create a basic user interface. Now, we'll learn how to group Elements together to form Components. We'll cover: -In this chapter, we'll learn: - What makes a Component - How to model a component and its properties in Dioxus - How to "think declaratively" ## What is a component? -In short, a component is a special function that takes input properties and outputs an Element. Typically, Components serve a single purpose: group functionality of a User Interface. Much like a function encapsulates some specific computation task, a Component encapsulates some specific rendering task. +In short, a component is a special function that takes input properties and outputs an Element. Much like a function encapsulates some specific computation task, a Component encapsulates some specific rendering task – typically, rendering an isolated part of the user interface. -### Learning through prior art +### Real-world example -Let's take a look at a post on r/rust and see if we can sketch out a component representation. +Take a look at a post on Reddit: ![Reddit Post](../images/reddit_post.png) -This component has a bunch of important information: +We can build this post as a component, consisting of multiple subcomponents. It has several inputs: - The score - The number of comments @@ -62,166 +61,4 @@ If we included all this functionality in one `rsx!` call it would be huge! Inste - **MetaCard**: Original Poster, Time Submitted - **ActionCard**: View comments, Share, Save, Hide, Give award, Report, Crosspost -### Modeling with Dioxus - -We can start by sketching out the Element hierarchy using Dioxus. In general, our "Post" component will be comprised of the four sub-components listed above. First, let's define our `Post` component. - -Unlike normal functions, Dioxus components must explicitly define a single struct to contain all the inputs. These are commonly called "Properties" (props). Our component will be a combination of these properties and a function to render them. - -Our props must implement the `Props` trait and - if the component does not borrow any data - `PartialEq`. Both of these can be done automatically through derive macros: -```rust -#[derive(Props, PartialEq)] -struct PostProps { - id: Uuid, - score: i32, - comment_count: u32, - post_time: Instant, - url: String, - title: String, - original_poster: String -} -``` - -And our render function: -```rust -fn Post(cx: Scope) -> Element { - cx.render(rsx!{ - div { class: "post-container" - VoteButton { - score: cx.props.score, - } - TitleCard { - title: cx.props.title, - url: cx.props.url, - } - MetaCard { - original_poster: cx.props.original_poster, - post_time: cx.props.post_time, - } - ActionCard { - post_id: cx.props.id - } - } - }) -} -``` - -When declaring a component in `rsx!`, we can pass in properties using the traditional Rust struct syntax. Our `Post` component is simply a collection of smaller components wrapped together in a single container. - -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 `Scope` generic over some `Props` and return an `Element`. - -As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`. - -```rust -#[derive(PartialEq, Props)] -struct VoteButtonProps { - score: i32 -} - -fn VoteButton(cx: Scope) -> Element { - cx.render(rsx!{ - div { class: "votebutton" - div { class: "arrow up" } - div { class: "score", "{cx.props.score}"} - div { class: "arrow down" } - } - }) -} -``` - -## Borrowing - -You can avoid clones by using borrowed component syntax. For example, let's say we passed the `TitleCard` title as an `&str` instead of `String`. In JavaScript, the string would be copied by reference - none of the contents would be copied, but rather the reference to the string's contents are copied. In Rust, this would be similar to calling `clone` on `Rc`. - -Because we're working in Rust, we can choose to either use `Rc`, clone `Title` on every re-render of `Post`, or simply borrow it. In most cases, you'll just want to let `Title` be cloned. - -To enable borrowed values for your component, we need to add a lifetime to let the Rust compiler know that the output `Element` borrows from the component's props. - -```rust -#[derive(Props)] -struct TitleCardProps<'a> { - title: &'a str, -} - -fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element { - cx.render(rsx!{ - h1 { "{cx.props.title}" } - }) -} -``` - -For users of React: Dioxus knows *not* to memoize components that borrow property fields. By default, every component in Dioxus is memoized. This can be disabled by the presence of a non-`'static` borrow. - -This means that during the render process, a newer version of `TitleCardProps` will never be compared with a previous version, saving some clock cycles. - -## The inline_props macro - -Yes - *another* macro! However, this one is entirely optional. - -For internal components, we provide the `inline_props` macro, which will let you embed your `Props` definition right into the function arguments of your component. - -Our title card above would be transformed from: - -```rust -#[derive(Props, PartialEq)] -struct TitleCardProps { - title: String, -} - -fn TitleCard(cx: Scope) -> Element { - cx.render(rsx!{ - h1 { "{cx.props.title}" } - }) -} -``` - -to: - -```rust -#[inline_props] -fn TitleCard(cx: Scope, title: String) -> Element { - cx.render(rsx!{ - h1 { "{title}" } - }) -} -``` - -Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality. - -However, it's great for quickly throwing together an app without dealing with *any* extra boilerplate. - -## The `Scope` object - -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? - -React uses global variables to store this information. Global mutable variables must be carefully managed and are broadly discouraged in Rust programs. - -```javascript -function Component(props) { - let [state, set_state] = useState(10); -} -``` - - -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: Scope) -> Element { - cx.render(rsx!("hello")) -} -``` -## Moving forward - -Next chapter, we'll talk about composing Elements and Components across files to build a larger Dioxus App. - -For more references on components, make sure to check out: - -- [Components in depth]() -- [Lifecycles]() -- [The Scope object]() -- [Optional Prop fields]() - +In this chapter, we'll learn how to define our own components. diff --git a/docs/guide/src/components/propsmacro.md b/docs/guide/src/components/propsmacro.md index 1e4e994b3..1c54bd8a1 100644 --- a/docs/guide/src/components/propsmacro.md +++ b/docs/guide/src/components/propsmacro.md @@ -1,81 +1,77 @@ # Component Properties +Dioxus components are functions that accept Props as input and output an Element. In fact, the `App` function you saw in the previous chapter was a component with no Props! Most components, however, will need to take some Props to render something useful – so, in this section, we'll learn about props: -All component `properties` must implement the `Properties` trait. The `Props` macro automatically derives this trait but adds some additional functionality. In this section, we'll learn about: - -- Using the props macro +- Deriving the Props trait - Memoization through PartialEq - Optional fields on props - The inline_props macro +## Props +All properties that your components take must implement the `Props` trait. We can derive this trait on our own structs to define a Component's props. +> Dioxus `Props` is very similar to [@idanarye](https://github.com/idanarye)'s [TypedBuilder crate](https://github.com/idanarye/rust-typed-builder) and supports many of the same parameters. -## Using the Props Macro +There are 2 flavors of Props: owned and borrowed. -All `properties` that your components take must implement the `Properties` trait. The simplest props you can use is simply `()` - or no value at all. `Scope` is generic over your component's props and actually defaults to `()`. +- All Owned Props must implement `PartialEq` +- Borrowed props [borrow](https://doc.rust-lang.org/beta/rust-by-example/scope/borrow.html) values from the parent Component -```rust -// this scope -Scope<()> +### Owned Props -// is the same as this scope -Scope -``` - -If we wanted to define a component with its own props, we would create a new struct and tack on the `Props` derive macro: - -```rust -#[derive(Props)] -struct MyProps { - name: String -} -``` -This particular code will not compile - all `Props` must either a) borrow from their parent or b) implement `PartialEq`. Since our props do not borrow from their parent, they are `'static` and must implement PartialEq. - -For an owned example: +Owned Props are very simple – they don't borrow anything. Example: ```rust #[derive(Props, PartialEq)] struct MyProps { name: String } -``` -For a borrowed example: -```rust -#[derive(Props)] -struct MyProps<'a> { - name: &'a str -} -``` - -Then, to use these props in our component, we simply swap out the generic parameter on scope. - -For owned props, we just drop it in: - -```rust fn Demo(cx: Scope) -> Element { todo!() } ``` -However, for props that borrow data, we need to explicitly declare lifetimes. Rust does not know that our props and our component share the same lifetime, so must explicitly attach a lifetime in two places: +> The simplest Owned Props you can have is `()` - or no value at all. This is what the `App` Component takes as props. `Scope` accepts a generic for the Props which defaults to `()`. +> +> ```rust +>// this scope +>Scope<()> +> +>// is the same as this scope +>Scope +>``` + +### Borrowed Props + +Owning a string works well as long as you only need it in one place. But what if we need to pass a String from a `Post` Component to a `TitleCard` subcomponent? A naive solution might be to [`.clone()`](https://doc.rust-lang.org/std/clone/trait.Clone.html) the String, creating a copy of it – but this would be inefficient, especially for larger Strings. + +Rust allows for something more efficient – borrowing the String as a `&str`. Instead of creating a copy, this will give us a reference to the original String – this is what Borrowed Props are for! + +However, if we create a reference a String, Rust will require us to show that the String will not go away while we're using the reference. Otherwise, if we referenced something that doesn't exist, Bad Things could happen. To prevent this, Rust asks us to define a lifetime for the reference: ```rust -fn Demo<'a>(cx: Scope<'a, MyProps<'a>>) -> Element { - todo!() +#[derive(Props)] +struct TitleCardProps<'a> { + title: &'a str, +} + +fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element { + cx.render(rsx!{ + h1 { "{cx.props.title}" } + }) } ``` -By putting the `'a` lifetime on Scope and our Props, we can now borrow data from our parent and pass it on to our children. - +This lifetime `'a` tells the compiler that as long as `title` exists, the String it was created from must also exist. Dioxus will happily accept such a component. ## Memoization -If you're coming from React, you might be wondering how memoization fits in. For our purpose, memoization is the process in which we check if a component actually needs to be re-rendered when its props change. If a component's properties change but they wouldn't necessarily affect the output, then we don't need to actually re-render the component. +Dioxus uses Memoization for a more efficient user interface. Memoization is the process in which we check if a component actually needs to be re-rendered when its props change. If a component's properties change but they wouldn't affect the output, then we don't need to re-render the component, saving time! For example, let's say we have a component that has two children: ```rust fn Demo(cx: Scope) -> Element { + // don't worry about these 2, we'll cover them later let name = use_state(&cx, || String::from("bob")); let age = use_state(&cx, || 21); @@ -88,35 +84,15 @@ fn Demo(cx: Scope) -> Element { If `name` changes but `age` does not, then there is no reason to re-render our `Age` component since the contents of its props did not meaningfully change. +Dioxus memoizes owned components. It uses `PartialEq` to determine if a component needs rerendering, or if it has stayed the same. This is why you must derive PartialEq! -Dioxus implements memoization by default, which means you can always rely on props with `PartialEq` or no props at all to act as barriers in your app. This can be extremely useful when building larger apps where properties frequently change. By moving our state into a global state management solution, we can achieve precise, surgical re-renders, improving the performance of our app. +> This means you can always rely on props with `PartialEq` or no props at all to act as barriers in your app. This can be extremely useful when building larger apps where properties frequently change. By moving our state into a global state management solution, we can achieve precise, surgical re-renders, improving the performance of our app. +Borrowed Props cannot be safely memoized. However, this is not a problem – Dioxus relies on the memoization of their parents to determine if they need to be rerendered. -However, for components that borrow values from their parents, we cannot safely memoize them. +## Optional Props -For example, this component borrows `&str` - and if the parent re-renders, then the actual reference to `str` will probably be different. Since the data is borrowed, we need to pass a new version down the tree. - -```rust -#[derive(Props)] -struct MyProps<'a> { - name: &'a str -} - -fn Demo<'a>(cx: Scope<'a, MyProps<'a>>) -> Element { - todo!() -} -``` - -TLDR: -- if you see props with a lifetime or generics, it cannot be memoized -- memoization is done automatically through the `PartialEq` trait -- components with empty props can act as memoization barriers - -## Optional Fields - -Dioxus' `Props` macro is very similar to [@idanarye](https://github.com/idanarye)'s [TypedBuilder crate](https://github.com/idanarye/rust-typed-builder) and supports many of the same parameters. - -For example, you can easily create optional fields by attaching the `optional` modifier to a field. +You can easily create optional fields by attaching the `optional` modifier to a field: ```rust #[derive(Props, PartialEq)] @@ -128,7 +104,7 @@ struct MyProps { } fn Demo(cx: MyProps) -> Element { - ... + todo!() } ``` @@ -147,21 +123,16 @@ The `optional` modifier is a combination of two separate modifiers: `default` an - `default` - automatically add the field using its `Default` implementation - `strip_option` - automatically wrap values at the call site in `Some` -- `optional` - combine both `default` and `strip_option` +- `optional` - alias for `default` and `strip_option` - `into` - automatically call `into` on the value at the callsite For more information on how tags work, check out the [TypedBuilder](https://github.com/idanarye/rust-typed-builder) crate. However, all attributes for props in Dioxus are flattened (no need for `setter` syntax) and the `optional` field is new. +## The `inline_props` macro +So far, every Component function we've seen had a corresponding ComponentProps struct to pass in props. This was quite verbose... Wouldn't it be nice to have props as simple function arguments? Then we wouldn't need to define a Props struct, and instead of typing `cx.props.whatever`, we could just use `whatever` directly! - -## The inline_props macro - -Yes - *another* macro! However, this one is entirely optional. - -For internal components, we provide the `inline_props` macro, which will let you embed your `Props` definition right into the function arguments of your component. - -Our title card above would be transformed from: +`inline_props` allows you to do just that. Instead of typing the "full" version: ```rust #[derive(Props, PartialEq)] @@ -173,10 +144,10 @@ fn TitleCard(cx: Scope) -> Element { cx.render(rsx!{ h1 { "{cx.props.title}" } }) -} +} ``` -to: +...you can define a function that accepts props as arguments. Then, just annotate it with `#[inline_props]`, and the macro will turn it into a regular Component for you: ```rust #[inline_props] @@ -184,9 +155,7 @@ fn TitleCard(cx: Scope, title: String) -> Element { cx.render(rsx!{ h1 { "{title}" } }) -} +} ``` -Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality. - -However, it's great for quickly throwing together an app without dealing with *any* extra boilerplate. +> While the new Component is shorter and easier to read, this macro should not be used by library authors since you have less control over Prop documentation. diff --git a/docs/guide/src/interactivity/index.md b/docs/guide/src/interactivity/index.md index a1f963bd3..8826398e5 100644 --- a/docs/guide/src/interactivity/index.md +++ b/docs/guide/src/interactivity/index.md @@ -1,6 +1,6 @@ # Adding Interactivity -So far, we've learned how to describe the structure and properties of our user interfaces. Unfortunately, they're static and quite a bit uninteresting. In this chapter, we're going to learn how to add interactivity through events, state, and tasks. +So far, we've learned how to describe the structure and properties of our user interfaces. Unfortunately, they're static and quite uninteresting. In this chapter, we're going to learn how to add interactivity through events, state, and tasks. ## Primer on interactivity @@ -179,6 +179,30 @@ In general, Dioxus should be plenty fast for most use cases. However, there are Don't worry - Dioxus is fast. But, if your app needs *extreme performance*, then take a look at the `Performance Tuning` in the `Advanced Guides` book. +## The `Scope` object + +Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration. + +Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state? + +```javascript +// in React: +function Component(props) { + // This state persists between component renders, but where does it live? + let [state, set_state] = useState(10); +} +``` + +React uses global variables to store this information. However, global mutable variables must be carefully managed and are broadly discouraged in Rust programs. Because Dioxus needs to work with the rules of Rust it uses the `Scope` rather than a global state 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: Scope) -> Element { + cx.render(rsx!("hello")) +} +``` + ## Moving On This overview was a lot of information - but it doesn't tell you everything! From fc7d94b8d163cbc194aa4c870249bbdcbb58fde5 Mon Sep 17 00:00:00 2001 From: Reinis Mazeiks Date: Thu, 10 Feb 2022 13:28:54 +0200 Subject: [PATCH 156/256] Proofread chapter 7 --- docs/guide/src/interactivity/hooks.md | 25 ++++++------------- .../guide/src/interactivity/importanthooks.md | 23 +++-------------- docs/guide/src/interactivity/index.md | 8 +++--- 3 files changed, 17 insertions(+), 39 deletions(-) diff --git a/docs/guide/src/interactivity/hooks.md b/docs/guide/src/interactivity/hooks.md index b9ec32e58..a90afe2dc 100644 --- a/docs/guide/src/interactivity/hooks.md +++ b/docs/guide/src/interactivity/hooks.md @@ -80,7 +80,7 @@ This pattern might seem strange at first, but it can be a significant upgrade ov ## Rules of hooks Hooks are sensitive to how they are used. To use hooks, you must abide by the -"rules of hooks" (borrowed from react)](https://reactjs.org/docs/hooks-rules.html): +"rules of hooks" ([borrowed from react](https://reactjs.org/docs/hooks-rules.html)): - Functions with "use_" should not be called in callbacks - Functions with "use_" should not be called out of order @@ -245,19 +245,10 @@ fn example(cx: Scope) -> Element { 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` - -For a more in-depth guide to building new hooks, checkout out the advanced hook building guide in the reference. - -## Wrapping up - -In this chapter, we learned about the mechanics and intricacies of storing state inside a component. - -In the next chapter, we'll cover event listeners in similar depth, and how to combine the two to build interactive components. +- [use_state](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_state.html) - store state with ergonomic updates +- [use_ref](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_ref.html) - store non-clone state with a refcell +- [use_future](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_future.html) - store a future to be polled after initialization +- [use_coroutine](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_coroutine.html) - store a future that can be stopped/started/communicated with +- [use_context_provider](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_context_provider.html) - expose state to descendent components +- [use_context](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_context.html) - consume state provided by `use_provide_context` +- [use_suspense](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_suspense.html) diff --git a/docs/guide/src/interactivity/importanthooks.md b/docs/guide/src/interactivity/importanthooks.md index fb985ce90..161c5becf 100644 --- a/docs/guide/src/interactivity/importanthooks.md +++ b/docs/guide/src/interactivity/importanthooks.md @@ -2,23 +2,13 @@ Most components you will write in Dioxus will need to store state somehow. For local state, we provide two very convenient hooks: -- `use_state` -- `use_ref` +- [use_state](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_state.html) +- [use_ref](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_ref.html) Both of these hooks are extremely powerful and flexible, so we've dedicated this section to understanding them properly. > These two hooks are not the only way to store state. You can always build your own hooks! -## Note on Hooks - -If you're struggling with errors due to usage in hooks, make sure you're following the rules of hooks: - -- Functions with "use_" should not be called in callbacks -- Functions with "use_" should not be called out of order -- Functions with "use_" should not be called in loops or conditionals - -A large majority of issues that seem to be "wrong with Dioxus" are actually just a misuse of hooks. - ## `use_state` The `use_state` hook is very similar to its React counterpart. When we use it, we get two values: @@ -84,7 +74,7 @@ cx.spawn({ You might've noticed a fundamental limitation to `use_state`: to modify the value in-place, it must be cheaply cloneable. But what if your type is not cheap to clone? -In these cases, you should reach for `use_ref` which is essentially just a glorified `Rc>` (typical Rust UI shenanigans). +In these cases, you should reach for `use_ref` which is essentially just a glorified `Rc>` (Rust [smart pointers](https://doc.rust-lang.org/book/ch15-04-rc.html)). This provides us some runtime locks around our data, trading reliability for performance. For most cases though, you will find it hard to make `use_ref` crash. @@ -117,7 +107,7 @@ names.write().push("Tiger"); If you don't want to re-render the component when names is updated, then we can use the `write_silent` method: ```rust -names.write().push("Transmogrifier"); +names.write_silent().push("Transmogrifier"); ``` Again, like `UseState`, the `UseRef` handle is clonable into async contexts: @@ -135,8 +125,3 @@ cx.spawn({ } }) ``` - - -## Wrapping up - -These two hooks are extremely powerful at storing state. diff --git a/docs/guide/src/interactivity/index.md b/docs/guide/src/interactivity/index.md index 8826398e5..6075a4a5a 100644 --- a/docs/guide/src/interactivity/index.md +++ b/docs/guide/src/interactivity/index.md @@ -44,9 +44,9 @@ fn App(cx: Scope) -> Element { } ``` -State in Dioxus follows a pattern called "one-way-data-flow." As your components create new components as their children, your app's structure will eventually grow into a tree where state gets passed down from the root component into "leaves" of the tree. +State in Dioxus follows a pattern called "one-way data-flow." As your components create new components as their children, your app's structure will eventually grow into a tree where state gets passed down from the root component into "leaves" of the tree. -You've probably seen the tree of UI components represented using an directed-acyclic-graph: +You've probably seen the tree of UI components represented using a directed acyclic graph: ![image](../images/component_tree.png) @@ -173,11 +173,13 @@ With these building blocks, we can craft new hooks similar to `use_state` that l In general, Dioxus should be plenty fast for most use cases. However, there are some rules you should consider following to ensure your apps are quick. -- 1) **Don't call set—state _while rendering_**. This will cause Dioxus to unnecessarily re-check the component for updates or enter an infinite loop. +- 1) **Don't call set_state _while rendering_**. This will cause Dioxus to unnecessarily re-check the component for updates or enter an infinite loop. - 2) **Break your state apart into smaller sections.** Hooks are explicitly designed to "unshackle" your state from the typical model-view-controller paradigm, making it easy to reuse useful bits of code with a single function. - 3) **Move local state down**. Dioxus will need to re-check child components of your app if the root component is constantly being updated. You'll get best results if rapidly-changing state does not cause major re-renders. + ## The `Scope` object From f5907a682e8320951c11cb6172b77abe4ca150e7 Mon Sep 17 00:00:00 2001 From: Reinis Mazeiks Date: Thu, 10 Feb 2022 15:50:11 +0200 Subject: [PATCH 157/256] Add a better introduction to what Components are. --- .../components/component_example_title.png | Bin 0 -> 8426 bytes .../components/component_example_votes.png | Bin 0 -> 4244 bytes docs/guide/src/components/index.md | 28 +-------- docs/guide/src/components/propsmacro.md | 55 +++++++++++++++--- 4 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 docs/guide/src/components/component_example_title.png create mode 100644 docs/guide/src/components/component_example_votes.png diff --git a/docs/guide/src/components/component_example_title.png b/docs/guide/src/components/component_example_title.png new file mode 100644 index 0000000000000000000000000000000000000000..60a5a329e36bac5b3aa2c63e8c27e3d9fed86b68 GIT binary patch literal 8426 zcmc(FWl$VIllB6^AvlY(S)AZba9Ci`V8Pu9?k)iWi%W3#kl>QwZo!t|ki`kXHMkz{ zRee?8y?6KC{kyK3nVPDe>7J*iXL_ERC^c1iTr3JK004li0EK7(07%aTu#JKCTq~EZ zlPRv;p&dj~>Qq4q>g#5E; zu_^d2!HFa>01^N{07d#N3|4Nc2OvYDM`=a{(KR_IB0#ACt&`JJJ8I*OTZr@93AH%w z*Gd}3e=}g^E|{-OxwBInV`H;F!*j^QX6?T?{NQtYeckx- zC5}T<{0R`gOZ>w22+pWj`=i+|IX55YJ%eE8 zwnri%0f{WbH>@}4WRv});gXJUmdsOY^0sMD-MsQ4>V^i~lO>BpLiM`Y;Z>NpRHn

J;Nn&ICk=JERm+-JGAaYXOxcTH^pRBQ5bj3h~ zG_GViW7!_i*~M0TRJ(Ouq{OA6{REtNyNFK2hY@v

7N*+YycQ)0%PpveYr-6z*}L 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 literal 0 HcmV?d00001 diff --git a/docs/guide/src/components/component_example_votes.png b/docs/guide/src/components/component_example_votes.png new file mode 100644 index 0000000000000000000000000000000000000000..b46739b2448965d953193dbdea05893caeff30bf GIT binary patch literal 4244 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 f|2HLT^#mAoxhbQ~VP)# Dioxus `Props` is very similar to [@idanarye](https://github.com/idanarye)'s [TypedBuilder crate](https://github.com/idanarye/rust-typed-builder) and supports many of the same parameters. @@ -20,16 +20,41 @@ There are 2 flavors of Props: owned and borrowed. Owned Props are very simple – they don't borrow anything. Example: ```rust -#[derive(Props, PartialEq)] -struct MyProps { - name: String +// Remember: owned props must implement PartialEq! +#[derive(PartialEq, Props)] +struct VoteButtonProps { + score: i32 } -fn Demo(cx: Scope) -> Element { - todo!() +fn VoteButton(cx: Scope) -> Element { + cx.render(rsx!{ + div { + div { "+" } + div { "{cx.props.score}"} + div { "-" } + } + }) } ``` +Now, we can use the VoteButton Component like we would use a regular HTML element: + +```rust +fn main() { + dioxus::desktop::launch(App); +} + +fn App(cx: Scope) -> Element { + cx.render(rsx! ( + VoteButton { score: 42 } + )) +} +``` + +And we can see that the Component indeed gets rendered: + +![Screenshot of running app. Text: "+ \ 42 \ -"](component_example_votes.png) + > The simplest Owned Props you can have is `()` - or no value at all. This is what the `App` Component takes as props. `Scope` accepts a generic for the Props which defaults to `()`. > > ```rust @@ -42,7 +67,7 @@ fn Demo(cx: Scope) -> Element { ### Borrowed Props -Owning a string works well as long as you only need it in one place. But what if we need to pass a String from a `Post` Component to a `TitleCard` subcomponent? A naive solution might be to [`.clone()`](https://doc.rust-lang.org/std/clone/trait.Clone.html) the String, creating a copy of it – but this would be inefficient, especially for larger Strings. +Owning props works well if your props are easy to copy around - like a single number. But what if we need to pass a larger data type, like a String from an `App` Component to a `TitleCard` subcomponent? A naive solution might be to [`.clone()`](https://doc.rust-lang.org/std/clone/trait.Clone.html) the String, creating a copy of it for the subcomponent – but this would be inefficient, especially for larger Strings. Rust allows for something more efficient – borrowing the String as a `&str`. Instead of creating a copy, this will give us a reference to the original String – this is what Borrowed Props are for! @@ -61,7 +86,21 @@ fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element { } ``` -This lifetime `'a` tells the compiler that as long as `title` exists, the String it was created from must also exist. Dioxus will happily accept such a component. +This lifetime `'a` tells the compiler that as long as `title` exists, the String it was created from must also exist. Dioxus will happily accept such a component – we can now render it alongside our VoteButton! + +```rust +fn App(cx: Scope) -> Element { + // For the sake of an example, we create the &str here. + // But you might as well borrow it from an owned String type. + let hello = "Hello Dioxus!"; + + cx.render(rsx! ( + VoteButton { score: 42 }, + TitleCard { title: hello } + )) +} +``` +![New screenshot of running app, now including a "Hello Dioxus!" heading.](component_example_title.png) ## Memoization From 81ea7a14287c79c1658e8b3be4c7ab6ebf12d219 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Thu, 10 Feb 2022 23:33:02 +0800 Subject: [PATCH 158/256] feat: add window api --- packages/desktop/src/desktop_context.rs | 14 ++++++++++++++ packages/desktop/src/lib.rs | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 0b9c323c1..4d09bb966 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -48,6 +48,10 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); } + pub fn visible(&self, visible: bool) { + let _ = self.proxy.send_event(UserWindowEvent::Visible(visible)); + } + /// close window pub fn close(&self) { let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); @@ -63,6 +67,16 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); } + pub fn always_on_top(&self, top: bool) { + let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top)); + } + + pub fn cursor_visible(&self, visible: bool) { + let _ = self + .proxy + .send_event(UserWindowEvent::CursorVisible(visible)); + } + /// set window title pub fn set_title(&self, title: &str) { let _ = self diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 5d45895a9..a0344c75a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -299,6 +299,12 @@ pub fn launch_with_props( // close window *control_flow = ControlFlow::Exit; } + UserWindowEvent::Visible(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_visible(state); + } + } UserWindowEvent::Minimize(state) => { // this loop just run once, because dioxus-desktop is unsupport multi-window. for webview in desktop.webviews.values() { @@ -333,6 +339,19 @@ pub fn launch_with_props( window.set_resizable(state); } } + UserWindowEvent::AlwaysOnTop(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_always_on_top(state); + } + } + + UserWindowEvent::CursorVisible(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_cursor_visible(state); + } + } UserWindowEvent::SetTitle(content) => { for webview in desktop.webviews.values() { @@ -363,11 +382,15 @@ pub enum UserWindowEvent { DragWindow, CloseWindow, FocusWindow, + Visible(bool), Minimize(bool), Maximize(bool), Resizable(bool), + AlwaysOnTop(bool), Fullscreen(Box>), + CursorVisible(bool), + SetTitle(String), SetDecorations(bool), } From 332ec30954d7d7edb89a306ebb8d16f1c063692a Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Thu, 10 Feb 2022 23:38:57 +0800 Subject: [PATCH 159/256] fix: change method name --- packages/desktop/src/desktop_context.rs | 26 +++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 4d09bb966..2f2c41d28 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use dioxus_core::ScopeState; -use wry::application::event_loop::EventLoopProxy; +use wry::application::{event_loop::EventLoopProxy, window::Fullscreen}; use crate::UserWindowEvent; @@ -34,44 +34,50 @@ impl DesktopContext { /// ```rust /// onmousedown: move |_| { desktop.drag_window(); } /// ``` - pub fn drag(&self) { + pub fn drag_window(&self) { let _ = self.proxy.send_event(UserWindowEvent::DragWindow); } /// set window minimize state - pub fn minimize(&self, minimized: bool) { + pub fn set_minimized(&self, minimized: bool) { let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized)); } /// set window maximize state - pub fn maximize(&self, maximized: bool) { + pub fn set_maximized(&self, maximized: bool) { let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); } - pub fn visible(&self, visible: bool) { + pub fn set_visible(&self, visible: bool) { let _ = self.proxy.send_event(UserWindowEvent::Visible(visible)); } /// close window - pub fn close(&self) { + pub fn close_window(&self) { let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); } /// set window to focus - pub fn focus(&self) { + pub fn set_focus(&self) { let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } + pub fn set_fullscreen(&self, fullscreen: Option) { + let _ = self + .proxy + .send_event(UserWindowEvent::Fullscreen(Box::new(fullscreen))); + } + /// set resizable state - pub fn resizable(&self, resizable: bool) { + pub fn set_resizable(&self, resizable: bool) { let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); } - pub fn always_on_top(&self, top: bool) { + pub fn set_always_on_top(&self, top: bool) { let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top)); } - pub fn cursor_visible(&self, visible: bool) { + pub fn set_cursor_visible(&self, visible: bool) { let _ = self .proxy .send_event(UserWindowEvent::CursorVisible(visible)); From f9366dff9299ec64f3d8741ae38c2c155e4a8af6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 11:18:36 -0500 Subject: [PATCH 160/256] feat: add docs autdeploy --- .github/workflows/docs.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..4f437ae85 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,35 @@ +name: github pages + +on: + push: + branches: + - master + paths: + - docs + +jobs: + build-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v1 + with: + mdbook-version: "0.4.10" + + - name: Build + run: cd docs && + cd guide && mdbook build -d ../docs/nightly/guide && cd .. && + cd reference && mdbook build -d ../docs/nightly/reference && cd .. && + cd router && mdbook build -d ../docs/nightly/router && cd .. + + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@v4.2.3 + with: + branch: gh-pages # The branch the action should deploy to. + folder: docs/nightly # The folder the action should deploy. + target-folder: docs/nightly + repository-name: dioxuslabs/docsite + clean: false + # token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now From 8be5cb097bdc419deb4b390d45227d7d37d4a81e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:01:34 -0500 Subject: [PATCH 161/256] docs: try fire deploy --- docs/router/src/chapter_1.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/router/src/chapter_1.md b/docs/router/src/chapter_1.md index b743fda35..303a0d049 100644 --- a/docs/router/src/chapter_1.md +++ b/docs/router/src/chapter_1.md @@ -1 +1,3 @@ # Chapter 1 + +The Dioxus Router is very important From b6f120ef58554f186bd72aeeeaa540573aa321ae Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:05:19 -0500 Subject: [PATCH 162/256] ci: limit main CI to ignore on docs --- .github/workflows/main.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6b6f9be21..f94fa9d29 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,15 @@ -on: [push, pull_request] - name: Rust CI +on: + push: + paths: + - packages + - examples + - src + - lib.rs + - Cargo.toml + pull_request: + jobs: check: name: Check From c0e9f39ccaedd04ed246b0aef8296fb61d5e8c51 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:05:53 -0500 Subject: [PATCH 163/256] ci: include .github as a regen folder --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f94fa9d29..a92c5af17 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,6 +8,7 @@ on: - src - lib.rs - Cargo.toml + - .github pull_request: jobs: From 95ea917fca4c320d2f5919f1de3e062dcb0cdcb1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:08:38 -0500 Subject: [PATCH 164/256] ci: fix pathing --- .github/workflows/main.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a92c5af17..8a6134bbb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,13 +3,15 @@ name: Rust CI on: push: paths: - - packages - - examples - - src + - packages/** + - examples/** + - src/** - lib.rs - Cargo.toml - .github pull_request: + branches: + - master jobs: check: From ae78a8381c12cc4cdb95cc02f7320c32a676de51 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:08:52 -0500 Subject: [PATCH 165/256] ci: fix pathing --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8a6134bbb..d8e62ade3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,9 +6,9 @@ on: - packages/** - examples/** - src/** + - .github/** - lib.rs - Cargo.toml - - .github pull_request: branches: - master From a858934d41e4082e278a0dc32ca1100b39357557 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:09:50 -0500 Subject: [PATCH 166/256] ci: use filtering for all --- .github/workflows/macos.yml | 15 +++++++++++++-- .github/workflows/windows.yml | 12 ++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 57b408a23..7003b3925 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,7 +1,18 @@ -on: [push, pull_request] - name: macOS tests +on: + push: + paths: + - packages/** + - examples/** + - src/** + - .github/** + - lib.rs + - Cargo.toml + pull_request: + branches: + - master + jobs: test: name: Test Suite diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 2b6ea5c23..cee707dde 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,9 +1,17 @@ name: windows + on: push: + paths: + - packages/** + - examples/** + - src/** + - .github/** + - lib.rs + - Cargo.toml + pull_request: branches: - master - pull_request: jobs: test: @@ -50,7 +58,7 @@ jobs: run: | rustc -Vv cargo -V - set RUST_BACKTRACE=1 + set RUST_BACKTRACE=1 cargo build --features "desktop, ssr, router" cargo test --features "desktop, ssr, router" shell: cmd From 7c6026067231dcd82efbee061a541a7c5c76f720 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:20:23 -0500 Subject: [PATCH 167/256] ci: fire deploy --- docs/router/src/chapter_1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/router/src/chapter_1.md b/docs/router/src/chapter_1.md index 303a0d049..3ff33c1e6 100644 --- a/docs/router/src/chapter_1.md +++ b/docs/router/src/chapter_1.md @@ -1,3 +1,3 @@ # Chapter 1 -The Dioxus Router is very important +The Dioxus Router is very important! From affdaf05ce893a86bd02a1230ae49d3b5c005f7d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:21:07 -0500 Subject: [PATCH 168/256] ci: fix docs --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4f437ae85..8bd3a0603 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,7 +5,7 @@ on: branches: - master paths: - - docs + - docs/** jobs: build-deploy: From 13456ec00cac2e8c93dd88be5e79fe218b49250e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:21:38 -0500 Subject: [PATCH 169/256] ci: fire deploy --- docs/router/src/chapter_1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/router/src/chapter_1.md b/docs/router/src/chapter_1.md index 3ff33c1e6..d4a45381b 100644 --- a/docs/router/src/chapter_1.md +++ b/docs/router/src/chapter_1.md @@ -1,3 +1,3 @@ # Chapter 1 -The Dioxus Router is very important! +The Dioxus Router is very important!! From 84fb8cf6cfbc5d9de517147d4c745df932ebc8c9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:22:47 -0500 Subject: [PATCH 170/256] fix: add folder --- docs/nightly/readme.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/nightly/readme.md diff --git a/docs/nightly/readme.md b/docs/nightly/readme.md new file mode 100644 index 000000000..99f706e81 --- /dev/null +++ b/docs/nightly/readme.md @@ -0,0 +1 @@ +this directory is for deploying into From d5120d81aa30faf66fa99a413a3b0b9a12c59efe Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:29:00 -0500 Subject: [PATCH 171/256] ci: enable deploy key --- .github/workflows/docs.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8bd3a0603..c00c6dd35 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,10 +6,12 @@ on: - master paths: - docs/** + - .github/workflows/docs.yml jobs: build-deploy: runs-on: ubuntu-latest + environment: docs steps: - uses: actions/checkout@v2 @@ -32,4 +34,4 @@ jobs: target-folder: docs/nightly repository-name: dioxuslabs/docsite clean: false - # token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now + token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now From a5477d287c8ad01fb41764cc60838fef395f0064 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 12:33:33 -0500 Subject: [PATCH 172/256] ci: fix deploy dir --- .github/workflows/docs.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c00c6dd35..d9bae48d6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,9 +22,9 @@ jobs: - name: Build run: cd docs && - cd guide && mdbook build -d ../docs/nightly/guide && cd .. && - cd reference && mdbook build -d ../docs/nightly/reference && cd .. && - cd router && mdbook build -d ../docs/nightly/router && cd .. + cd guide && mdbook build -d ../nightly/guide && cd .. && + cd reference && mdbook build -d ../nightly/reference && cd .. && + cd router && mdbook build -d ../nightly/router && cd .. - name: Deploy 🚀 uses: JamesIves/github-pages-deploy-action@v4.2.3 From b9541d20fb6730f78cc202194b2e73dd5a612cba Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 14:07:16 -0500 Subject: [PATCH 173/256] docs: link to nightly guide --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a44bf3c1..05ef19bf8 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,9 @@

Website | - Guide + Guide (0.1.8) + | + Guide (Master) | Examples

From cea2e494e97c4422a63f4e69171bc7fc32e03a80 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 14:08:11 -0500 Subject: [PATCH 174/256] docs: reorder guide --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05ef19bf8..02857ed18 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,11 @@

Website | + Examples + | Guide (0.1.8) | Guide (Master) - | - Examples

From 62f8e71f7f36add69d041d22138dbc0746c64b7a Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 09:05:32 +0800 Subject: [PATCH 175/256] fix: borderless example --- examples/borderless.rs | 6 +++--- packages/desktop/src/desktop_context.rs | 16 +++++++++++++++- packages/desktop/src/lib.rs | 18 +++++++++++++++++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/examples/borderless.rs b/examples/borderless.rs index d52e9fb5a..3122c91fa 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -13,7 +13,7 @@ fn app(cx: Scope) -> Element { link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } header { class: "text-gray-400 bg-gray-900 body-font", - onmousedown: move |_| window.drag(), + onmousedown: move |_| window.drag_window(), div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", @@ -23,13 +23,13 @@ fn app(cx: Scope) -> Element { button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| window.minimize(true), + onclick: move |_| window.set_minimized(true), "Minimize" } button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| window.close(), + onclick: move |_| window.close_window(), "Close" } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 2f2c41d28..c3f193629 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -48,6 +48,7 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); } + /// set window visible or not pub fn set_visible(&self, visible: bool) { let _ = self.proxy.send_event(UserWindowEvent::Visible(visible)); } @@ -62,6 +63,7 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } + /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: Option) { let _ = self .proxy @@ -73,16 +75,23 @@ impl DesktopContext { let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); } + /// set the window always on top pub fn set_always_on_top(&self, top: bool) { let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top)); } + // set cursor visible or not pub fn set_cursor_visible(&self, visible: bool) { let _ = self .proxy .send_event(UserWindowEvent::CursorVisible(visible)); } + // set cursor grab + pub fn set_cursor_grab(&self, grab: bool) { + let _ = self.proxy.send_event(UserWindowEvent::CursorGrab(grab)); + } + /// set window title pub fn set_title(&self, title: &str) { let _ = self @@ -90,12 +99,17 @@ impl DesktopContext { .send_event(UserWindowEvent::SetTitle(String::from(title))); } - /// hide the menu + /// change window to borderless pub fn set_decorations(&self, decoration: bool) { let _ = self .proxy .send_event(UserWindowEvent::SetDecorations(decoration)); } + + /// skip/hidden the taskbar icon + pub fn set_skip_taskbar(&self, skip: bool) { + let _ = self.proxy.send_event(UserWindowEvent::SkipTaskBar(skip)); + } } /// use this function can get the `DesktopContext` context. diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index a0344c75a..1835e6876 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -72,7 +72,9 @@ use tao::{ pub use wry; pub use wry::application as tao; use wry::{ - application::{event_loop::EventLoopProxy, window::Fullscreen}, + application::{ + event_loop::EventLoopProxy, platform::windows::WindowExtWindows, window::Fullscreen, + }, webview::RpcRequest, webview::{WebView, WebViewBuilder}, }; @@ -352,6 +354,12 @@ pub fn launch_with_props( window.set_cursor_visible(state); } } + UserWindowEvent::CursorGrab(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + let _ = window.set_cursor_grab(state); + } + } UserWindowEvent::SetTitle(content) => { for webview in desktop.webviews.values() { @@ -365,6 +373,12 @@ pub fn launch_with_props( window.set_decorations(state); } } + UserWindowEvent::SkipTaskBar(state) => { + for webview in desktop.webviews.values() { + let window = webview.window(); + window.set_skip_taskbar(state); + } + } } } Event::MainEventsCleared => {} @@ -390,9 +404,11 @@ pub enum UserWindowEvent { Fullscreen(Box>), CursorVisible(bool), + CursorGrab(bool), SetTitle(String), SetDecorations(bool), + SkipTaskBar(bool), } pub struct DesktopController { From c9fa19d009407874e9c581096c58c51ee75f83c7 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 09:10:27 +0800 Subject: [PATCH 176/256] fix: ci problem --- packages/desktop/src/desktop_context.rs | 5 ----- packages/desktop/src/lib.rs | 11 +---------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index c3f193629..5ae38adff 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -105,11 +105,6 @@ impl DesktopContext { .proxy .send_event(UserWindowEvent::SetDecorations(decoration)); } - - /// skip/hidden the taskbar icon - pub fn set_skip_taskbar(&self, skip: bool) { - let _ = self.proxy.send_event(UserWindowEvent::SkipTaskBar(skip)); - } } /// use this function can get the `DesktopContext` context. diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 1835e6876..101ab91b7 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -72,9 +72,7 @@ use tao::{ pub use wry; pub use wry::application as tao; use wry::{ - application::{ - event_loop::EventLoopProxy, platform::windows::WindowExtWindows, window::Fullscreen, - }, + application::{event_loop::EventLoopProxy, window::Fullscreen}, webview::RpcRequest, webview::{WebView, WebViewBuilder}, }; @@ -373,12 +371,6 @@ pub fn launch_with_props( window.set_decorations(state); } } - UserWindowEvent::SkipTaskBar(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_skip_taskbar(state); - } - } } } Event::MainEventsCleared => {} @@ -408,7 +400,6 @@ pub enum UserWindowEvent { SetTitle(String), SetDecorations(bool), - SkipTaskBar(bool), } pub struct DesktopController { From 30bb92f09afe1de588d644b5e2060524407f718f Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 09:46:23 +0800 Subject: [PATCH 177/256] fix: change method name --- packages/desktop/src/desktop_context.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 5ae38adff..d33ff5abe 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -34,7 +34,7 @@ impl DesktopContext { /// ```rust /// onmousedown: move |_| { desktop.drag_window(); } /// ``` - pub fn drag_window(&self) { + pub fn drag(&self) { let _ = self.proxy.send_event(UserWindowEvent::DragWindow); } @@ -54,12 +54,12 @@ impl DesktopContext { } /// close window - pub fn close_window(&self) { + pub fn close(&self) { let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); } /// set window to focus - pub fn set_focus(&self) { + pub fn focus(&self) { let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); } From 557201816b1bd18e92173902d110534f304628c5 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 09:51:12 +0800 Subject: [PATCH 178/256] fix: borderless example --- examples/borderless.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/borderless.rs b/examples/borderless.rs index 3122c91fa..1b7e2e0f5 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -13,7 +13,7 @@ fn app(cx: Scope) -> Element { link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } header { class: "text-gray-400 bg-gray-900 body-font", - onmousedown: move |_| window.drag_window(), + onmousedown: move |_| window.drag(), div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", @@ -29,7 +29,7 @@ fn app(cx: Scope) -> Element { button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| window.close_window(), + onclick: move |_| window.close(), "Close" } } From 4a3680ee1b31aa686b5716bf5447ce30ff01eb17 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 10 Feb 2022 21:00:15 -0500 Subject: [PATCH 179/256] chore: remove all warnings --- packages/core-macro/examples/html_test.rs | 8 --- packages/core-macro/examples/inline_props.rs | 22 -------- packages/core-macro/examples/prop_test.rs | 24 --------- packages/core/examples/borrowed.rs | 33 ------------ packages/core/examples/component_children.rs | 40 --------------- packages/core/examples/simple_hello.rs | 9 ---- packages/core/examples/works.rs | 54 -------------------- packages/core/tests/lifecycle.rs | 1 + packages/core/tests/miri_stress.rs | 22 +++----- packages/core/tests/passthru.rs | 5 +- packages/core/tests/sharedstate.rs | 2 +- packages/interpreter/src/bindings.rs | 2 + packages/router/examples/simple.rs | 18 ++----- packages/web/examples/hydrate.rs | 2 - packages/web/tests/hydrate.rs | 1 - 15 files changed, 17 insertions(+), 226 deletions(-) delete mode 100644 packages/core-macro/examples/html_test.rs delete mode 100644 packages/core-macro/examples/inline_props.rs delete mode 100644 packages/core-macro/examples/prop_test.rs delete mode 100644 packages/core/examples/borrowed.rs delete mode 100644 packages/core/examples/component_children.rs delete mode 100644 packages/core/examples/simple_hello.rs delete mode 100644 packages/core/examples/works.rs diff --git a/packages/core-macro/examples/html_test.rs b/packages/core-macro/examples/html_test.rs deleted file mode 100644 index c8e33ef8b..000000000 --- a/packages/core-macro/examples/html_test.rs +++ /dev/null @@ -1,8 +0,0 @@ -fn main() { - // use dioxus_core_macro::*; - // let r = html! { - //
- // "hello world" - //
- // }; -} diff --git a/packages/core-macro/examples/inline_props.rs b/packages/core-macro/examples/inline_props.rs deleted file mode 100644 index 28602ac30..000000000 --- a/packages/core-macro/examples/inline_props.rs +++ /dev/null @@ -1,22 +0,0 @@ -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<'a>( - cx: Scope<'a>, - chkk: String, - chkk2: String, - r: u32, - cat: &'a str, - drd: String, - e: String, -) -> Element<'a> { - let r = chkk.len(); -} diff --git a/packages/core-macro/examples/prop_test.rs b/packages/core-macro/examples/prop_test.rs deleted file mode 100644 index 525b3c8b0..000000000 --- a/packages/core-macro/examples/prop_test.rs +++ /dev/null @@ -1,24 +0,0 @@ -fn main() {} - -pub mod dioxus { - pub mod prelude { - pub trait Properties { - type Builder; - const IS_STATIC: bool; - fn builder() -> Self::Builder; - unsafe fn memoize(&self, other: &Self) -> bool; - } - } -} - -/// This implementation should require a "PartialEq" because it memoizes (no external references) -#[derive(PartialEq, dioxus_core_macro::Props)] -struct SomeProps { - a: String, -} - -/// This implementation does not require a "PartialEq" because it does not memoize -#[derive(dioxus_core_macro::Props)] -struct SomePropsTwo<'a> { - _a: &'a str, -} diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs deleted file mode 100644 index b7a9f513c..000000000 --- a/packages/core/examples/borrowed.rs +++ /dev/null @@ -1,33 +0,0 @@ -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_html as dioxus_elements; - -fn main() {} - -fn app(cx: Scope) -> Element { - cx.render(rsx!(div { - app2( - p: "asd" - ) - })) -} - -#[derive(Props)] -struct Borrowed<'a> { - p: &'a str, -} - -fn app2<'a>(cx: Scope<'a, Borrowed<'a>>) -> Element { - let g = eat2(&cx); - rsx!(cx, "") -} - -fn eat2(s: &ScopeState) {} - -fn eat(f: &str) {} - -fn bleat() { - let blah = String::from("asd"); - eat(&blah); -} diff --git a/packages/core/examples/component_children.rs b/packages/core/examples/component_children.rs deleted file mode 100644 index ac76740b8..000000000 --- a/packages/core/examples/component_children.rs +++ /dev/null @@ -1,40 +0,0 @@ -#![allow(non_snake_case)] - -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_html as dioxus_elements; - -fn main() { - let mut dom = VirtualDom::new(parent); - let edits = dom.rebuild(); - dbg!(edits); -} - -fn parent(cx: Scope) -> Element { - let value = cx.use_hook(|_| String::new()); - - cx.render(rsx! { - div { - child( - name: value, - h1 {"hi"} - ) - } - }) -} - -#[derive(Props)] -struct ChildProps<'a> { - name: &'a str, - children: Element<'a>, -} - -fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - cx.render(rsx! { - div { - "it's nested {cx.props.name}", - &cx.props.children - } - }) -} diff --git a/packages/core/examples/simple_hello.rs b/packages/core/examples/simple_hello.rs deleted file mode 100644 index 94a897e7c..000000000 --- a/packages/core/examples/simple_hello.rs +++ /dev/null @@ -1,9 +0,0 @@ -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_html as dioxus_elements; - -// very tiny hello world -fn main() { - dioxus::VirtualDom::new(|cx| rsx!(cx, "hello world")); -} diff --git a/packages/core/examples/works.rs b/packages/core/examples/works.rs deleted file mode 100644 index 0de94b5c3..000000000 --- a/packages/core/examples/works.rs +++ /dev/null @@ -1,54 +0,0 @@ -#![allow(non_snake_case)] - -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_html as dioxus_elements; - -fn main() { - let _ = VirtualDom::new(parent); -} - -fn parent(cx: Scope) -> Element { - let value = cx.use_hook(|_| String::new()); - - cx.render(rsx! { - div { - child( name: value ) - } - }) -} - -#[derive(Props)] -struct ChildProps<'a> { - name: &'a str, -} - -fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - cx.render(rsx! { - div { - h1 { "it's nested" } - grandchild( name: cx.props.name ) - } - }) -} - -#[derive(Props)] -struct Grandchild<'a> { - name: &'a str, -} - -fn grandchild<'a>(cx: Scope<'a, Grandchild>) -> Element<'a> { - cx.render(rsx! { - div { "Hello {cx.props.name}!" } - great_grandchild( name: cx.props.name ) - }) -} - -fn great_grandchild<'a>(cx: Scope<'a, Grandchild>) -> Element<'a> { - cx.render(rsx! { - div { - h1 { "it's nested" } - } - }) -} diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index f977ba96e..35058d418 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -1,4 +1,5 @@ #![allow(unused, non_upper_case_globals)] +#![allow(non_snake_case)] //! Tests for the lifecycle of components. use dioxus::prelude::*; diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 8ce030147..fe98c83ae 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -1,3 +1,4 @@ +#![allow(non_snake_case)] /* Stress Miri as much as possible. @@ -311,22 +312,11 @@ fn leak_thru_children() { #[test] fn test_pass_thru() { - #[inline_props] - fn Router<'a>(cx: Scope, children: Element<'a>) -> Element { - cx.render(rsx! { - div { - &cx.props.children - } - }) - } - #[inline_props] fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element { cx.render(rsx! { header { - nav { - &cx.props.children - } + nav { children } } }) } @@ -365,9 +355,9 @@ fn test_pass_thru() { class: "columns is-mobile", div { class: "column is-full", - &cx.props.nav, - &cx.props.body, - &cx.props.footer, + nav, + body, + footer, } } }) @@ -398,7 +388,7 @@ fn test_pass_thru() { let mut dom = new_dom(app, ()); let _ = dom.rebuild(); - for x in 0..40 { + for _ in 0..40 { dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); dom.work_with_deadline(|| false); dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); diff --git a/packages/core/tests/passthru.rs b/packages/core/tests/passthru.rs index e00aa545c..948de8b67 100644 --- a/packages/core/tests/passthru.rs +++ b/packages/core/tests/passthru.rs @@ -1,4 +1,5 @@ #![allow(unused, non_upper_case_globals)] +#![allow(non_snake_case)] //! Diffing Tests //! @@ -40,9 +41,7 @@ fn nested_passthru_creates() { #[inline_props] fn Child<'a>(cx: Scope, children: Element<'a>) -> Element { - cx.render(rsx! { - children - }) + cx.render(rsx! { children }) }; let mut dom = VirtualDom::new(app); diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs index 70e823569..18d569914 100644 --- a/packages/core/tests/sharedstate.rs +++ b/packages/core/tests/sharedstate.rs @@ -1,4 +1,4 @@ -#![allow(unused, non_upper_case_globals)] +#![allow(unused, non_upper_case_globals, non_snake_case)] use dioxus::{prelude::*, DomEdit, Mutations, SchedulerMsg, ScopeId}; use dioxus_core as dioxus; diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index e5cd4480c..ecd74870d 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -1,3 +1,5 @@ +#![allow(clippy::unused_unit, non_upper_case_globals)] + use js_sys::Function; use wasm_bindgen::prelude::*; use web_sys::{Element, Node}; diff --git a/packages/router/examples/simple.rs b/packages/router/examples/simple.rs index 4aebafb84..4afb122ae 100644 --- a/packages/router/examples/simple.rs +++ b/packages/router/examples/simple.rs @@ -1,8 +1,9 @@ +#![allow(non_snake_case)] + use dioxus_core::prelude::*; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; use dioxus_router::*; -use serde::{Deserialize, Serialize}; fn main() { console_error_panic_hook::set_once(); @@ -25,26 +26,17 @@ static APP: Component = |cx| { fn Home(cx: Scope) -> Element { cx.render(rsx! { - div { - h1 { "Home" } - } + h1 { "Home" } }) } fn BlogList(cx: Scope) -> Element { cx.render(rsx! { - div { - - } + div { "Blog List" } }) } fn BlogPost(cx: Scope) -> Element { let id = use_route(&cx).segment::("id")?; - - cx.render(rsx! { - div { - - } - }) + cx.render(rsx! { div { "{id:?}" } }) } diff --git a/packages/web/examples/hydrate.rs b/packages/web/examples/hydrate.rs index c78f4036a..3097db2e7 100644 --- a/packages/web/examples/hydrate.rs +++ b/packages/web/examples/hydrate.rs @@ -1,8 +1,6 @@ -use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; -use wasm_bindgen_test::wasm_bindgen_test; use web_sys::window; fn app(cx: Scope) -> Element { diff --git a/packages/web/tests/hydrate.rs b/packages/web/tests/hydrate.rs index b0ecc6c51..b8a034a5b 100644 --- a/packages/web/tests/hydrate.rs +++ b/packages/web/tests/hydrate.rs @@ -1,4 +1,3 @@ -use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; From da0f596cdee27ba3d32aa314b8bc4a3388fd2210 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 10:36:29 +0800 Subject: [PATCH 180/256] feat: commit code --- examples/borderless.rs | 2 ++ packages/desktop/src/desktop_context.rs | 6 +++--- packages/desktop/src/lib.rs | 24 +++++++++++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/examples/borderless.rs b/examples/borderless.rs index 1b7e2e0f5..55f44d592 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -9,6 +9,8 @@ fn main() { fn app(cx: Scope) -> Element { let window = dioxus::desktop::use_window(&cx); + // window.set_fullscreen(true); + cx.render(rsx!( link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } header { diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index d33ff5abe..d180e1235 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use dioxus_core::ScopeState; -use wry::application::{event_loop::EventLoopProxy, window::Fullscreen}; +use wry::application::event_loop::EventLoopProxy; use crate::UserWindowEvent; @@ -64,10 +64,10 @@ impl DesktopContext { } /// change window to fullscreen - pub fn set_fullscreen(&self, fullscreen: Option) { + pub fn set_fullscreen(&self, fullscreen: bool) { let _ = self .proxy - .send_event(UserWindowEvent::Fullscreen(Box::new(fullscreen))); + .send_event(UserWindowEvent::Fullscreen(fullscreen)); } /// set resizable state diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 101ab91b7..143225f6a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -292,6 +292,11 @@ pub fn launch_with_props( let window = webview.window(); // start to drag the window. // if the drag_window have any err. we don't do anything. + + if window.fullscreen().is_some() { + return; + } + let _ = window.drag_window(); } } @@ -321,10 +326,23 @@ pub fn launch_with_props( window.set_maximized(state); } } - UserWindowEvent::Fullscreen(fullscreen) => { + UserWindowEvent::Fullscreen(state) => { for webview in desktop.webviews.values() { let window = webview.window(); - window.set_fullscreen(*fullscreen.clone()); + + let current_monitor = window.current_monitor(); + + if current_monitor.is_none() { + return; + } + + let fullscreen = if state { + Some(Fullscreen::Borderless(current_monitor)) + } else { + None + }; + + window.set_fullscreen(fullscreen); } } UserWindowEvent::FocusWindow => { @@ -393,7 +411,7 @@ pub enum UserWindowEvent { Maximize(bool), Resizable(bool), AlwaysOnTop(bool), - Fullscreen(Box>), + Fullscreen(bool), CursorVisible(bool), CursorGrab(bool), From 69abb898239428435ad770448d4c3e768ceb3328 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 10:57:53 +0800 Subject: [PATCH 181/256] feat: commit code --- examples/borderless.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/borderless.rs b/examples/borderless.rs index 55f44d592..f81028dbd 100644 --- a/examples/borderless.rs +++ b/examples/borderless.rs @@ -9,7 +9,9 @@ fn main() { fn app(cx: Scope) -> Element { let window = dioxus::desktop::use_window(&cx); + // if you want to make window fullscreen, you need close the resizable. // window.set_fullscreen(true); + // window.set_resizable(false); cx.render(rsx!( link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } From 04d3098c9b4e8478f0fafad36b667780de5ac829 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 11:21:25 +0800 Subject: [PATCH 182/256] feat: add more `window api` example --- examples/borderless.rs | 42 ----------------- examples/window_event.rs | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 42 deletions(-) delete mode 100644 examples/borderless.rs create mode 100644 examples/window_event.rs diff --git a/examples/borderless.rs b/examples/borderless.rs deleted file mode 100644 index f81028dbd..000000000 --- a/examples/borderless.rs +++ /dev/null @@ -1,42 +0,0 @@ -use dioxus::prelude::*; - -fn main() { - dioxus::desktop::launch_cfg(app, |cfg| { - cfg.with_window(|w| w.with_title("BorderLess Demo").with_decorations(false)) - }); -} - -fn app(cx: Scope) -> Element { - let window = dioxus::desktop::use_window(&cx); - - // if you want to make window fullscreen, you need close the resizable. - // window.set_fullscreen(true); - // window.set_resizable(false); - - cx.render(rsx!( - link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } - header { - class: "text-gray-400 bg-gray-900 body-font", - onmousedown: move |_| window.drag(), - div { - class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", - a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", - span { class: "ml-3 text-xl", "Dioxus"} - } - nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" } - button { - class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", - onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| window.set_minimized(true), - "Minimize" - } - button { - class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", - onmousedown: |evt| evt.cancel_bubble(), - onclick: move |_| window.close(), - "Close" - } - } - } - )) -} diff --git a/examples/window_event.rs b/examples/window_event.rs new file mode 100644 index 000000000..525f0d841 --- /dev/null +++ b/examples/window_event.rs @@ -0,0 +1,97 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch_cfg(app, |cfg| { + cfg.with_window(|w| w.with_title("BorderLess Demo").with_decorations(false)) + }); +} + +fn app(cx: Scope) -> Element { + let window = dioxus::desktop::use_window(&cx); + + // if you want to make window fullscreen, you need close the resizable. + // window.set_fullscreen(true); + // window.set_resizable(false); + + let (fullscreen, set_fullscreen) = use_state(&cx, || false); + let (always_on_top, set_always_on_top) = use_state(&cx, || false); + let (decorations, set_decorations) = use_state(&cx, || false); + + cx.render(rsx!( + link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } + header { + class: "text-gray-400 bg-gray-900 body-font", + onmousedown: move |_| window.drag(), + div { + class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", + a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", + span { class: "ml-3 text-xl", "Dioxus"} + } + nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| window.set_minimized(true), + "Minimize" + } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + + window.set_fullscreen(!fullscreen); + window.set_resizable(*fullscreen); + + set_fullscreen(!fullscreen); + }, + "Fullscreen" + } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| window.close(), + "Close" + } + } + } + br {} + div { + class: "container mx-auto", + div { + class: "grid grid-cols-5", + div { + button { + class: "inline-flex items-center text-white bg-green-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + window.set_always_on_top(!always_on_top); + set_always_on_top(!always_on_top); + }, + "Always On Top" + } + } + div { + button { + class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + window.set_decorations(!decorations); + set_decorations(!decorations); + }, + "Set Decorations" + } + } + div { + button { + class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + window.set_title("Oh Dioxus !!!"); + }, + "Change Title" + } + } + } + } + )) +} From 8801e4be0028ec445919ec9a9f3208bf5c5071ba Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Fri, 11 Feb 2022 11:29:00 +0800 Subject: [PATCH 183/256] feat: change example --- examples/window_event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/window_event.rs b/examples/window_event.rs index 525f0d841..cb578829f 100644 --- a/examples/window_event.rs +++ b/examples/window_event.rs @@ -86,7 +86,7 @@ fn app(cx: Scope) -> Element { class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", onmousedown: |evt| evt.cancel_bubble(), onclick: move |_| { - window.set_title("Oh Dioxus !!!"); + window.set_title("Dioxus Application"); }, "Change Title" } From eb39f360e3f334eb9e4fb7f5eb4628c72d6222bf Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 11 Feb 2022 17:59:59 +0100 Subject: [PATCH 184/256] fix some uncaught error during runtime test --- packages/web/src/dom.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index c722ab737..48fb25e6e 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -87,7 +87,15 @@ impl WebsysDom { } }); - let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); + let document = load_document(); + let root = match document.get_element_by_id(&cfg.rootname) { + Some(root) => root, + // a match here in order to avoid some error during runtime browser test + None => { + let body = document.create_element("body").ok().unwrap(); + body + } + }; Self { interpreter: Interpreter::new(root.clone()), From f8c78f8ca9e6375712e088c50fc122f6297a922b Mon Sep 17 00:00:00 2001 From: Benjamin Lemelin Date: Fri, 11 Feb 2022 14:47:29 -0500 Subject: [PATCH 185/256] Transparent window means transparent WebView --- packages/desktop/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 143225f6a..a58996f89 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -177,6 +177,7 @@ pub fn launch_with_props( let mut webview = WebViewBuilder::new(window) .unwrap() + .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() .with_rpc_handler(move |_window: &Window, req: RpcRequest| { From d8bfc41662790ce076d8b291a486e61b435315d6 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 11 Feb 2022 21:58:44 +0100 Subject: [PATCH 186/256] fix code coverage action --- .github/workflows/main.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d8e62ade3..b1a6acede 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -95,12 +95,10 @@ jobs: image: xd009642/tarpaulin:develop-nightly options: --security-opt seccomp=unconfined steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Generate code coverage - run: | - apt-get update &&\ - apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ - cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml - - name: Upload to codecov.io - uses: codecov/codecov-action@v2 + - uses: actions/checkout@v2 + - run: apt-get update + - run: apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y + - run: cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + - uses: codecov/codecov-action@v2 + with: + fail_ci_if_error: false From c0753913ea6715e7d64be6d43b186761572982a4 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 11 Feb 2022 22:01:22 +0100 Subject: [PATCH 187/256] add names for clarity --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b1a6acede..3e84047d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -95,10 +95,13 @@ jobs: image: xd009642/tarpaulin:develop-nightly options: --security-opt seccomp=unconfined steps: + - name: Checkout repository - uses: actions/checkout@v2 + - name: Generate code coverage - run: apt-get update - run: apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y - run: cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + - name: Upload to codecov.io - uses: codecov/codecov-action@v2 with: fail_ci_if_error: false From e54b5aab04e1091de099d20f396adffa1f5071c7 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Fri, 11 Feb 2022 22:49:48 +0100 Subject: [PATCH 188/256] try fix docs workflow --- .github/workflows/docs.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d9bae48d6..f0d2a4b80 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,11 +2,12 @@ name: github pages on: push: - branches: - - master paths: - docs/** - .github/workflows/docs.yml + pull_request: + branches: + - master jobs: build-deploy: From 6e91b1d54e8da78b59dba8811d49f960141286d8 Mon Sep 17 00:00:00 2001 From: t1m0t Date: Sat, 12 Feb 2022 00:11:33 +0100 Subject: [PATCH 189/256] try fix syntax workflow --- .github/workflows/main.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e84047d0..029e50e88 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -96,12 +96,13 @@ jobs: options: --security-opt seccomp=unconfined steps: - name: Checkout repository - - uses: actions/checkout@v2 + uses: actions/checkout@v2 - name: Generate code coverage - - run: apt-get update - - run: apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y - - run: cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + run: | + apt-get update &&\ + apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ + cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml - name: Upload to codecov.io - - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v2 with: fail_ci_if_error: false From b64574810d56a5968919e44686e997219d8cd279 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sat, 12 Feb 2022 01:43:36 +0100 Subject: [PATCH 190/256] Clean up desktop's index.html Initially I wanted to delete only the duplicated `` opening tag, but then decided to make it a fully valid HTML document. Passes https://validator.w3.org/nu/#textarea check. --- packages/desktop/src/index.html | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 88fcba12a..e244b6faa 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -1,22 +1,15 @@ - - - - - - - - - -
-
- - - - + + Dioxus app + + + +
+ + From 2d1371167f05f3e5ffc53470093ff03c984a44d9 Mon Sep 17 00:00:00 2001 From: Aster Date: Sat, 12 Feb 2022 21:22:05 +0800 Subject: [PATCH 191/256] Use `===` when rhs is string --- packages/interpreter/src/interpreter.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 95c88d294..f0acd0802 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -102,15 +102,15 @@ export class Interpreter { SetAttribute(root, field, value, ns) { const name = field; const node = this.nodes[root]; - if (ns == "style") { + if (ns === "style") { // @ts-ignore node.style[name] = value; - } else if (ns != null || ns != undefined) { + } else if (ns != null || ns !== undefined) { node.setAttributeNS(ns, name, value); } else { switch (name) { case "value": - if (value != node.value) { + if (value !== node.value) { node.value = value; } break; @@ -125,7 +125,7 @@ export class Interpreter { break; default: // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { + if (value === "false" && bool_attrs.hasOwnProperty(name)) { node.removeAttribute(name); } else { node.setAttribute(name, value); @@ -142,7 +142,7 @@ export class Interpreter { node.checked = false; } else if (name === "selected") { node.selected = false; - } else if (name == "dangerous_inner_html") { + } else if (name === "dangerous_inner_html") { node.innerHTML = ""; } else { node.removeAttribute(name); @@ -200,10 +200,10 @@ export class Interpreter { `dioxus-prevent-default` ); - if (event.type == "click") { + if (event.type === "click") { // todo call prevent default if it's the right type of event if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { + if (target.tagName === "A") { event.preventDefault(); const href = target.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { @@ -213,7 +213,7 @@ export class Interpreter { } // also prevent buttons from submitting - if (target.tagName == "BUTTON") { + if (target.tagName === "BUTTON") { event.preventDefault(); } } @@ -237,16 +237,16 @@ export class Interpreter { if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); } - if (event.type == "submit") { + if (event.type === "submit") { event.preventDefault(); } - if (target.tagName == "FORM") { + if (target.tagName === "FORM") { for (let x = 0; x < target.elements.length; x++) { let element = target.elements[x]; let name = element.getAttribute("name"); if (name != null) { - if (element.getAttribute("type") == "checkbox") { + if (element.getAttribute("type") === "checkbox") { // @ts-ignore contents.values[name] = element.checked ? "true" : "false"; } else { @@ -349,7 +349,7 @@ export function serialize_event(event) { case "submit": { let target = event.target; let value = target.value ?? target.textContent; - if (target.type == "checkbox") { + if (target.type === "checkbox") { value = target.checked ? "true" : "false"; } return { From aad055cd84e7d592aba9606b0ff6447b620bcaf1 Mon Sep 17 00:00:00 2001 From: Aster Date: Sat, 12 Feb 2022 21:45:42 +0800 Subject: [PATCH 192/256] Fix typo `WryProtocol` --- packages/desktop/src/cfg.rs | 14 ++++++++++---- packages/desktop/src/lib.rs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 5baa46f5d..25e280a0f 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,3 +1,4 @@ +use wry::application::window::Icon; use wry::{ application::{ event_loop::EventLoop, @@ -13,12 +14,12 @@ pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); pub struct DesktopConfig { pub window: WindowBuilder, pub file_drop_handler: Option bool>>, - pub protocos: Vec, + pub protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) event_handler: Option>, } -pub type WryProtocl = ( +pub type WryProtocol = ( String, Box WryResult + 'static>, ); @@ -31,7 +32,7 @@ impl DesktopConfig { Self { event_handler: None, window, - protocos: Vec::new(), + protocols: Vec::new(), file_drop_handler: None, pre_rendered: None, } @@ -75,7 +76,12 @@ impl DesktopConfig { where F: Fn(&HttpRequest) -> WryResult + 'static, { - self.protocos.push((name, Box::new(handler))); + self.protocols.push((name, Box::new(handler))); + self + } + + pub fn with_icon(&mut self, icon: Icon) -> &mut Self { + self.window.window.window_icon = Some(icon); self } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index a58996f89..596d9c260 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -261,7 +261,7 @@ pub fn launch_with_props( .unwrap_or_default() }); - for (name, handler) in cfg.protocos.drain(..) { + for (name, handler) in cfg.protocols.drain(..) { webview = webview.with_custom_protocol(name, handler) } From 0596088312b32a3e9279cb09df119c82ead0b376 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 12 Feb 2022 10:57:57 -0500 Subject: [PATCH 193/256] docs: fix lifetimes on children --- docs/guide/src/components/component_children.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/src/components/component_children.md b/docs/guide/src/components/component_children.md index ea9b1fc45..809ae3d9b 100644 --- a/docs/guide/src/components/component_children.md +++ b/docs/guide/src/components/component_children.md @@ -30,7 +30,7 @@ struct ClickableProps<'a> { title: &'a str } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}" @@ -64,7 +64,7 @@ struct ClickableProps<'a> { body: Element<'a> } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -98,7 +98,7 @@ struct ClickableProps<'a> { children: Element<'a> } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -125,7 +125,7 @@ While technically allowed, it's an antipattern to pass children more than once i 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) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { match cx.props.children { Some(VNode::Text(text)) => { // ... @@ -184,7 +184,7 @@ struct ClickableProps<'a> { onclick: EventHandler<'a, MouseEvent> } -fn clickable(cx: Scope) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { cx.render(rsx!( a { onclick: move |evt| cx.props.onclick.call(evt) From 4981e4c26b17ec20950cb895d31a44c76aceeb73 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 12 Feb 2022 11:02:05 -0500 Subject: [PATCH 194/256] ci: make codecov be quieter --- codecov.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..69cb76019 --- /dev/null +++ b/codecov.yml @@ -0,0 +1 @@ +comment: false From a325c03dd93b0cfb042487d228ad493adc7aebd0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 12 Feb 2022 11:02:55 -0500 Subject: [PATCH 195/256] fix: lifetimes again --- docs/guide/src/components/component_children.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/src/components/component_children.md b/docs/guide/src/components/component_children.md index 809ae3d9b..1259422af 100644 --- a/docs/guide/src/components/component_children.md +++ b/docs/guide/src/components/component_children.md @@ -30,7 +30,7 @@ struct ClickableProps<'a> { title: &'a str } -fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}" @@ -98,7 +98,7 @@ struct ClickableProps<'a> { children: Element<'a> } -fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -125,7 +125,7 @@ While technically allowed, it's an antipattern to pass children more than once i 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<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { match cx.props.children { Some(VNode::Text(text)) => { // ... @@ -160,7 +160,7 @@ struct ClickableProps<'a> { attributes: Attributes<'a> } -fn clickable(cx: Scope) -> Element { +fn clickable(cx: Scope>) -> Element { cx.render(rsx!( a { ..cx.props.attributes, @@ -184,7 +184,7 @@ struct ClickableProps<'a> { onclick: EventHandler<'a, MouseEvent> } -fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { onclick: move |evt| cx.props.onclick.call(evt) From 78ac592c0aaec89041c6e0210edf3573b5334ce4 Mon Sep 17 00:00:00 2001 From: Aster Date: Sun, 13 Feb 2022 00:34:45 +0800 Subject: [PATCH 196/256] Custom for icon --- packages/desktop/Cargo.toml | 1 + packages/desktop/src/cfg.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index fa1bc88dc..fcb3a19e9 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -32,6 +32,7 @@ dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } +image = "0.24.0" [features] diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 25e280a0f..a177c8a4b 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,3 +1,6 @@ +use image::io::Reader as ImageReader; +use image::ImageFormat; +use std::io::Cursor; use wry::application::window::Icon; use wry::{ application::{ @@ -86,6 +89,19 @@ impl DesktopConfig { } } +impl DesktopConfig { + pub(crate) fn with_default_icon(mut self) -> Self { + let png: &[u8] = include_bytes!("default_icon.png"); + let mut reader = ImageReader::new(Cursor::new(png)); + reader.set_format(ImageFormat::Png); + let icon = reader.decode().expect("image parse failed"); + let rgba = Icon::from_rgba(icon.as_bytes().to_owned(), icon.width(), icon.height()) + .expect("image parse failed"); + self.window.window.window_icon = Some(rgba); + self + } +} + impl Default for DesktopConfig { fn default() -> Self { Self::new() From f6b30d26b969e77df57fd20084d11057d7307e6b Mon Sep 17 00:00:00 2001 From: Aster Date: Sun, 13 Feb 2022 00:35:02 +0800 Subject: [PATCH 197/256] Add default desktop icon --- packages/desktop/src/default_icon.png | Bin 0 -> 77196 bytes packages/desktop/src/lib.rs | 56 ++------------------------ packages/desktop/src/readme.md | 51 +++++++++++++++++++++++ 3 files changed, 55 insertions(+), 52 deletions(-) create mode 100644 packages/desktop/src/default_icon.png create mode 100644 packages/desktop/src/readme.md diff --git a/packages/desktop/src/default_icon.png b/packages/desktop/src/default_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d0742ea8a067fdcdbd24b45866ec7794dfa4a399 GIT binary patch literal 77196 zcmX_nRahKN7bb2YFgOI)3>qxBySux)yX)W@+=4pDk2?7Fg?DK~J2fm`QnidKH5da}6BBd&Rx0- z>IgWZ3kYDNK_QY$Y9KE!wq-lFww1r>zq5;J#ap32>3p+nhzhJm%O;mD^DuR z^P;LtDVl_tEPI}5HBqsN$IK9Lo1w%U|v zIa_RaIPv>{Tgvjn9+8ydU8el+g|@c#?(WQq^=1r}-rWFt&Yid0RySlApce$l0`!Ie zVs|-oq>dqn949jS80n;_qq1(Wg@ztU5U89d4v|P>;kwyvS3u*L?5C$wiy?&EZU%49 zcOLi4=A@wRxiV!Y&uf#Lm9;gSswZ1}Z~R!!16=wK1_*x0OUwX5?}u|+_$gV0W2C(4=_6^gv@0f4zy!&H+ z_OOXRB#~oZ8DB<^n)-P;>*9~S@2SHcX>j|+Tf6UzTls3$#*W+C^t9k$41URGom0!$ zowe&OFE6hhL#i`%5xuOeY$C3)atJ^iU5FtsNp8V+D(B|YwnnbP4=zgv?MEH$aIPGXpf^+mO{Ga+)D{(CMjuV$WJFv*#Ox7~MIwt3ztrFrhKYrAqE zo>VTDQEZi;#*7l3pPtTpiOpL%7dShh+&uMeF&Rz}TwPviux`*(2tE$|=26sWDxFa6 z|8{zz(d>|Ci5$CoyD9-Jg7>8YrCo3wo*Wg#sS$rX4)QyGygeL>zi82=)C{H2YhUZX ztSs_ftt@t`wN!a4;72ySuV}y8KR4BxjVq<<`#quh-Bswv;Gdn=Xy_?cYSxA7dmR^5 zXR=z(6x8gTRsePvAAMibnHE-a?+!im-~2|BsQDkq%=zjRsDI{ThxRisXtX#{R<)j# z!$#r!c%RgGI&Q>hD%yK_+;r8B?2@OM?mW8$VaPp>l&e-eTId(_U)S!{jxrR~nNR9@ z^}yoz*j?`pcR>OMBCD%~{>e+;zwr?f4kFdt!*8|PE&dWGdfF_>avI50uhOwhF4@QG zIWEbOOJUIEdH@&M>PI?wrR30Gd3GCTo8Io4?s&!86{Uiym5X0beqZ<;9v+Nmx@Xy% zV@nfqJRcR9D@Z+{EC2}y@#?n+3f2om zqp*Fsjy)wCbKY0_1cGPsD#v>on`PO4{#G8+|LznPnuaD7Q3wMf4Vo1U>9yFzk18T+Kh?b$kIRL*(?2w4-=bAPrV4P#SNB~hfyOu=PZ{QNp%YPk@p}6vAQd&;Lx3mYH-1g zioJAd()Ah6m#ZHBoxHR_6@gz(Kx>drp~GW@P3XUlGl^G{E4F32 zo={cMY0WvwebIiQ_y~H{;07eZOS7S^S6t-=wAZHXqpJMXo=kKXAA$y z8;vzMsf2FaRg+XVO}**8n>1B{`}F$KYOvPiAGY_^W2|)qb})T!_nE_q6UZuD)s|0aWN4v z#pTc(Ziwnzk0*yRH@OW)<_Aet0#7Eh$t3vW5_O*Q@$bQf?3{@<#9aJ%@$r5^oleJ# z)KBoOff62x0h8%NuEMtBS^MaIFbP_{rM1Hm2sc^Jbfc|iTjHuv`u(@d%;R)tHTGzD zt=FB^0msCI3Dj+8!RR~N_b@AC5KRy@Qh8+V2vdwYEkiM`BUVTON4OK2`03{9>6(R)0czGOhZ~WgqHC&fFw>m=#&9!+dL6bHVYD$7ZrfNb$LJXhHuG0B?VRRZZjgL*C)s`L^Qx!03^0TN>?24r{cnFn#lSSw>uGI_L>K?de2B((As3tbQNKsoKMWeHwSb?7Y zne%%y+-@m%a?Bw2{kJ$G@&4}e!9oHHuJ zaNqDe+36YW@1zH-LAs~I7 zw=;}52>+kBe9@O6r#jM0NGy@^AFv<*f}xJkL|IApvloPWA$J1hmqwIT1gG(zi!zVT zi(T(LMfIi01$Gdvq`4;>a~JTgqs&ND6f!S~5R?R>qzhX1=G8HA9FGGsC`LZ&{xT$o8=pmdpjDE+=^}n5oG7iRIX))1& zkqS`Jj0$huq$ISwM@x?|t;F64gmF9p&~f~53}ogu@wFYCvB4w=JMa0YO&B9l~puQKDfHVX^Y3i`_C9_d@MamOYjeQ+ws~& zz94XIk)Y-z2~d;3PjC8GldGj<%*t_(+w|ocskAG30jk)QUY6?JLL?AoN4B|N#bkkhm&G($jo+J>e{N?h%MY9WADfcY zhYl0pX^UEulwP#7wB8@Uz-Yv`)bb7}m0I;;J{Oqt>4Xko&P=B&D}*-H`?g?zXiZ{TFw}n!YJ?lnfCW1y93CdeiN@I!9dzC&+R72Y?0nfq$m{WvC{L<_0`;AdCX5D|JNt)PhNFl+ z?~oZKLF?hUw0|wKFyFd3Kgw25K`hsa{P!H6>m0k5{giRleSP)Y?#r+C9(|{5HcJjS z3xBlZ9CR0fe;ARy50ActKzVt2G1{)wcfHN>zs+j%Jq^qAsr^9x3x-q6+?!7mQx9*X z{?;y-B@V)fUEj(FC`Ba-R+VyhnJz|Yr=50=)3#&=_^`>2jzQLB6Y#ON#skTx_ZLik`n^yNc^@1Mm{-Vo1Q`jNh8(kN@2(S>>y)+1RIw2%S!9Ih;6~pG~GzR5Sk>B>_;jF%6aEu7Bq=ILzLaktP_eJn)p+NYI9 z4%+320~t2M;3WlAaFZ2Y`K*CPXe{HhlNkUd4OnfAjf+hB3f*gE*n)G-U zurNBHL5t-X zFzgHcL3Oxey!`A%_#6fI0~2Z0uKj92^y4w^V<}rS1w)Qwdrq9#_m*hRAP|EEUXm0m zCYl>P;|FKJL5sNYyZ<{p#qYp&I&uSD3);h}K2D*NVAO;HVPaYrSg%(I*yKJAB?0Q& z=|0M!u#28u^$z?_o`whSj6vS@Q0II2z}-;Z)0(xUnXOL2g;VJt+2dbOD#fNuDt z9GSWwQ_NvJ+R~!L6eQJ6-w2s}>o2BB7x09xfaPO%Z)i5AONupAsrmIp!e?{F?uWXj zRWiKlDMReo!ibW#M=NwTr}Ke$-H5~c)1m*tCwar!Ekc!SGW_Q{Ro$Xf<<6OqBH9(l z{gIuWef&w@ay0AAWErC9P8X~BsqgoYUAqvYjCObQE%#07_#~Njebw_K8U=>!Fql@O zdAKYg#4y8g+dI%idwgJcsVGf^_2HO+wl=%g8-Wflb_zP>j}A%+Z-{d?!BX@I zd)wX?OZ8h?;ftd32nw zUbQw`*i`7HWQ&xT4r8z^U->qw>*}0exi_w2;U{@+#!~99VFWMZeCgK$6?Q~Tj|IN% z_fu|7@IpOxaev`~&&Ogst&9v2E}r}&jCtYE-lj{p18_By3BHh7>y zHrj9URZp%7)99V+woW)dKi_yz8o6>`cU7mUuv@Ist&Thtd&y>Td=1v=8@^CHdrbUPNr>eN zy1BWyGUA7_eqxeBYT%}YtHaG$A5ULl$n>@$6~wcO)M1j$h=Sz$u1 zEwdk}B~AFhI=(cir0qAO7oMCmRWS`HI2d%+wZBHtw}YF4i&e&(`@bbekAHgORlTCi|SJ%`3P<^^FruP$X1f^Xb=zxxOGK_!ggOpd_pomNCMEBQZOB z`LGapI{)m*>(xT(1w$=^$!uP;+l@_OegduQY&$*01EO#9$INQh*!ya_YqBzP#7v|& zf7TkdXR855O-En}D|6;wm;1$rnlJL8k3KY(;j$Yg2n}}?GT(|%GJeqL4Jb{ z=R2hNBVw`c8&}QknGa6a0$1GjVkpcCF|-Qc_`bsJVctrcyZyN_ zr031SSMQmR?m+0e(1u{8aKf%))$&pmoKaJxIl`SRv!96JedA-6)wF6T>pyPUmr|6l z@kxt|{6EX+dLGSMEj771xhV2lHN&{SthN^U-|qK-+LJ3NRt&S$qfVjl8^|1woe1vP zfBaxZf4C!Pu<(~aaste!v$xMNV0wZ~o|{P}C+gJnL+tb?wxoG#^WlM1>B7CHI->%u z7q8Ww144OI0%vx|QQ-3_kf*>X@1+)^i4LVi%hoG{W?2C2vzcz1H_Dh7wfqYM#FYH` z625OL?;0U#y58#g0=}*3NKy&M;gPoQ_pdeH9?!SO55>d3zJ+eLKl;+o5)V&Qv!BM= zsmtw>{Y9jVmm&cs8`m{@y{}-J>8Cp9Dd2K{Db|P72;qccWoLKKM-k*F^b6<)9b$wl zsEzVh$WRprfdC`T`7#c?$`;$k){+rLjz{L#!NjjyEfs2iq#AD3q&&MCQ+6hJMsEXB z`fu9LGwnN{YFS?{R+}#7wt+S`qkeyj^mc>%3KNR!U5e^P>-F6`)PY9lav+4v;}WC}`%d8D&~tKY z^4G4F)l)O^xBY?V3Kw9MAKOH7L}Aod_MI=?@bGar?&ISVZT+;|*JyP)_Log#?BNL* z*6Edeem~oF9^C97#=L!RT5}q`0^|Lwy1ry#YSCWCEbc5eDiXmW>)LaVEx>c$cl4^o zZcD*l5|_612OdnuZs$YG5*Ad zUh|vB5438Pk9ydgcCE+7mn=C?V^`3oPm)!{BKgI}*KHi>z3JxJp7-7xH`y`NqK3&* z`?==T3p{N<4hAaDSEq8>VdX56Mw`AJv1GYTH_;z=78&Mi=>`4Y!a6D)bR01Z*tlDB zBpPrfuK!&AZp&m2A##war)@O?DjyD-h8@w+@je~Zz}NRjDQR_4t>)H#OJS?f+DZR}&t z$5YOxDWLE8$fG9x>taD-7DA!jZZSonk^v}#bId}6X4;&9hve;z5L^YQtTGy-(*J=6 zv4xe@ihA5nt92z3iq@O5VjN-ChhO7Pit~5Wm{RWeNk%;1hl-i?ZPvUf{%}jYWmK9d zp1#)<<;F36Q`2QWaiSZZQ7_J`9Mv#;^V|6w(R?+4zu5|IYLg2U|J|CV>fYbR5^}Fh zfM=F9ut?Cy?hl1A<+o77YUKIDTvo7O5VcUL$+EloezwW1Dgzn6+V&uNv1efUY?L6m z7-A`+iVPO5PZJ0QZHiyeautB}i^7H}La=4r`Q_|# zzkq*}OEcg~q0zPfNQqX}4C@V;AgF#RZfR+0%2N3U7fm1Cvma4AnIu)T!Be0))VfBR z^8NT4-&Ql!+5VO@4s08=-#1kE_lA54HXTj*{h!5j{&yC{8B`E}xVx)Tuj9EGY&{U% z&*r?}CJYjJ|Jz|iHg}i+6SxN{>KuDZ%a(`c-PpE^&vAVk%`$PswHwXCoYSU4^V9MR z%Q1G<{zh7tnGT)q1^mhsP}6yU5r)RJfNXc0{7f|`IFW!=m9`t)AA&x6ghhyE-0tlV zK!}!o-STvrQC+}d22{A1rX+x7c5g0{9*}PdiwaX%tnlz2j%6Y)4*y4;(pQ@8yycLn zE(T^a^~a)@0ZYIS)uv!@DGh@4c!!s>rp2=?ZnuB9#M(2Z^OVy0g830_iv?2~Gn8NJ z86$##seuX^*l_4gx+{ZBI{0AIVu6x7NF((hLj2?*_o-u6Bup=mxBkf{ADw03mqb2& z=Se$8%$ianQkCXmI`$vBh8eJK1)o_bX+Xs8>mcOkOfQ9i zLU~$?bcW#{XP@6-nNiI!mIVBR*d2_kjtVJj3yAH8rwI9XH;PnPIfi+*fiJA$+DBv1 zsJ<$6(laG>Bk%t_TPp)!qhV#TI}lSXM>D3Sb2vuFuU1orSY9f*BWLBsC$p&QVd^Ib$V`{_Fqb>0eHqSx}K=a3Z2wcZrSe>%{IbBVHVr%3>0Oc z2Mu*dE5^Jj!YKb1Aq}e^q=b*!Lx zq>>Nr!)FhpCMD%UVMFTs_8=>!HP~Ye)}6~--K7aX&vYqVr%l;l4O?pOM*9(k<{1es z5`fA;C{Q}oEc>k4i?tm*r6Z)MwV(gaSUf)8oq7{56&7*gI?|}Cp25MwWF6NDeoGr3 zR&iTyD0(I;P80gGyWv-PV9Qo&_X*xX;GXe(;{b&QXXLCYz@+TP=2S9(=cObBQ#N62 zMVlx#uTa=a@%`mG3=F4^o61h;w7_Mtyu4iNxZ8smWHoXfyfFf8hFo46#A3~0^0laO zCV&5A`C3LriVn}A5bh#!_h{2(Cn6;!Mf@98X_5Y?Xk#{(X1LH{IxJkCdB?)L9Yx&^ z7^F3!B_X8r$2JfWjJG)AULQYCx%Snk9gj#yHK{@F1+!JNzrU7;O(})HDqty^5h>rR zAwy;y&{Y<(np)NC{GQ~sHB*Rq>Wa>v4Xg3z!A)Z_9@vdZoSPvAzb(KQY`P^-G?p|b zW}}+WE>|SU5P)RW8(A`u;MiHD<)-+3ZmCAQ$JO)p zL6Z+P@{c6iBSas-uxuZdkePA@6@6OyLRXSmgj6@8S(~C<)CN4Lz4_~RkK;giqGpa@ z7wpmaHNTfL;)ATrIqyDNk{GKjPr#EsP~30BXUeM|_BI%(3OHYZq%6>Yfwk*3odnu1sBk=~)(GCY+u=iY$N$ zi`#GbG}N2a;gxU;CyXquu7-)P(PrDdjsxx@j1`bH{FY3*8j*ATEupN05rw~h7+pa#=kc}Guv)uGuFH0> zmu-|b4cw4zz>+BQ1$9x!@Y`-AXRxD!HidxWgrTC|0oR@{5kl9l0D-wlyka@XkdVBy zO8Q5~9CjR$;b)0W@IL4e<0aY;j$1jGX^}}1zWUM>!pwJwG-SYlk6%R+Ex9h%2&;dTePYqMbo7`Qp^gt zktlN!Vs=+!LOAuEO< zClhqj{I~o?024AVW;U(dxT}UyUnOn69a=c6);@bphv6YC6wwmzlkLQV3IBSgDGCb% z#H~x5zN~TTe0yc9OA({0)R>?wBUC@AJt`>TL|Ks%UZ|XWcSQ~tCw_qk>|JzrKplEG(ZP)ehC($=hd0iOTO_B_C=Z)g~o{889{t(5J3 z-U6$ut1AIF+b>0l2f+%cF`DoC&1eW^VNHwc%#1^X8=b3jG9M2wYd^OH+C9zU@w|5a ziZ;az1Xn$YbSi@{jkFgJLqVu&VDRR@Vws*Iw`_;8HL4isfKU>!3JU$v|HIGG){2xq#jpA8ezjWCPma&Np@GFn>IefZcVG4b z9?SF5Mm|f9Ue1NB+)0>-ofF7$RzKTl zhCY)Jxq?Hl$vitdTgNVa9W?dYA_PLXoF(+WjFqHEpoXq)+330S@Lly_{0IGuFG;O5 z_drJFYv&l(@_%^7R7&^OVDmrn(@78ot4+(QDWm%+TxKgT2QPl*Z_n?4j64St=y1(y z_QZx*(EUr6wDZ{CkoRvqeQq*0!2%#c5Xt=07=<_l2xNSJsvj4SBmgxjqiixfX%nfS z9E9|xTTmg12#OSq3;F%ar1}jdhvJyPJVHvJb>F0(V?WmZ9F&&QCzq|Yny2BkTWcPC zxY+c!X(bnFxN&jzE36~vNQB6Pl3M5j4jB>&(>f}s`6XQ_amLf&&UDIXrCtSHgQwSa zEcZDF5Z8inCpU(U&_OAi?qz`4WO7c0UvprjV|v7^&?F*?svyIMhsA6oyZbj2ywQ#c zC?B85QVii2b;^Xvcmg`fwjN!Wh~y%t~1f}Qk3kvNR=F$gzARtKMcgvB#Y|9bA+%)IEBXDmNn%wTI{3seg29d_&E15EP(X$w zwQ~1Oa&3goSNwV#88jUGYz={I2Z1RILZGN@x&p!AvF+9~Z~0fh<{MiY#Si;>Jg_dD z16nf{OcC42d|Z0|sg{&(HI`tZRPHh7XrYAJ8Gjj(<4IUd0TFB*bPL3V5|csh-r(mq zS!>YI&Wb0IxyNSB?JKSG8=)?-nWV_2{Q0>eaDHbs-OKzNrF9A@WB5?luOB!j=Bk*~ ztTD4gT2dO3VLGqVSRN_^6a?T}3v0tBOZEG(6Py~8NITa{Xr+Ny^R71-L>JS}u0=hl z>GQnMB?BM4r~+a?WyC}jEg~S%(oH`5Hr6V~$l{yNWL=~h0G-K}asLemGb%MAZ>1A1 zxqij1-Ugq|=4kl;t?3I6LnZ6apLY{WgA3}iKzIfU9ZH356g?m;#BiVTF&SZ4Py!Oj zNMj=3y!w~h`S5{}<~%}r;~_cJ|Fa08(Q9=~5VE<@RUu;-`q8~<7{yif{1)_>FZ z4#l%cu9$bGb;c$lhaQ?s7w0~ni9(IV0|`JAlG6S&>J&~wPFz+lRy-idXYq5#qvv!3 zhLVG(ke0?-JZgGp4;h#*0EJU*L=VeFQu7k`AfQ@{YI87|^I>ohtnt|&fS0X{9p~m+ znpE^4t+nIBcn}s$*YcIPNHm$v(=jBcrl99jDA2Z^2;AQ8%^gk||I_`? zpob+L)kret>`5#T41LZb)}{Io^%&iLE11QkzK`;%SEaRA3l``&B?Z+^!3gT`6v6W4 zQPCrkx;<6xPX$nA&od(AnqXQ81N#dx^n)2U3Hl$8KBiBYQ=$(kD9V|oNzf!9o0JCL zzs3ohSd=(+uIu#joOXbPdu-!TPMa4DsRQqI?n@R*>mL?kEBaQNYI56#v%Z|)8=iioMuM22J? zP)uo@YtShnhV%E(vlL2gR{`hio>R5|ZKF@(_=Ce;4QRCm;i_o`e?PNSA5$erJqkBz zd3GDBspyIfQt{39n}GwOl2WEYejEs~LymM7Zd*u^Q#3N8q(NlUS!6L;dFp^6l#(6H_xP%qZK6)3EDNwfM;F~gM{aC<5SaJF+1EtZ6^qlzY`PHIO^5(YhJ zOx6mAk1Pq{#bJuSdQsTZ+)CK~O0XHmAs9hR02l%xNox*|ODZ(!>ZoNVIH}~aEGm+s z$-s>O9n8A@`SgZ}%^C5SFT)f_kV$3a76GhQ)or!TZiBy@ADd}QH@_E*|oRhxJf^q0az#; zJH}2DRo$vYR`0(o5II|M1ylnV)I!D6JR2KCGhlOon6xnYKv}k&H`F&2BByk3?R9km z&%Ix45&wWj;EHz~z%x$*rxcs`3J&wT1{(flpa_VyWYl{1e?wtG0#&sl|s}M zM=IlqEKbzU%ib@^Uc;7neBg~F1NZnA?sJlza#Uf%fmJHSI!5wC0VW^sK|Y5Q!7?O@ zF9MW5>!OoJWK1JT2w@B%P7nn4ne?6>yOR)yv~We;gR&3r+(Mt96D1qLq7|r(WkU(Mr{5p-$(jI|uhGtF z=D%GR^iQY?DY4uH*MZmT7k*9bBJEVEDd+=EDt{|u*wzE=g4pX$a?tgGzzHHbB{u{&lEW17r+@o(&c8Xp)H5gjVC|JWO2wh0NSS3k^*kGqNREx6LQ%M+X^g6&W)m27a&r^YJ@AdRii73@ph- zj%Bjj-+&2+HIeA)fe}qe{7|Z0zF|e{Wgzs6LUHV{4Z0H)w$yG_QG%E%nvwtk38tw7 z7f~!SUj%9JvX$pKiH4vJj|-I2j`h>E$`jC0am7z zl;JKUy$~25=Pt&OY&1oMe7X^jOYR?U2-M<&p}=$|k>?P~Loo4(^Sdn|o>x~G{jbIu zyumX?(yO~eOM5&U0%-$O$(pOG3n@j>iK#Fih2&wn`@9bGFqa0z0!{%gXdcH{`0RvI z7=;KB=p;!nB1&}h9uSs7^*ZB_2w~C&PNo@thT$bNElCGBU5}!x&Xc_ClnwDIsXxIW zBh3jM8!1*w$w2xdKs)Y08FmeYiNgQQXJs4kwX&QnJT8`h6JBdNcmZ9Cq7=J=s6FOE z9l+z~50)nRRu}{6e|}D0CvZZgtPhp{Ra`QPmR>95-Nv3T>f*@Y}fsxpJ z-ELPu2YFZ}IqGg={Ux_mrlYG=%*;Y5CIloFu^fggHt4Ke+G^ef&Wd(&{%>`k@u4Tp zwSgI#{nP?;4j7cSE%auJ0GnL?VCGmt>vLhzl!D1fF=?q#hy2^G&r$2E)n4-z*}qvW zmCSc38s{RxbvpK$mYAF;zwG_qt)8(LW4^dG_;aVp%Fl{@*<+b8{zp& zHrX$VaSiwjOF5rELlQ3{U>W5cJ}N1ub`se8nWf6?x{9%vk^51)zd!WpB{}6(k_0S`rFJY|&*g5{p1AAaqSKLuSZ?s)hm!35CAO{BnS;iJci&r99ZEwNHg}Uvs-Uv zFY53a9gS;jY{WV{JKOyJ6Z`uS&NtX?0AY*89>Mlyvd&W;@DnOjJl7ftbAthR0wLP#&+ew!~qShDS zM2?3HMDJ|vGUTw%^9V@A<)V?XPPdV10lCsDeNzbkj@%63+rKJU8PfmY2A6jB zV(vgdbD%>*@ilI7)&pBzt%wpNQ4*=eE;fP-f7f(7dcW-Hmt^8U@8x;aRH^Eg6Z{{q z#H=!(hZo5i`EokW&jp^eFv*mHP=PnU2*BVldEcSH%!pRmITr85tgscLlf=z-Ny-V! zgEI#&#q;-icvfL=jN+Ju7kZH}seE`gN5>kctLv9<0Y@Ps0;lZzU{YdAG~VTb7JB6(~23@A;_r7F}D9!Dsu6eslA^ zC6X03YkF}nMA%Ut89O9DBt>!O)q4DzXov(yQeGHaVAwHqM4HKwi|-z%0whZWZ`$pz z-RGz82=5P$FvTe5u+hxMn$HL%M^n%{OS6UW@t^TwSuh%FYPcN$fB^;C#&t^5YpNZE zql?bJ&`9nW(m(QozswnEnEzItqq{w~Cd0*Nui^%_2#8Ag7(Da1BzEs**eZ$E>9LId zn|cB#9`p!{>Is5?MN8mlSQlYaSRP!I;vEq>Y=&h>5rlG}95uqZ;l!yNi>9AbWQIRG zX9^2XQKS+pJj}XOuOfswrjX<9(S75#WD-}~&M8Of`a*3IWqS7>y}joseOX97D@+CB zAj1EWICnfh!s$miRQ1NiQkj;3gXi_D^sF0c8BQd8oSI4wVS)(5;0iXc*IBqFc4WMF(`uD* zKgNF>>FA<-h1fT#W&F4d<8OZhL24T8_bbyRw|6d>(ie5zTCf*Rzw9D$zV81O9e7Ln zBPa9$kW_HQ)0zK@UcWsORx3ee3H;nP0`s0;sz`bv z3Gd}J9bTc)@0&gY+8C!aRip4218{)Uc#akSM7`G6$_+HGpfffY7Z3ZO3UR>iX| zN5pC%7H@3aj!_;c0B!%6k;|`E)xWu0)c-j48nMQE@wxEcY$NrX*ge!k%?I#iOUW_xC&1yNy=Iql5(2W)O<$S3i$TdfyH@Tx_oJ4{U7?d8ETPhLK(ba}eg2ho^ zw%C$SFRjUoQ&B$F6Aq7b;W5bV_e+pR8XDYXqic-%Bk4ng40Fzbq0qsM(|nLVAUOys zpxv4V908IL5XL8dDx{D>s06AAxA0nd6tA2i06S0){xXBK28;GkhKVq^3|;~Pm9yC* zGIFTpIRv##X7_ZX#raTmQQzN}sUZNIwFl=Wl`*)uxHdAf>Y|emz?7GiJ~%N;6M;^m ze~+Jms0mYq^AaxvUqM-qHWx}qN_g(ePfAdp~6#b_yGI#*7ZXv#0JiDj<_TJ1W;*W1L+5A#2t$}k zk+GFY0cM_$DcwshsM*XBx`qdOofGLB!#(byELAtEn0-k_K5|03$ot z`Avq6rdTlmebU1*t*_?no;Wz9lZn!FLlFTy6c<>*YEaL>>i_co8FTW21DOzqzLfE7aC^}emrp*M65r{2BjT6XbDY;B% z%lU^gZgxd$ixx@z*9Jib5(;Qx@8>|us=)zbv{<1xE}7X9Max6KdUw?qH; zL+!9S&)W_$Z)Qqc3`zVs`|^HGkP zCL;Bq8N}m#*04Cy1|A6~^tDBDtPG}Ilo+29=2#Zz&9v=m<8K9PB{z6FB)WJ;>%LoW zAxt+A0slr5w0-80Kr=dz3yCT$scZL)TUs^*BCv2b7(SNC8W9yOfk1CPYtVzChQE;N zi}YR&)-)T;@XwOiU0mt9wzgFpa@j0XD!i6-8C^DP6G6Bl^&g(0w9@p;-l4&)7-*y* z@tVh8f1z|{)+pdyF&bN7qq%eLlKbZG(tpb+!=?>$rppxP5*1AHymxc7e`*O8VKDLukS!IUd@$UalAOrQ=dtUR`3U&=TM7OWhMh$f^8`^)V+c@C{DPj88- zf@B~gd>HK>Z>TyJkIc9-(3b)I>u7Y{($Q$opvADk_dj&jo$rF-i%nnMqqy(9XbQbX zlu%nvDCPguT3EAguxr|oBub}vn;EF3@xs>0X`^VKu=PWMWcYJ9aG#}IUshgk<^ffH z_psq=c_OMTVTHRRwAJ;H{Kf=e-0?pFo6O3UIs!nML{2_|Ier#_nY3VCNQl1@$X|=D zkPVSho#yW#@~qg2&sfSM%5<6Qfj5Gs;0FnO87O~yHB=1J6n|jAnpCkepbBumjAe4W zqvR(qa0*WpKz2X%j;?f?(@v=&n{+Udq#~7|Sk=7VO?AO)r@NeXQvbzpOh~3JyK#%2M&bJozsb`>;KH6_1Yai2eV1x z3y;^{fx^m|#%wyGp87*So3yl>86C{lLtDUpn13)I7V=EDCCW+%5llnUGxA#h)y z_LedhjvVy&bWpx89}xzVGeA8)aUO!itO83jG%la9C1Jdl`j0auwCl~(^Wqo05taj^ z`-)VdGKfH;QbiQBLj9e*Rc$~h6_OgNSZ1^NTGhI)DF@Mi`-5GqhbJVzxEt|jhK}YL zNo1;OV*mVm=(zA@FT!46Hu_u_CpgZUGqLZK{wG`km(tWmIB}YR3;e zXU@1m;p6e-8Bw@I0HkqfqWl#3BNJsxQXf*1E0fNEaYG0*5KNX7<$2-5F17nzpcYAJ zLUtZquCC>(f%!%hK@3ZRq&o?EkT$>8|NDx+zk3o~0wmF}po;moC4g)L{}+~=N&feu zAHJ5}VDds9s+wk;hHu}_#Iv8I{7#5>cnJZyNNt3Ab#*a$0qHC(Ac1kVh%lzuXV+eB zi+;dPy<3hhgAeyB*|G*1Uv*qTVSH4o-n7K08AO`JiE}gI`-Mvi3K<>bC;=Id9M_~a z9PN-OhN`6)1_f#>W^_3W{l1;QE^;UY(V0;Eol;4=C)nF|hdRu}=zbRbeQ4xuf|eLM zxF|W;H+^n?f+IrS%!D9n1bzs0(yGl`c=rpGF^Md52^~hXqvNzwp@qIx%64zv65nxR|d%PS$ITCcx)?S^=$jCUCr!O@Xsp(k6 zHJ{EGh3C8e6-PIDkb(sy5f}nWk@kBp@<++>_S(kx&aJ~OlKYd@xDX7EvlQcx)|P^P zGSZhZ_86()m2~3yQg=_vckfhu;Eh*vAr2uX(duq7+gqtfQFdEC123oM5A)4Bm`suV za3PolW;@rHyf3sQ%JP?`24q~g)US|I^2S#nQ0`kZMQGkG7!4%z_Fqdnc3_x~YhUFd-bgIAEh#~Qkm1%rLUouvM z4xw}&NfRFqJe5zE+6^x!k%H?(FpNG5I{iOd>mlc3l?D% za*}f%Lgrw#=lZsP-;ITil<1%?5Bv8A-kDglfP^>Tzl3yR^gw0hK4QQ=sYB})2m)0% z+=$zc!yQFv>Vy@Og;pSH4)XTE<3MpNO?B*=xmc4H4<48a1&t1g#a) z6+V@U&=DD-RW#SL#aRUR9fvK@Avf49v%Llj#pCzyr91tPPzBMu=>k~hU;AY{LX=iV zkfeu-pearB-wiZi45tXz2!ugcv}!&-sJGws3bup%4)3r+t!_3k-0=InIq`P6!{&4BVxV$ zDmLHRNO0|P8mPOKP9)6~S=@&W*2dEfmTOP`4^L+q6i3%}?Hdgm+}+)s;O_1kTmuAm zf_re6!QI{6-GWO9?(X(B&sX*R01Aq!>Y467efHXGT^kW51@){9f}u}1IFyq0A=t|= z*g?_4oJkrxM8`_Nd+kT_ir7ggwuTWQDZOD=$4N>4?`PN76&HPSx^x6+6bztCA@g-K zyp;IbAyg%nT%tjx)->iM7 z&ma;*^1LH<18xSQij10M6d*1r8W!VoYC<_%&_wUEff=bf`QJ+f8ntszX5}!9!K>>N9=NrTkD?0}RP6vD*@W zqj=Qf-)oFQ06r^`3LrlD^7?dh5zQ97pi?MfDXOSq(u-qX{$CK>Rb}@NQSl1}^b*>P zlHgeJZYtWhkt`VSh@OhOp!{ZJj3a+qtd!}jUq6Quot2CJ=>4Hc#b=xl_?-+k6T_qV z%K@CdaYEuGkF59FV`M! zg)#Oj5se~xzYN8~h#chCv0Bu#9(#3ysj_TH)fL0QIKNPPTuml8yr#j%FlYaPS{i9x zVk2|QQc^+e03#CtPA}w?Lre>1jzyryTRBOOGBR%I@gLO!BwjFQz#{8^GRQDvzlt~~ z-ue(794D2J+o9F1mBsuuv9$`L4!Rzg4xdl@mA758<%Pfft~c7BNb9c3$Vo;S1kPU6 zQpY|=dv)jiD%VMz%cKokBcUTjMA>)Nq+ef<7yQY7*>GL-%%f+Sf9i^zl6fnk1Qr zLeyo}<7M!0aB7R@{+rLqm3SEc?|0D!){ytBI}mV!$#*v7tlu8d;qW;l)$Tjyf}UD{n^^+ zW~bL^5Gnx(=CFb2(dWNhWAuhBqsX9!^wa@I^$#NU)k+iq7;ZI8=;To zUeY447IFniB_v^%9rj&ReGhOX#s@505tFFX?+0gxE4BGtU%AMYB|y2^Gu+sUbmLv0 zBnPD7>Dav)Kw}A|mZ8z0$82|zOezfMBy4>_;O65(@#D9`2agMNgrw#JE|b3$@xs@3 zdYx8GcAi}XJQ#VI5EI7^N#~ihjV`m|SZ^El`}xS-m)|P* z>sJm5^Wc%=z@tp30&b(X=AxaqIDTG)TFCP&jc= z{s0c7dMy9+zuNIC5sw#7YFr@aOnPRA+(#6m%Bi4|^Mw)cV{bL-TwDn5vg*UwAdr8(V5xX;RgF&cT0?Fk|KjzA#UCOq=DL zp_?J!)*wew&nStQVqfFM{Q4)P{mUtYkn344yl>0yk$_#t zk#NA~?c3u_sK9OE?VQdKIItA=W%}H&D7IB)l-{Ng7Jlghx8&+-D@6dMpd^bjue6BA ziGY!(3_Jh(i*~a*Bg$WIplX_m6$O9e>gwv5qUShza6(&CX#Qzqa0@nSVQ!V+&b}cy z=0OaPKu?5lI7++LhD5*F^rGmaNl->+B&T?mX4&g3@pUzO&LJ9tDN*}7Ax=Rp$_fRt zJaH6kdOPJryA}0F>`y*Ekj>pE(Rcinb0J=<^;%u+y+`+=3U7mhwrpw|%A9@hb_aw( zo=T8O@3!J-z9g#mh=jb24d;Iu{a9z3`tqL;AkJx$rKw?@?zxG*b#=zX|&N zmohDX7a`Q=GC$R~_nW-IKL~QMIF;QXW;PJY76r%@w z-+(!Ld&$32LAgPquzB7@W8^Zi+kZDxc3qCWwz;Q@0Jlk}$IJa0PTmc@2^g?dG~)cR zy5MtEm{MifY_~yi6hoSe9=Z!2LBqF%m^cP`pm2}7|BDy_h7YVbG$S+1J~e$BC62qr16n@Ix?mFNVn@E6;xLd$YrAu?dQ)bELJ ztiAaz#9A7IzrK$5tyd@{>Yt3x5|G%@x?bCQch#gNVK9L^iRT}9dVlEl!z;WM!oSTk z+<9nhvBvm=Bn(DgKv_ldt8a3xDUZ%U$dr(x8J;|m*r*)m3T-vXsE9>`^q1yOu>IU! z*cO>>8z(H|y7N{!2XVs6l8^$HrKn?WqF3cV1PNW|1uqvsm+5s0;63RkutnJ51StAm zC+rY<9_Ie>Dc>K!G~ZeRLqQi{^6!x}*q!k{9))qjH;~#!{n8bS97QbIs-19jgPd64 zhpQ_i9;8Mx5&UX9K`Sk-pd1Yp*r(JyTkH2&^I=i_N|H`2*5hIPt=`s5?!i~Zh z27xOkgs+j(`s)|`S7hv-ryadJR#L_=aBu_6ta9pzl2kBAF|ZvNu$8{#;2;Ih(lR%m zgIB?e(keL|>9*3cMX60DP*FMEhWdg2G2em5)Xi&lA$v04413rQewfUs3(zEFeD<0^ zV6(o*UXr2<-$;ObMBn%KB(fmpPwvsn8XKyvw9wNu15PJmw~zqXLfEw~$Y})~GMEjs<%RIrT8gwa97b8dLeR{;WRz>r+d{9`5-ZEtIzYE%s?GWS)`arfRcv#g$WCtCzQi{d zaw%<`AWm;RGVmw{Qlrc6datV*3|z&k_>A93cj8#}774M$b$~lejr#lhWBkOQo}j7( z`qa6OXq7W*SG-lwvY5ZdM1F4+f#^1wAx{5W#@3zs&Q8oAP1-H^M?4QDLTn&0sxJx3 zN`-7fMkt)?^|+aDPHNVlR5oK%^zd1#lqGve#%r7Jhl}4A0_Pd!&mfx%0aDBNzzKk_ za$aXP?z<*)eI&8gs2z-#8wUHZ64S}p6`_-EI6PVkJ2kxK;L?PyZs0KEe%kzc-r{DG zm8oPnesy?q^0G2AGO~4NS_72<865fqCj?{knNNn2j;}aLN~^x5QnUM)2`VwBNYB-P z@^@P2U`H^9>u(Y+LS)4pVJ4RPp}A7wUOlTkwq(6XtP-qdRuLxr`|@5}wUl%;D0i21 zOP&IS406ZPwn60)M^($}O^-IIWiMH`s`IpNyvT*)Fd)Sg=;RCV3JyPYCgY8J&(6_| zF{QCn!nyaCS|~@djj1D>JC+5ZrYQaKso)5Ipar#GqslBkkRC=Phk5{5jqI6>I80kw zbm$w504KZIMP2(hcRF&;?Id5j@37a}qoI=G`70c|S8+iZAfh}&s|qO+#+NrT6TFQR z{mA_f6{DY};_U+QHK+BcG1(kFC3PcOvm+Las@L71x+}>3#dk9W{NWSIN{Re!{>e{u zzzfg>PaXe4opB%RGWlLbBJCce){4uvhi<`2ETlQ!O5gsdXH$l>nI|DtKb{Tyz-A!l zoWw??t#})(L||ykN}mg3gs^||;da`eAj$&hFoT{pP$k9)mzL_^7n{fEx+)|sgUASMo8eOi@JG%D z2s`9@DJ6`U4TYnA{}y=kNAmu=b0#ggV?MU(&A(v@J)j~_sv1vxUwVVI2=(P-slS4Q zqRs(P`ItIQKrY3j&LFEWml`j04mPX}X&Sn#GIjr@y)%yAoF);rrp#Z?I4C1EW!B&# z1y9BhIwujU1gZb>U*Gs?wn+@X#%H7Z<9b0=(I>Z2b2ASQCHkoSbjtU`6EWOTi)a#N z)EIttSBl*{t#?5+InxXEC;`bspTr8kDRR-32Y^mxyl<^ofZp_`p>=9TmB`;Zz*l}W zZ*<-K4MZYbWF^e1dd0xam+kP2M5)>|;!;b+YyO2Em|ZZSdO$xnh)ufb{2)p{iXP{$ zRV1d70C!MN-8p_V-yAUF`4diIF>}$y_En#y<9-eH=#*foaMSI-ReK1 zp|n+^hCJg=s%z&`u!l8op{YO8>thc5FI#4(wLEBo3wa!5BhFoIw~u0UhD z`u*JM;MwmYDc6>SY;<*qxlb(7InDPB8$m^+SnH{t=%z3JiObLa;e@ z365ig;B*|$9YH~7NY_z64nHQ~HsqJ3xvBl(y_Ty#TKvZ5+5`4Q?-Jn7ovvpS2=!8m zZyR-qtoVJr`Nesu14m!FDIR)zkg9ty~mNvpJqYpXdPj6Y2okN8)_&n{EF+ zg}6Y9b>M}hWne8#1*ksr;(IO2YT?RI#0iowkV1I9m;M%}N%^lJLE)KlW=npdFBgSX z-1KWHq~qfK>E*T}OS&09;$YKA6ft@CdH4tF8wo_)*jOlL0;9(WRnlk;&WfhO{y4`v z_jvC;<-f|))7qbFpC{4K%kw{|4D{e$?_kGi{)hF%A}dWarF2Dn@c0GgITkGbrg*GQ zT`rF?I)T?R6>|0+UL`o?K=J<_D;i1C4?vSu!BIW^y0al5m=`PPiG zRk|`YXT_ooXG=6n)oJzkc?;EP#7{#8N>lIWp!#6bz$5Sz<84XLX);FF%hd?m_1(Bg z`@sfM&hM=%@!?H3Xi{m1Gvz#LMg-SVgl0x%4!eqM9hvN5Q@-Eapa=!a==G&}VR4vi z^9+9Wl?^jw`!H1Sb=zKHOc>pk_45%m7R|MJsKB$A7)Tl4QzHzy5Ff>iY$}3A&Z61T z5I3Wskx5js&6{}H$>D2Gl1mP5x1 z-(Jf8-Bvg#zn{;~&%RE#vyy(hpBdw*yD>V04Vl*c$!|_0{V~_;)V|fDnu*^_ z6~8Msc5*iIEAPxo61}Ye7Q?3%Am2o_Vc-2W@cwe~LFLyDAg1ou?7IbVjzhj0y-^u> zUx_Q1s(E9YP0h^2Vq;@}H#IHbpp5hC8Nya(Ov%74j+S_D_{eyDTBlQp9}wi)-+p?4 z?VtJhfecAHzE}t!c{gkl6=$;AW-PZXvKtvvyEH)*$&y5ERC#wnR*3$I)wl&D<;ng~ z#td449m@F8S{WiDFl2*6Qm09qg$WUiM1xK=T>?*Z3OrVk!0m;u-Rn+WmY~;n29lpL zCKV>`S3Z2cn=6}QGGN*&HncD7_EdTTBHykaCZ_j3y?*f7-q)j~Y_4oMFp@u@Pk}oJ zYu)1@%c5D3EACWV{ujxQdm46RWR^Sv-H0I2c_+Mm{Fob;6=eFHR6sPo1kKie?tHno z3MH_EDIlFGs%*WGdX$mWItFDV+?gy=U1261_$0a&^wN@D&XEf&+&+Z(vxCbA!zbMQ zKiXonIZ?)~a-Ff_)o~KBrPwrOsb-2lA7J6Xn?j9A$ss{50w~>={J}^pqJ@NBOxtU8 zl9kC{pDAc8>&l(`14?vP)V?p@Zj#*NtA|)1(@dZ)SB5p`Z6`Q_WGISoJx9M;QQ9MF zxTKRcrAEMC5-@<{&E~MA@kl`t5aTD(?$xgPZQC&54Iz)b3eeWS;8-xP3gVR$q)B5F zMu)HO%86UWqAbI4N_p(cf(qNr2YCZGf%W_6x&9}Uljv~I9iH)E2e%iI)#BZ3mJ-`mTk1Gd)q(iC;&{FgemWc7F0p zSPEzA$ZcHRQ-S_+(czkkph!#FMtDccLxZ7!rHy!R*hY~#VyrWMyK0iPyImbD>lV>S ziAttdRgpFC89gt`zhl#MV$B`ZU_8i0A`t%JKtl=}Hp4Dv(s%8~Gzh?pPXPGM(gFZ% za?S=*ztohhW#5uR7HI(@!d>Nm-s!lKcg$ws5JW;t5M*dZ*69#Q2J;g4jRTTp& z^8%3bA#f=RhZ_%0q!OZ}U}$IAyKd8e_)H4@g{-BE+A9*fNym;_9O#>^E{pa~8Y&7@DY9)kF^F%$?UM==$owfaA#F>X_Zq=~@lDrq zGM&Tb3D}G^GZtWQ3Bn;cv6-akyYkpPY`Asjnb9v9S{(>h--tLNE#RUWwy`YIF*m2R z#<`Fnaz(L5?0~e!Vl=9VrzEJA2fx2{755mK1&nCkmJ+Muwp~F74qTMdW_U3Mdb>xS zweH@n&0+-^Y{|XgdCEa6$^^Wo8B0G}^iuUj@lREEPfT|Q=J@?O6Z}b8NQT)1BgUF5 zcxWQeAPzsS^8*I=>)g~sXC)go{i0=NqUj>)f8~Bp@Pr+Y_^d`swf!aNN7uTm9w?7j zzb$-pP|~;nWAg#lkmF53ssg8Irc<68EkDZeRk`l;o&+lhnNrWDm~6!OB6vSUIvA&W zc@ObaK(9k(PObZ#!(cKRG^Su&J!lmcRy#2IAC*88WLSqz%+0*T`CXFqO}iXkM}Q}m z4$kkKmGvc@va%rc*5Yqt+w;W^`<$xbqT$J&^s-z~3d2Y;e{(rT>=FQT1R zjtRbO%ModCIqZbLyYiw_z4}_B`|OiP@2Fo?IdTxKpLfkk=L78 zzcYZkF8B!&IX_(Kfqy@yM%XtWGw?cd0XdyNc|%QVE!viE--_u3sC%nggvbA!$5a39 ztG(Svl~T{gnHW^qOFcynsa>DUhSmyprq{A&3=>5B2M!n02>c(hX$yHNWWcy#J=7eB z*%Ubnhr#J%CmiJzuP9HlWqxBWqlh^HGYHNs&pMzSRu3!v3{RBmYf(s;PLB=^j_;wJ zHPU-iIi+)DLCfNz*!$fcliw3?MLOOTF{ftGaBaHhe|uPWLK@MWQC?VAwGB^HB;WI0 zpb4NSCL4kk<%qn>ch0pU?%a(Px00{$(LjA;G$yW^ zakxxAs`+RT7!ko0mg`KK)tzx#toE=4Y}OOk=`kQ#W)n=|^mra-qki}!MQEe{Y(mhA zB+`X!-W_IJx#PCw4(4^ef+)DMiJoUdfBO!9+pKMp6lHv*D+CUd1rR|t>H1{FJQyqp=N zouel|LMtv?sfwZ|oBJOwX+obcIi9VxI@KY_<4%iKz{dUl{zT=T8^|?mpL1R@ zLfY0fxgqQigvSEgREw}h1>UMh5s}>ABf-qeCL(EqOwJ16C}Zw})$Qo>sf~wK1qay; zn+i(B&74PbcWAI#cwRbfBg|_+(3>q&l2tECu$X> zG^wMyvN5?;6~6#y(u`U}8F=?1(xq3Jo^njL61^(_YpApbzVd*oo}8TR-CS_f{SSj! zeUtHsb&!m=iKSHm5CktVp6BerS*tjktOh}(m|^F_=n0)^3LgAs6O3ZU;m|KOMVJ2a zWL60YdR@Q0yF=V`o|pgF`J}qn{r+Nb8IiaAm*#Ta^Q7|6)6D^rXKpgA`IsH@3c|b& z*wVBQ8~iPGbY*!L6t)!x)~$iET#h!HPSH^E!lIDkLC@A(2&j{oT2^ucSCEpYNk%Tn z`gp@U>jBC}zUqXlr76pZ*eG@yJKFvDg7Bc$5hlMM zS>vbU^%!5#0-JG0?lZV)Q-qD1J6?4(t37=j98XhZ`Yxe`9do-BoKa`h%hOKsWB-t) zhbPlRdDfzrEwyb|*RIitMde-F((zFHZS_&lp{D?Lk?jMo>(isi#oZGpYl4vRabg~v zRnDxooF&JoWuWsRPIk*F9l1G=)%Vdev%`m~jg}_^28a~^Ej8W!vT-3$&72H%qUxRs zb>c7;el%bF;o#tK*@Ni8geHn={AOK&($h8ntj%uWrLh)-LwejsHz-TuI4m-3%1VAI zEjx5!@^0#o==OGRG`Fs&Bacho6~Zo9SBF^Qw(P+R`u?zHFZjMq^u7%*5(bi`0ilW@ z0QT&mx3{;WnHeQcr+qoqN{urB5`QX+#BXZoz72)RsJDEI`m_aovX^Q4Az69i2yi!F zK!Cnz!D6#WUkj8>JU-9Pab{^zc~Y<>0kzc(K(p|8u={n7>22$70B_Mf+6e8oekY}S z$kl$C$R;%0tcDS%BBg-02w$@kr9u)hPe-au()~Gpy7s!r{)wC@=*A@85|_ImUaS7< zWX+dRC)>SHMf)2oE34>ZMoG@!ZW;vhULv5P*1QEmcvm(-$aQ#uD^ZcN>+Mk-ANQ+2 zxOZGA!iwc0QquMlD1Ns&B?3Tt(;#cLfQjqi;^WNs_53igZ4u#AQ*vCHw7l8DT?59H z*85eP2f@>te$2bu@17I6p@h#NpGF3xW>jrQ#d>w!Wf~Dt3iz=5wUjKTXUvuXuDh!7 zs2LYZpB9$P5K9=jU8!f*FtTH}^`Q%aCq*rlZWuHT_D&h9xYpi*rzvtg{@=A{p|5(o zF*g@C2h-iVfTGM_dGJN~Qd1Enp5c+~x8VEZHpEx%zcY3QqB3il${a0fbBaU7$pZaM zp$Mbo?nFGf@$9UUD>Ar666$JJ%jmXQF+Z9yF0yG*#!x6HiW@(;uXmbZJzYv8aN6b(zjFHTe2b=y7&H2Kv zfqPrihgiMa8_6W%WwL^=q#f1SJh34cm?4yGYx9z8M(;!j49}h{r-D9$IL!D1FgrDMpxpM0z~18ULZP*NIgMfWjknjIPBAX8 zOFyccC#Dq}b@V+?Ylktf-JqDHjhNxzAxVO~0FU*%-`XI8u|y=%2(s{l^MFjbJS#XNI=bU=uGZ2h>P&S(5z6h(EZU zXgLreenQSJvy3!FhrhOKNL3>5c(bo|)`tQOHHIa9w~e)Lgs_1+{*+u(Mucy`b1R z2DjxQh^#uBMwSs|nuCwFDD?Gm_N>YDqU#s0iy8uLY}atDv;4H`v-INkXd}{Sj@4;k zn+(zg{PDXjngk92EzQ0a=wx~O0XC|OqgxKvth@?gZ0Sh}?6{{YGS#gPt{1&$@qs3L zp`kH4UU6O_3mo3F4mql5a+mqb2TgP_fpVTXio|(KGOOFlToo+7t2?Ip)82318dc6& z-G*L64#<#9-e&-gg$UqG7iXV8Lctqc*~S|c1jq*h^vG7n5t7gj7_myn1XtnXXcBLb zh(FpG7rGteB)$l(K`~^^dyIaE3X*oXCr_cRtif#$RA;rBoOu7k-~>N%KE9XWBTTn$ zx#gp$@W~*8_X<=i*lkkqB*gNFlFm@wn>XyY0@oDs^#`@q|@YYsvXF^)%LcA<45bpRm@NG4N~UI z_|X;5!7Ts<^Q(4ZJm##GM0GbK!1|@YaIOBWZm0D_B<}^5P1G>QvZUy1?e!jh#DuF| zR|CtgFU6>qO8jC0Cq+F?otn|^<~R5K^y(ewki^voO_#PCucF312Ox6pTLV1!&~h)C z;fKrs-$2LyCx46<^XVH&tt>yFIqc9Cy_MaNI3>P};(MuDuQ z&o;29A9wQfCEod)iPjEmOaCAVJpP^SbnldUNND4oGc`qZZNK-NXtFmeS}0fv6MPp& zH9w+Vu>?0#y=OjQe$mnqEg{3bgG8YQDOVQc7jTT0HLkn#<<|Vex$rS36$%DNb1TMe z?Em_0{R)7IUT8L)(Wb%#hiWr3{tp^jsA45A`)0vcO#$*Oi0$eO`=NM_H5Y37_yDx1`-U3?Q3_I$^tI}>KZrNp* zP$|9WB+9g1WdZ>6p zQSEEmbos~m+l0eBN*nal--^D1993~li&QDq82sAf^&KZ2&wQ((yNAg50OvF0?)Dc0 zYr&_l;gzVn-6=ZVM3pFA59Y<*|Ik3G-9RCY;Pwg*l|djb)%3j&-=PN#^z;d*T9lRV zW0JL#hoPNPMw5?moW*lB-KxYbF#0u;sYpy?HZQy=tsgCP&-GAFX%ty+1Zm%K&`huB zq^S06M=c@UV8uCnp2)JXxD8-%ztkU0g9KOSg{y}FeOe!wiqqY>h@G&OAZT^o!IBt@ zZ5D1jdLf9L1WSL!%@xJ|7R2g(o|7{8G{Q7E0v%Xkz$0m+}>m)ZaWg_g#U#ao zgpz{XsuC%iyfGdmHuyY3DF0trrsB8Ro zDB*6}yy7)ST2@}-wZZ)}h4N=^iSl4;QYRwFntpx9~Z5xR|boYoh9_o+jz!QX$d7s=oeaM zgPCAlcu6UwI97Dku#AGvru@|DO1qf7hjww0j?dhCXA(5g)1)$4Y`iKwDc3?``435OxyiyQ2a#+BZ*Q$@2wW@9i1y-O`=TMIScxBQ?c*LS{IpX8nZ zig~>{Yu@r*|0EJrdU2csE(E;l=hp+lk9l(GMURFv@Dm16mh2R(pnmmS)xPB^Hhrey`R zL4cXFyB*k9&~5zY@rwrfsvbYfO$(BC=dHhI8KNz+W#aW!i%WC{o{54u{tW9f@*y=( zp1|%bL`tX9=W2{8(cWdJqbKZ~rNl$?;_bVEzF}!p-l_znP_Qm}^eOr(=q&vUfM?{k zyy$%Jtqm1rT%Mq=Gll0h2Y!E_+i=@@i@R5{J(q9f%RYvKhRvf`2Rt(gs_8}I(C$(rVv%<)DRq2uevB? zwOMItEYfqW_fOuXFs7vC?68CGvgm`*1Zct-Gli?2%E{k2j!uM&%_8!PY^USuolLX~ z6Y-LyUKAs4hcQrod%5o`5;h~O0_$k|TGz|DSJF+{EmO>^JuvDlrkc^P>y_b*(Do&u z(2Q@bUIZeyqlHgc)zl!Ge=23%%C1>I5QXe;>w>w7|5dag4`~uPSAC!l1rj9y*$hI> zx}7zd6Nb7Nwy%J_K`3&-!fmh@{j_uIAMpOCh3x3fB zm97ggx)d?I>hbJu<gcM$`aD zAhY>9A7BZ#uo;|h5dgIz6@agKbYEP{z_<4|&ZNMrzaCnXhAkz({U;G0F?)EnHK1Ve zhDHH4Pch&cUlcN%*q|d}Ele@KB&S|YSdiTYY6pVbvdpn`NuSCVoi+eLGK2f1FhMeL7% zv+v(hI2*=}R2%x1r!4x25J!({N?90`?htbRtd)}MJQy0NYQ0P>Zibp=nJw-(RX^>p zH%HO{FHCMahGIrxd<=9B=)}_f1poemsLBE+gO&ne=(pqOw@Co`)AwnUmi-*p0eeQW ze79EhBkXl4Xb#522R3{Zi(-yMUiHE)GBDoNxs)7KN7vJOff4J1X4i(n~Wk>!Tso?r?FfO|po^Km=$ zqwU~}N{#ksA(#Ehy-op!aWtbI5@}}okXcf64r8_bYQY!EAL942on9-N2xw7I8RO4K zK9;VJt0lDISv1-&N=REu1BNp_RT(5p98&M0y+|ELPg-RA{l3Z8mc`W_>r-~yxqVM3 zZoSH2HIwoFoTS7LAk7ge^Sw`j1@C|fU|V2aqYfF78b3Z+iV{V+425IOcIEoQ$BQC(z8GRvc{zfZ(ec@b}bf&MdVSn z1>VXuogST`a>=i}N;Z6KhfHrea*6QqqX34@!|VH}{cAMPVXE}g@}94}kceVi-I`gV zP=VH$@;PjGOs!t6uFpf=KBHAwl7YCYv~U;f_v!?aj<~McRzqdm`>a1 zkg$uKV20%FCy`Dvi)L`@v-cgo$@Bk8H|g0EZ*5-vDAAvVXPFS+t<4rEg-vahIHkX2 zUZCQ$sM?4n$v*`7xv_%%1HsxL_f-UdwW-Y z;(sU`%bs}Ikq_zTgt;&sicTAW=O>2WucoZnuONXR3{E5nG3!g?VjN&mCJ5`rtD&7e zFn{rLhOgojyHQGy#4wJWMA&Sd{OIj-e?G2U-2&bjcF5--&b0(bzy|ccw?^al;O6%b ztWtXGTfx`1e8ogRD^Qbm3v(`p3NYuLi( zHPTr@*z-%a)cV3Gixj*OPcw_vLn{Jh1l@Rk=$sQ&&wz8X`*Sv{fXmy^Jb)m77X0Kk zOEyM^H1BTAvEOY`H5i^|ZXF<2h>;`j`o3^h8^zR5kuR!);&hO5*sXD3QBCth=-r$W z@hE>nt5(iveH*D;vlEYQB$)L1TQ_f6oW9aNHx7qdb|ntKif0xx%D(CLFPv(Vhvth? zjrOPfmF)-&;ahk2PBByA|A6DBHMu>OjY2l;%xvN@FqcHHG1U`dm(Grxjfs&EX zuVypyvNZnV^a1}-VTc@rh?AHI(`RJKIx-DI{@!Ui&BsS%iO-n$4@6hqNmT%Q1L}wa zr7{Z33dGdHegJS0-~Lai#Gpp&sLPUm^WR9r*MCq^IR1+lOy_R9(~hYSnFQ%zAgUHq0~MmYGSR?-CBBR(3Mu2YJHrOW#a$)dNa)=BL(NF z5O=FOxw3!o3+uOi>MJ^@jwnQRuF~&~fxSo;^3QQni1dp7bq|F!9gW{3OF*wwAmZ-tqf^_AKS3y!Xrh9`M~*Q?bG` ziN7K$mxT=l44ungnZzo={K-(_SIsR;`MG9YCn0LgR#`2x})qxD;+qNOnM zOcKgWdS$`(Z1efL>%sM#;!z49WkVny`AsBs}*|3TN$D__x1>3b4^d{`-yP5S;; zhHpYa77(0J?;co0GBjNhJ^|30=Bn2qSj#@Us903#g&f>|&f;vzr!c6^zqeep*njl# z>Qh8`WeNI)R0>(Xhf5lem&uVxJxhhW_jFQa{_@^dmZQv75m}UY-HLXD(#7ekymT&? zRgiu31D1S2Hz0?Pb^H1Y4sM8g)eA!o4)88?Isp$N-Om}vPj#?oDt1{)brFq=*MYr8 z@8&rJi^5QHQHT|qZ$}&o=3R$^BkVHPtSLFR(H`?nrlA6p{DC$&E^t(pNIR?R ztti+81ZI|&hcSG`c6GnsPGn@ceejyz4yIXc-g9sFBC){wAh}n(!rMI$Q`6PL1A~a= zNwA5d(4dLBLMbe=WM0=3s};Zhb6k|j%$B|j^t}y-FWk&u)p{E2IF09xs z#amjc5!(&IhN&zqMPoL;AOA(q#)?tiuA%nlv1Bt(hHO<&ayNM;Z{SYm*wcD#I?x$e z>DqcE=F*eNkcpLs)O2F0Ojk?VQV7%J8_}z8Opm?z_L&sFGpVr1XscP8qB2N!a1k#5 zVrLZ)bv0>7PhxX5=wrS`iTqTk?YnNWE^c=T>8O{n#pe`z3mo^7GzGW;=U_}TW&2MT zbFP3Z501r9fkqentIK)%YN{w^p0grWlsvOTS-DK<5r+kFiS8(e=%}MVQJ_vc%{t=T zU1Ca6SLOUPyuo#t29e)PT&)(O6Wo`~avzwG!hsUBuxjBo;A!D)o*Rg9(xVeX@x0PB zNy}Ktmxmt@)l%P%KRUBHU@RXc{yfU0unVr>`Alj&$=QI-9*uTK-P=A#$xN2>?uVO>C3rB_#ePgaP-;lIwPL9{&XJVsh5WRM^wD zx38~{jkvq)6*yPC1(Yx|l$0($lfSX~hf+)iP}Nzx0!OeuV${{p>KkHCM__rYQl5?V*?e5S@e56?;L^KnL=% z9uUI=o&m#M^Oqk!xVn=*YY&A(6dDH1OPKCC(5}9QAbk zaIOp(HT76*X=_L-`|jYBrSXDmP}u^d8fs*pN`ibKKaVf7fKdoH<$O9c994xq4F6kJ z=w&*Tr2LrIeL38=Wdu7;DQAH{1qF$;byrMb92glB-`LoAwJCR^n(o^Gub55el-B;k z)Tafqu}=&0we_>128#ZKAH@X%+DU=NkV(6PyR4y0J~z|Ls<&l@Vox|!1@!w~u0>1! zcC12t8ShYHF0L_dmJ0?BQYjg4^_=K_Uf{n+2r}oB14MNvi+-_v=4tF<~`z8C1(~ z42qEYR4l(sBJMj-TH@_mPVugo%<(6Zp`Evqvx8tMMNyK)W3zd}is`8}bK`WiQHY*c3Yhpc1T z;p8Ly-vXo9es7q2Ef%pvhDvt&Co*0Jh?80m8AGJJWOP2TRgPf6^_^To*9}ZST^uwX z7zp{bP|$K{{qdU3@U%+}G#`~24TdbGlpZ#Po+tVD@k-;@a{7@|Z#%Km*o7un++fXE z+1L-r+Us8`8=D#Q+Zu|8)s&bC2Z4nTl=aZ{(CGu1HQhqiWGRO>x(jeZ26+wtkqlXL zrN?AWCroxuc4ryv3A$~-&Y`O%NpMRgCVToKq@hDrDMXPQ!!2@3K_(XLl*bHvA3jNT zbbuWEvD9HI`gx8zrwn#D4mm8#m)}8x{Y_uIku?>xPy{D69P#F)5Bc(ZwASNwBth8U z8E9Bp6L)z1T3%7J-xrf$%&5&sT$Eof{9RcRUkzGs=VUGX03N6QW<3NAwL1>r)C>^( z2AE)WMgW=qV~QAN(Ic_-fthfOSIkc9=gEWu+d^p=MUaByh0;Vbv?1|N3YZ<2w8l;e zm{$RP^e-(H{;J9Gaj{>WuJ7x)f1U_{8$$0Ka*bImw87$=P`OvCFAX;!WsfXCAl+*Z zkmIlw*-koe@{m|%-Wqg?%rUe4M9!B4yd~pE9?!@2LNuagA-sq|i<3%iXWl;9c;kQu zectnVWmC`qMWP?ddH2Wr@YeHUvx3L{ip7I<5?oYsC{Er;=G{17X%MI;V`yHOeB?|$ zmgXE_g9bJ;vT?|T*U@z0Nmj=-g*fZ73krLt(sL1vd7!w-*rXbIBDx_|?vIk+(2A8d zn80}^+VL@%LGZrG%mB??rV>1cvT} zgee%rS7S5pmWJaoQ-bc3kkM*sVi_&X2Wf$o$e7E&p}(mNuH=SC`73K)pSLU~Qm--q zA+%E+X7hMxQ^rt=ijFH1pFs2&MZl)Iq=)Ph{K>eyuU3!%~91|=}jye1(p ztzA(EVMch@UZ(>`&3^<>i4?UUXXy|D-Zj6AyL;S1jz$M;2HL2a2r2>JuU8QVZBs!% zVpyZ}Z=M1CSA(AsJv=8AT%h(PmKl)qBug>yJkni-YILGa8fzP_Ojf`ny9|Jyj13q4 z9=*T#$Kl>-Iv#TM0eM#CX+WnmsF`o-UO}bWCr{A^$248#FjW`jl*0s92e(c6(kV?? zc00k#0jy8k*szeb2Q!p#^jJ%IihYjP*a$`E(WjsNtkW&c#1=v&FzNU3G=GMA%;vaS zxhHD9HADRWv2+$}QGM^%|B8TgH`3kR(%sz+Lzkp9QbTulcS%bt-5}ivNOw0pXMX?Z z4RA3tXP~; z8x%wgIJB_r)joSHnc_ke)z3Firb~B@&n%%*@sC>X6Z!Uev)ym3sgFI9pqZ=ly4(>T zW3H4MkZ^f2Wfdmco&5<@oU1OukL&YuD{Ic;L=dIFxw2PaaO^RfpHt(1Vv@V_u1mXF z#%0@<@!_yCyJOl{T;Hd-Co{?6D?hV>%<|CY?fzBL{CqAdGf8Fnjh{o3Q%B&;{L6h! z%!lZ(dC{n27sxT8Feb((GGOCF0i5hj}Tf?`_ZGJK)~Z$dLP3?ft+ zooQXeFwBunQ(X>6Q|v9Cf1}KX1L)X8?@lJvM#0B@V78lE@cS17`hX5QcA{+Xc>6_n zG^kProud^rX_0q5kyjHzmxa*__cBU~EtI8|Qq`~eE3+vUh1yEd_dRq!Nw5{#g0g@W zBxjaFebTGbB6b1s(Ha4T54-Hs?OmI66s0hrXsN7J%w znRT27aAP;_`ZJY$Z0Jv$Pdd>@pnh(-3tXNZM&?)h!dT^uhq+mtEn8-S#ZJrZ(OT4)T8DxlSurEB>17Md;Ca*_Peyq3bbD>LvJnc(a%g!x zgJ^$=$+#QT=x84h)j~js+b;>--v5?C9NJ%TT+$zKnnrqqwODN^Fh6{l}vQ)s<{ z`AD1YW54m9B0b%2n z6H}SP8_Lo3vie>D$8O^p!CdzT9b}rKmHNg_Wfo%bO<9#TS)E4AoMk;L1EBT4=b+i1s*srq=Fcj6wf0yA3pR_wk z8de+aB>wNi!ui0Sl;_>nkmv6O8QKsR=+PM(6HW6FDJsfgYAY*ALfLIft~PL((laZ$ zAIhe9H6jV?J*~}{_ayQ`{Sa}-B4i_Ib}nV4!peswD-4sxdA}L(-caXvYS+33%!!=r zI0^JUt_b+=WenjN)G+8SPjfAJ`{h2tMR45up_di@E9(~plJE6 zolCIWFZ@Mn-PX8$+t)_kYKTDR2SRkRzprU?oP*F6d<*Y|U~=>?-e6b!z~s4lS|W74 z&kO|z~20!jk#zhYT4

M;Ig0xsnp{B?j30@}Tazxg}1i|Tbn@eJK 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 E0YnYs?f?J) literal 0 HcmV?d00001 diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 596d9c260..f4e28ca2a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -1,54 +1,6 @@ -//! Dioxus Desktop Renderer -//! -//! Render the Dioxus VirtualDom using the platform's native WebView implementation. -//! -//! # Desktop -//! -//! One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. -//! -//! Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. -//! -//! -//! ## Getting Set up -//! -//! Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project: -//! -//! ```shell -//! $ cargo new --bin demo -//! $ cd app -//! ``` -//! -//! Add Dioxus with the `desktop` feature: -//! -//! ```shell -//! $ cargo add dioxus --features desktop -//! ``` -//! -//! Edit your `main.rs`: -//! -//! ```rust -//! // main.rs -//! use dioxus::prelude::*; -//! -//! fn main() { -//! dioxus::desktop::launch(app); -//! } -//! -//! fn app(cx: Scope) -> Element { -//! cx.render(rsx!{ -//! div { -//! "hello world!" -//! } -//! }) -//! } -//! ``` -//! -//! -//! To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/). -//! -//! ## Future Steps -//! -//! Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! +#![doc = include_str!("readme.md")] +#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] +#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] pub mod cfg; pub mod desktop_context; @@ -152,7 +104,7 @@ pub fn launch_with_props( props: P, builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig, ) { - let mut cfg = DesktopConfig::default(); + let mut cfg = DesktopConfig::default().with_default_icon(); builder(&mut cfg); let event_loop = EventLoop::with_user_event(); diff --git a/packages/desktop/src/readme.md b/packages/desktop/src/readme.md new file mode 100644 index 000000000..dc034fe67 --- /dev/null +++ b/packages/desktop/src/readme.md @@ -0,0 +1,51 @@ +Dioxus Desktop Renderer + +Render the Dioxus VirtualDom using the platform's native WebView implementation. + +# Desktop + +One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. + +Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. + + +## Getting Set up + +Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project: + +```shell +$ cargo new --bin demo +$ cd app +``` + +Add Dioxus with the `desktop` feature: + +```shell +$ cargo add dioxus --features desktop +``` + +Edit your `main.rs`: + +```rust +// main.rs +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx!{ + div { + "hello world!" + } + }) +} +``` + + +To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/). + +## Future Steps + +Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! \ No newline at end of file From 21b436b7bfac41894d01066abcdb8734cc55b5af Mon Sep 17 00:00:00 2001 From: Aster Date: Sun, 13 Feb 2022 01:34:41 +0800 Subject: [PATCH 198/256] Remove image at runtime --- packages/desktop/Cargo.toml | 2 +- packages/desktop/src/cfg.rs | 36 +++++++++++++++++++------- packages/desktop/src/default_icon.bin | Bin 0 -> 846400 bytes 3 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 packages/desktop/src/default_icon.bin diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index fcb3a19e9..805c46f3b 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -32,7 +32,6 @@ dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } -image = "0.24.0" [features] @@ -42,3 +41,4 @@ tokio_runtime = ["tokio"] [dev-dependencies] dioxus-hooks = { path = "../hooks" } +image = "0.24.0" \ No newline at end of file diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index a177c8a4b..84786a133 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,6 +1,3 @@ -use image::io::Reader as ImageReader; -use image::ImageFormat; -use std::io::Cursor; use wry::application::window::Icon; use wry::{ application::{ @@ -91,12 +88,8 @@ impl DesktopConfig { impl DesktopConfig { pub(crate) fn with_default_icon(mut self) -> Self { - let png: &[u8] = include_bytes!("default_icon.png"); - let mut reader = ImageReader::new(Cursor::new(png)); - reader.set_format(ImageFormat::Png); - let icon = reader.decode().expect("image parse failed"); - let rgba = Icon::from_rgba(icon.as_bytes().to_owned(), icon.width(), icon.height()) - .expect("image parse failed"); + let bin: &[u8] = include_bytes!("default_icon.bin"); + let rgba = Icon::from_rgba(bin.to_owned(), 460, 460).expect("image parse failed"); self.window.window.window_icon = Some(rgba); self } @@ -107,3 +100,28 @@ impl Default for DesktopConfig { Self::new() } } + +// dirty trick, avoid introducing `image` at runtime +// TODO: use serde when `Icon` impl serde +#[test] +#[ignore] +fn prepare_default_icon() { + use image::io::Reader as ImageReader; + use image::ImageFormat; + use std::fs::File; + use std::io::Cursor; + use std::io::Write; + use std::path::PathBuf; + let png: &[u8] = include_bytes!("default_icon.png"); + let mut reader = ImageReader::new(Cursor::new(png)); + reader.set_format(ImageFormat::Png); + let icon = reader.decode().unwrap(); + let bin = PathBuf::from(file!()) + .parent() + .unwrap() + .join("default_icon.bin"); + println!("{:?}", bin); + let mut file = File::create(bin).unwrap(); + file.write_all(icon.as_bytes()).unwrap(); + println!("({}, {})", icon.width(), icon.height()) +} diff --git a/packages/desktop/src/default_icon.bin b/packages/desktop/src/default_icon.bin new file mode 100644 index 0000000000000000000000000000000000000000..2d4898f0cabc546d8fd1dc1127f8fe8e648f5aaa GIT binary patch literal 846400 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 V2#A0Ph=2%)fCz|y2-GKme*uRv2jc($ literal 0 HcmV?d00001 From ab99bc6c8d44d0788a124f41d67e45ff254f16f6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:35:07 -0500 Subject: [PATCH 199/256] chore: comment out image generator when not in use --- packages/desktop/Cargo.toml | 2 +- packages/desktop/src/cfg.rs | 47 ++++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 805c46f3b..0d7c13e3f 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -41,4 +41,4 @@ tokio_runtime = ["tokio"] [dev-dependencies] dioxus-hooks = { path = "../hooks" } -image = "0.24.0" \ No newline at end of file +# image = "0.24.0" # enable this when generating a new desktop image diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 84786a133..49a26ae78 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -103,25 +103,28 @@ impl Default for DesktopConfig { // dirty trick, avoid introducing `image` at runtime // TODO: use serde when `Icon` impl serde -#[test] -#[ignore] -fn prepare_default_icon() { - use image::io::Reader as ImageReader; - use image::ImageFormat; - use std::fs::File; - use std::io::Cursor; - use std::io::Write; - use std::path::PathBuf; - let png: &[u8] = include_bytes!("default_icon.png"); - let mut reader = ImageReader::new(Cursor::new(png)); - reader.set_format(ImageFormat::Png); - let icon = reader.decode().unwrap(); - let bin = PathBuf::from(file!()) - .parent() - .unwrap() - .join("default_icon.bin"); - println!("{:?}", bin); - let mut file = File::create(bin).unwrap(); - file.write_all(icon.as_bytes()).unwrap(); - println!("({}, {})", icon.width(), icon.height()) -} +// +// This function should only be enabled when generating new icons. +// +// #[test] +// #[ignore] +// fn prepare_default_icon() { +// use image::io::Reader as ImageReader; +// use image::ImageFormat; +// use std::fs::File; +// use std::io::Cursor; +// use std::io::Write; +// use std::path::PathBuf; +// let png: &[u8] = include_bytes!("default_icon.png"); +// let mut reader = ImageReader::new(Cursor::new(png)); +// reader.set_format(ImageFormat::Png); +// let icon = reader.decode().unwrap(); +// let bin = PathBuf::from(file!()) +// .parent() +// .unwrap() +// .join("default_icon.bin"); +// println!("{:?}", bin); +// let mut file = File::create(bin).unwrap(); +// file.write_all(icon.as_bytes()).unwrap(); +// println!("({}, {})", icon.width(), icon.height()) +// } From e06f6dc0305d90b4a3dc53ec876e879612c5dd79 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:37:39 -0500 Subject: [PATCH 200/256] fix: docs deploys should only be done on master commits --- .github/workflows/docs.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f0d2a4b80..0e19ecaec 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,7 +5,6 @@ on: paths: - docs/** - .github/workflows/docs.yml - pull_request: branches: - master From 542b48a59cb57aec0813f41cb54928199a7c62d9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:40:25 -0500 Subject: [PATCH 201/256] ci: disable on draft prs --- .github/workflows/macos.yml | 3 +++ .github/workflows/main.yml | 4 ++++ .github/workflows/windows.yml | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 7003b3925..16e306f6c 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -10,10 +10,13 @@ on: - lib.rs - Cargo.toml pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - master jobs: + JOB: + if: github.event.pull_request.draft == false test: name: Test Suite runs-on: macos-latest diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 029e50e88..d4ebdd48e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,10 +10,14 @@ on: - lib.rs - Cargo.toml pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - master jobs: + JOB: + if: github.event.pull_request.draft == false + check: name: Check runs-on: ubuntu-latest diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index cee707dde..41e98f9ee 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -10,10 +10,14 @@ on: - lib.rs - Cargo.toml pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - master jobs: + JOB: + if: github.event.pull_request.draft == false + test: runs-on: windows-latest name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }}) From 8d2e23bde626eaed6e5b13384cc7ac38bab5ea66 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:41:38 -0500 Subject: [PATCH 202/256] ci: fix draft request --- .github/workflows/macos.yml | 3 +-- .github/workflows/main.yml | 4 +--- .github/workflows/windows.yml | 4 +--- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 16e306f6c..d1351712a 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -15,9 +15,8 @@ on: - master jobs: - JOB: - if: github.event.pull_request.draft == false test: + if: github.event.pull_request.draft == false name: Test Suite runs-on: macos-latest steps: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d4ebdd48e..3e1ec15c0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,10 +15,8 @@ on: - master jobs: - JOB: - if: github.event.pull_request.draft == false - check: + if: github.event.pull_request.draft == false name: Check runs-on: ubuntu-latest steps: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 41e98f9ee..518da37b3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -15,10 +15,8 @@ on: - master jobs: - JOB: - if: github.event.pull_request.draft == false - test: + if: github.event.pull_request.draft == false runs-on: windows-latest name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }}) env: From 3c54ce06f2c2359aadf6f1c4ad0a4eeca4bcb403 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:43:15 -0500 Subject: [PATCH 203/256] fix: disable more --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e1ec15c0..07ccd8069 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,6 +34,7 @@ jobs: command: check test: + if: github.event.pull_request.draft == false name: Test Suite runs-on: ubuntu-latest steps: @@ -55,6 +56,7 @@ jobs: args: tests fmt: + if: github.event.pull_request.draft == false name: Rustfmt runs-on: ubuntu-latest steps: @@ -72,6 +74,7 @@ jobs: args: --all -- --check clippy: + if: github.event.pull_request.draft == false name: Clippy runs-on: ubuntu-latest steps: @@ -91,6 +94,7 @@ jobs: args: -- -D warnings coverage: + if: github.event.pull_request.draft == false name: Coverage runs-on: ubuntu-latest container: From e03f8ca90c324be7a9051ca3c138a5171a5ebbd5 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:44:17 -0500 Subject: [PATCH 204/256] ci: no more codecov failing --- codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/codecov.yml b/codecov.yml index 69cb76019..325c05ec8 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1 +1,2 @@ comment: false +fail_ci_if_error: false From c5025357247465fc9ca2e85f97e81b2c35da8805 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 18:59:15 +0100 Subject: [PATCH 205/256] Update dependencies Specifically set wry to 0.13; this has breaking changes (notably: RPC -> IPC). --- Cargo.toml | 12 ++++++------ packages/desktop/Cargo.toml | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e3a0a032..e1561cb59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,15 +57,15 @@ members = [ ] [dev-dependencies] -futures-util = "0.3.17" +futures-util = "0.3.21" log = "0.4.14" num-format = "0.4.0" separator = "0.4.1" -serde = { version = "1.0.131", features = ["derive"] } +serde = { version = "1.0.136", features = ["derive"] } im-rc = "15.0.0" -anyhow = "1.0.51" -serde_json = "1.0.73" +anyhow = "1.0.53" +serde_json = "1.0.79" rand = { version = "0.8.4", features = ["small_rng"] } -tokio = { version = "1.14.0", features = ["full"] } -reqwest = { version = "0.11.8", features = ["json"] } +tokio = { version = "1.16.1", features = ["full"] } +reqwest = { version = "0.11.9", features = ["json"] } dioxus = { path = ".", features = ["desktop", "ssr", "router"] } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 0d7c13e3f..d1f8b8697 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -13,15 +13,15 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } -argh = "0.1.4" -serde = "1.0.120" -serde_json = "1.0.61" -thiserror = "1.0.23" -log = "0.4.13" +argh = "0.1.7" +serde = "1.0.136" +serde_json = "1.0.79" +thiserror = "1.0.30" +log = "0.4.14" html-escape = "0.2.9" -wry = "0.12.2" -futures-channel = "0.3" -tokio = { version = "1.12.0", features = [ +wry = { version = "0.13.1" } +futures-channel = "0.3.21" +tokio = { version = "1.16.1", features = [ "sync", "rt-multi-thread", "rt", From ee2b869e9957312f1141065102f9fa00e3405ee7 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:39:47 +0100 Subject: [PATCH 206/256] Add optional feature flags of wry Check wry's documentation for each. Some of them are platform dependent or have platform dependent effects. (mostly MacOS and Linux) --- Cargo.toml | 7 +++++++ packages/desktop/Cargo.toml | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e1561cb59..3f0d50187 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,13 @@ web = ["dioxus-web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] +devtool = ["dioxus-desktop/devtool"] +fullscreen = ["dioxus-desktop/fullscreen"] +transparent = ["dioxus-desktop/transparent"] + +tray = ["dioxus-desktop/tray"] +ayatana = ["dioxus-desktop/ayatana"] + # "dioxus-router/web" # "dioxus-router/desktop" # desktop = ["dioxus-desktop", "dioxus-router/desktop"] diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index d1f8b8697..2622355eb 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -38,6 +38,13 @@ dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } default = ["tokio_runtime"] tokio_runtime = ["tokio"] +devtool = ["wry/devtool"] +fullscreen = ["wry/fullscreen"] +transparent = ["wry/transparent"] + +tray = ["wry/tray"] +ayatana = ["wry/ayatana"] + [dev-dependencies] dioxus-hooks = { path = "../hooks" } From c40d225d7db2490f47967d1fea5c65e1a524954d Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:50:29 +0100 Subject: [PATCH 207/256] Fix typo --- packages/desktop/src/cfg.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 49a26ae78..210ec9637 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -12,9 +12,9 @@ use wry::{ pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); pub struct DesktopConfig { - pub window: WindowBuilder, - pub file_drop_handler: Option bool>>, - pub protocols: Vec, + pub(crate) window: WindowBuilder, + pub(crate) file_drop_handler: Option bool>>, + pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) event_handler: Option>, } From a5bf25ce18d97ad58fa10fd0e9be5cb69f632cbd Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:51:37 +0100 Subject: [PATCH 208/256] Adjust visibility --- packages/desktop/src/cfg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 210ec9637..90fd0f1ae 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -19,7 +19,7 @@ pub struct DesktopConfig { pub(crate) event_handler: Option>, } -pub type WryProtocol = ( +pub(crate) type WryProtocol = ( String, Box WryResult + 'static>, ); From afa5a301c7ddae707130f0f8fb27a9446f4a879d Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:52:05 +0100 Subject: [PATCH 209/256] Fix typo --- packages/interpreter/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index ebca3cbf4..076a89197 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -1,4 +1,4 @@ -pub static INTERPRTER_JS: &str = include_str!("./interpreter.js"); +pub static INTERPRETER_JS: &str = include_str!("./interpreter.js"); #[cfg(feature = "web")] mod bindings; From 594a794f0538749f56f8fb56d011e46aead7ca1e Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:56:11 +0100 Subject: [PATCH 210/256] Switch from RPC to IPC --- packages/desktop/src/events.rs | 40 +++++++++++++++++------ packages/desktop/src/lib.rs | 42 ++++++++++++------------- packages/interpreter/src/interpreter.js | 12 ++++--- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 760bf7662..a991ec788 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -1,5 +1,4 @@ -//! Convert a serialized event to an event Trigger -//! +//! Convert a serialized event to an event trigger use std::any::Any; use std::sync::Arc; @@ -7,27 +6,50 @@ use std::sync::Arc; use dioxus_core::{ElementId, EventPriority, UserEvent}; use dioxus_html::on::*; +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct IpcMessage { + method: String, + params: serde_json::Value, +} + +impl IpcMessage { + pub(crate) fn method(&self) -> &str { + self.method.as_str() + } + + pub(crate) fn params(self) -> serde_json::Value { + self.params + } +} + +pub(crate) fn parse_ipc_message(payload: &str) -> Option { + let mm = serde_json::from_str(payload); + match mm { + Ok(message) => Some(message), + Err(e) => { + log::error!("could not parse IPC message, error: {e}"); + None + } + } +} + #[derive(serde::Serialize, serde::Deserialize)] struct ImEvent { event: String, mounted_dom_id: u64, - // scope: u64, contents: serde_json::Value, } pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { - let ims: Vec = serde_json::from_value(val).unwrap(); - let ImEvent { event, mounted_dom_id, contents, - } = ims.into_iter().next().unwrap(); + } = serde_json::from_value(val).unwrap(); - // let scope_id = ScopeId(scope as usize); let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); - let name = event_name_from_typ(&event); + let name = event_name_from_type(&event); let event = make_synthetic_event(&event, contents); UserEvent { @@ -105,7 +127,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc &'static str { +fn event_name_from_type(typ: &str) -> &'static str { match typ { "copy" => "copy", "cut" => "cut", diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index f4e28ca2a..90fb4a3b0 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -132,23 +132,24 @@ pub fn launch_with_props( .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() - .with_rpc_handler(move |_window: &Window, req: RpcRequest| { - match req.method.as_str() { - "user_event" => { - let event = events::trigger_from_serialized(req.params.unwrap()); - log::trace!("User event: {:?}", event); - sender.unbounded_send(SchedulerMsg::Event(event)).unwrap(); - } - "initialize" => { - is_ready.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = proxy.send_event(UserWindowEvent::Update); - } - "browser_open" => { - println!("browser_open"); - let data = req.params.unwrap(); - log::trace!("Open browser: {:?}", data); - if let Some(arr) = data.as_array() { - if let Some(temp) = arr[0].as_object() { + .with_ipc_handler(move |_window: &Window, payload: String| { + parse_ipc_message(&payload) + .map(|message| match message.method() { + "user_event" => { + let event = trigger_from_serialized(message.params()); + log::trace!("User event: {:?}", event); + sender.unbounded_send(SchedulerMsg::Event(event)).unwrap(); + } + "initialize" => { + is_ready.store(true, std::sync::atomic::Ordering::Relaxed); + let _ = proxy + .send_event(user_window_events::UserWindowEvent::Update); + } + "browser_open" => { + println!("browser_open"); + let data = message.params(); + log::trace!("Open browser: {:?}", data); + if let Some(temp) = data.as_object() { if temp.contains_key("href") { let url = temp.get("href").unwrap().as_str().unwrap(); if let Err(e) = webbrowser::open(url) { @@ -157,11 +158,8 @@ pub fn launch_with_props( } } } - } - _ => {} - } - None - }) + _ => (), + }) .with_custom_protocol(String::from("dioxus"), move |request| { // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case" // For now, we only serve two pieces of content which get included as bytes into the final binary. diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index f0acd0802..f042b74c9 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -2,7 +2,7 @@ export function main() { let root = window.document.getElementById("main"); if (root != null) { window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); + window.ipc.postMessage(serializeIpcMessage("initialize")) } } export class Interpreter { @@ -207,7 +207,7 @@ export class Interpreter { event.preventDefault(); const href = target.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { - window.rpc.call("browser_open", { href }); + window.ipc.postMessage(serializeIpcMessage("browser_open", { href })) } } } @@ -261,11 +261,12 @@ export class Interpreter { if (realId == null) { return; } - window.rpc.call("user_event", { + window.ipc.postMessage(serializeIpcMessage( + "user_event", { event: edit.event_name, mounted_dom_id: parseInt(realId), contents: contents, - }); + })); } }; this.NewEventListener(edit.event_name, edit.root, handler); @@ -544,6 +545,9 @@ export function serialize_event(event) { } } } +function serializeIpcMessage(method, params = {}) { + return JSON.stringify({ method, params }); +} const bool_attrs = { allowfullscreen: true, allowpaymentrequest: true, From 73ce79bd2a31e409fc34d0673c8efbd08fc01d31 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:57:46 +0100 Subject: [PATCH 211/256] Extract protocol hander into module --- packages/desktop/src/lib.rs | 47 +++----------------------------- packages/desktop/src/protocol.rs | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 packages/desktop/src/protocol.rs diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 90fb4a3b0..31812f680 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -160,50 +160,11 @@ pub fn launch_with_props( } _ => (), }) - .with_custom_protocol(String::from("dioxus"), move |request| { - // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case" - // For now, we only serve two pieces of content which get included as bytes into the final binary. - let path = request.uri().replace("dioxus://", ""); - - // all assets shouldbe called from index.html - let trimmed = path.trim_start_matches("index.html/"); - - if trimmed.is_empty() { - wry::http::ResponseBuilder::new() - .mimetype("text/html") - .body(include_bytes!("./index.html").to_vec()) - } else if trimmed == "index.js" { - wry::http::ResponseBuilder::new() - .mimetype("text/javascript") - .body(dioxus_interpreter_js::INTERPRTER_JS.as_bytes().to_vec()) - } else { - // Read the file content from file path - use std::fs::read; - - let path_buf = std::path::Path::new(trimmed).canonicalize()?; - let cur_path = std::path::Path::new(".").canonicalize()?; - - if !path_buf.starts_with(cur_path) { - return wry::http::ResponseBuilder::new() - .status(wry::http::status::StatusCode::FORBIDDEN) - .body(String::from("Forbidden").into_bytes()); - } - - if !path_buf.exists() { - return wry::http::ResponseBuilder::new() - .status(wry::http::status::StatusCode::NOT_FOUND) - .body(String::from("Not Found").into_bytes()); - } - - let mime = mime_guess::from_path(&path_buf).first_or_octet_stream(); - - // do not let path searching to go two layers beyond the caller level - let data = read(path_buf)?; - let meta = format!("{}", mime); - - wry::http::ResponseBuilder::new().mimetype(&meta).body(data) - } + .unwrap_or_else(|| { + log::warn!("invalid IPC message received"); + }); }) + .with_custom_protocol(String::from("dioxus"), protocol::desktop_handler) .with_file_drop_handler(move |window, evet| { file_handler .as_ref() diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs new file mode 100644 index 000000000..c8f007c1d --- /dev/null +++ b/packages/desktop/src/protocol.rs @@ -0,0 +1,47 @@ +use std::path::Path; +use wry::{ + http::{status::StatusCode, Request, Response, ResponseBuilder}, + Result, +}; + +pub(super) fn desktop_handler(request: &Request) -> Result { + // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case". + // For now, we only serve two pieces of content which get included as bytes into the final binary. + let path = request.uri().replace("dioxus://", ""); + + // all assets should be called from index.html + let trimmed = path.trim_start_matches("index.html/"); + + if trimmed.is_empty() { + ResponseBuilder::new() + .mimetype("text/html") + .body(include_bytes!("./index.html").to_vec()) + } else if trimmed == "index.js" { + ResponseBuilder::new() + .mimetype("text/javascript") + .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec()) + } else { + let path_buf = Path::new(trimmed).canonicalize()?; + let cur_path = Path::new(".").canonicalize()?; + + if !path_buf.starts_with(cur_path) { + return ResponseBuilder::new() + .status(StatusCode::FORBIDDEN) + .body(String::from("Forbidden").into_bytes()); + } + + if !path_buf.exists() { + return ResponseBuilder::new() + .status(StatusCode::NOT_FOUND) + .body(String::from("Not Found").into_bytes()); + } + + let mime = mime_guess::from_path(&path_buf).first_or_octet_stream(); + + // do not let path searching to go two layers beyond the caller level + let data = std::fs::read(path_buf)?; + let meta = format!("{}", mime); + + ResponseBuilder::new().mimetype(&meta).body(data) + } +} From cf543ab1dfe0a0af423c37d3850af04432cb075a Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:58:37 +0100 Subject: [PATCH 212/256] Extract controller into module --- packages/desktop/src/controller.rs | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/desktop/src/controller.rs diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs new file mode 100644 index 000000000..7d9a2f432 --- /dev/null +++ b/packages/desktop/src/controller.rs @@ -0,0 +1,106 @@ +use crate::desktop_context::DesktopContext; +use crate::user_window_events::UserWindowEvent; +use dioxus_core::*; +use std::{ + collections::{HashMap, VecDeque}, + sync::atomic::AtomicBool, + sync::{Arc, RwLock}, +}; +use wry::{ + self, + application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId}, + webview::WebView, +}; + +pub(super) struct DesktopController { + pub(super) webviews: HashMap, + pub(super) sender: futures_channel::mpsc::UnboundedSender, + pub(super) pending_edits: Arc>>, + pub(super) quit_app_on_close: bool, + pub(super) is_ready: Arc, +} + +impl DesktopController { + // Launch the virtualdom on its own thread managed by tokio + // returns the desktop state + pub(super) fn new_on_tokio( + root: Component

, + props: P, + proxy: EventLoopProxy, + ) -> Self { + let edit_queue = Arc::new(RwLock::new(VecDeque::new())); + let pending_edits = edit_queue.clone(); + + let (sender, receiver) = futures_channel::mpsc::unbounded::(); + let return_sender = sender.clone(); + + let desktop_context_proxy = proxy.clone(); + std::thread::spawn(move || { + // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + runtime.block_on(async move { + let mut dom = + VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver)); + + let window_context = DesktopContext::new(desktop_context_proxy); + + dom.base_scope().provide_context(window_context); + + let edits = dom.rebuild(); + + edit_queue + .write() + .unwrap() + .push_front(serde_json::to_string(&edits.edits).unwrap()); + + loop { + dom.wait_for_work().await; + let mut muts = dom.work_with_deadline(|| false); + + while let Some(edit) = muts.pop() { + edit_queue + .write() + .unwrap() + .push_front(serde_json::to_string(&edit.edits).unwrap()); + } + + let _ = proxy.send_event(UserWindowEvent::Update); + } + }) + }); + + Self { + pending_edits, + sender: return_sender, + webviews: HashMap::new(), + is_ready: Arc::new(AtomicBool::new(false)), + quit_app_on_close: true, + } + } + + pub(super) fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { + self.webviews.remove(&window_id); + + if self.webviews.is_empty() && self.quit_app_on_close { + *control_flow = ControlFlow::Exit; + } + } + + pub(super) fn try_load_ready_webviews(&mut self) { + if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { + let mut queue = self.pending_edits.write().unwrap(); + let (_id, view) = self.webviews.iter_mut().next().unwrap(); + + while let Some(edit) = queue.pop_back() { + view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) + .unwrap(); + } + } else { + println!("waiting for ready"); + } + } +} From e7a0e5f1d9fb03e9aa1ef8aa9978898f4c6ba56f Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:58:50 +0100 Subject: [PATCH 213/256] Extract user window events into module --- packages/desktop/src/user_window_events.rs | 72 ++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 packages/desktop/src/user_window_events.rs diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs new file mode 100644 index 000000000..93eeaccf1 --- /dev/null +++ b/packages/desktop/src/user_window_events.rs @@ -0,0 +1,72 @@ +use wry::application::event_loop::ControlFlow; +use wry::application::window::Fullscreen as WryFullscreen; + +use crate::controller::DesktopController; + +pub(crate) enum UserWindowEvent { + Update, + + CloseWindow, + DragWindow, + FocusWindow, + + Visible(bool), + Minimize(bool), + Maximize(bool), + MaximizeToggle, + Resizable(bool), + AlwaysOnTop(bool), + Fullscreen(bool), + + CursorVisible(bool), + CursorGrab(bool), + + SetTitle(String), + SetDecorations(bool), + + DevTool, +} + +use UserWindowEvent::*; + +pub(super) fn handler( + user_event: UserWindowEvent, + desktop: &mut DesktopController, + control_flow: &mut ControlFlow, +) { + // currently dioxus-desktop supports a single window only, + // so we can grab the only webview from the map; + let webview = desktop.webviews.values().next().unwrap(); + let window = webview.window(); + + match user_event { + Update => desktop.try_load_ready_webviews(), + CloseWindow => *control_flow = ControlFlow::Exit, + DragWindow => { + // if the drag_window has any errors, we don't do anything + window.fullscreen().is_none().then(|| window.drag_window()); + } + Visible(state) => window.set_visible(state), + Minimize(state) => window.set_minimized(state), + Maximize(state) => window.set_maximized(state), + MaximizeToggle => window.set_maximized(!window.is_maximized()), + Fullscreen(state) => { + window.current_monitor().map(|handle| { + window.set_fullscreen(state.then(|| WryFullscreen::Borderless(Some(handle)))); + }); + } + FocusWindow => window.set_focus(), + Resizable(state) => window.set_resizable(state), + AlwaysOnTop(state) => window.set_always_on_top(state), + + CursorVisible(state) => window.set_cursor_visible(state), + CursorGrab(state) => { + let _ = window.set_cursor_grab(state); + } + + SetTitle(content) => window.set_title(&content), + SetDecorations(state) => window.set_decorations(state), + + DevTool => webview.devtool(), + } +} From 2828f45e12e129a29006bdacbf0ccbec38fefbfe Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:59:25 +0100 Subject: [PATCH 214/256] Clean up desktop's lib.rs --- packages/desktop/src/lib.rs | 246 ++---------------------------------- 1 file changed, 11 insertions(+), 235 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 31812f680..371adced5 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -3,31 +3,28 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] pub mod cfg; +mod controller; pub mod desktop_context; pub mod escape; pub mod events; +mod protocol; +mod user_window_events; use cfg::DesktopConfig; +use controller::DesktopController; pub use desktop_context::use_window; -use desktop_context::DesktopContext; use dioxus_core::*; -use std::{ - collections::{HashMap, VecDeque}, - sync::atomic::AtomicBool, - sync::{Arc, RwLock}, -}; +use events::parse_ipc_message; use tao::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, - window::{Window, WindowId}, + window::Window, }; pub use wry; pub use wry::application as tao; -use wry::{ - application::{event_loop::EventLoopProxy, window::Fullscreen}, - webview::RpcRequest, - webview::{WebView, WebViewBuilder}, -}; +use wry::webview::WebViewBuilder; + +use crate::events::trigger_from_serialized; /// Launch the WebView and run the event loop. /// @@ -194,114 +191,8 @@ pub fn launch_with_props( _ => {} }, - Event::UserEvent(_evt) => { - // - match _evt { - UserWindowEvent::Update => desktop.try_load_ready_webviews(), - UserWindowEvent::DragWindow => { - // this loop just run once, because dioxus-desktop is unsupport multi-window. - for webview in desktop.webviews.values() { - let window = webview.window(); - // start to drag the window. - // if the drag_window have any err. we don't do anything. - - if window.fullscreen().is_some() { - return; - } - - let _ = window.drag_window(); - } - } - UserWindowEvent::CloseWindow => { - // close window - *control_flow = ControlFlow::Exit; - } - UserWindowEvent::Visible(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_visible(state); - } - } - UserWindowEvent::Minimize(state) => { - // this loop just run once, because dioxus-desktop is unsupport multi-window. - for webview in desktop.webviews.values() { - let window = webview.window(); - // change window minimized state. - window.set_minimized(state); - } - } - UserWindowEvent::Maximize(state) => { - // this loop just run once, because dioxus-desktop is unsupport multi-window. - for webview in desktop.webviews.values() { - let window = webview.window(); - // change window maximized state. - window.set_maximized(state); - } - } - UserWindowEvent::Fullscreen(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - - let current_monitor = window.current_monitor(); - - if current_monitor.is_none() { - return; - } - - let fullscreen = if state { - Some(Fullscreen::Borderless(current_monitor)) - } else { - None - }; - - window.set_fullscreen(fullscreen); - } - } - UserWindowEvent::FocusWindow => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_focus(); - } - } - UserWindowEvent::Resizable(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_resizable(state); - } - } - UserWindowEvent::AlwaysOnTop(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_always_on_top(state); - } - } - - UserWindowEvent::CursorVisible(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_cursor_visible(state); - } - } - UserWindowEvent::CursorGrab(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - let _ = window.set_cursor_grab(state); - } - } - - UserWindowEvent::SetTitle(content) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_title(&content); - } - } - UserWindowEvent::SetDecorations(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_decorations(state); - } - } - } + Event::UserEvent(user_event) => { + user_window_events::handler(user_event, &mut desktop, control_flow) } Event::MainEventsCleared => {} Event::Resumed => {} @@ -312,118 +203,3 @@ pub fn launch_with_props( } }) } - -pub enum UserWindowEvent { - Update, - DragWindow, - CloseWindow, - FocusWindow, - Visible(bool), - Minimize(bool), - Maximize(bool), - Resizable(bool), - AlwaysOnTop(bool), - Fullscreen(bool), - - CursorVisible(bool), - CursorGrab(bool), - - SetTitle(String), - SetDecorations(bool), -} - -pub struct DesktopController { - pub proxy: EventLoopProxy, - pub webviews: HashMap, - pub sender: futures_channel::mpsc::UnboundedSender, - pub pending_edits: Arc>>, - pub quit_app_on_close: bool, - pub is_ready: Arc, -} - -impl DesktopController { - // Launch the virtualdom on its own thread managed by tokio - // returns the desktop state - pub fn new_on_tokio( - root: Component

, - props: P, - evt: EventLoopProxy, - ) -> Self { - let edit_queue = Arc::new(RwLock::new(VecDeque::new())); - let pending_edits = edit_queue.clone(); - - let (sender, receiver) = futures_channel::mpsc::unbounded::(); - let return_sender = sender.clone(); - let proxy = evt.clone(); - - let desktop_context_proxy = proxy.clone(); - std::thread::spawn(move || { - // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - - runtime.block_on(async move { - let mut dom = - VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver)); - - let window_context = DesktopContext::new(desktop_context_proxy); - - dom.base_scope().provide_context(window_context); - - let edits = dom.rebuild(); - - edit_queue - .write() - .unwrap() - .push_front(serde_json::to_string(&edits.edits).unwrap()); - - loop { - dom.wait_for_work().await; - let mut muts = dom.work_with_deadline(|| false); - - while let Some(edit) = muts.pop() { - edit_queue - .write() - .unwrap() - .push_front(serde_json::to_string(&edit.edits).unwrap()); - } - - let _ = evt.send_event(UserWindowEvent::Update); - } - }) - }); - - Self { - pending_edits, - sender: return_sender, - proxy, - webviews: HashMap::new(), - is_ready: Arc::new(AtomicBool::new(false)), - quit_app_on_close: true, - } - } - - pub fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { - self.webviews.remove(&window_id); - - if self.webviews.is_empty() && self.quit_app_on_close { - *control_flow = ControlFlow::Exit; - } - } - - pub fn try_load_ready_webviews(&mut self) { - if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { - let mut queue = self.pending_edits.write().unwrap(); - let (_id, view) = self.webviews.iter_mut().next().unwrap(); - - while let Some(edit) = queue.pop_back() { - view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) - .unwrap(); - } - } else { - println!("waiting for ready"); - } - } -} From 934d5998dbc5bc8e14cce10d3b4f1332205f6ae8 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 20:00:45 +0100 Subject: [PATCH 215/256] Support maximize toggle and devtool --- packages/desktop/src/desktop_context.rs | 47 +++++++++++++------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index d180e1235..e1b726d73 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,7 +3,8 @@ use std::rc::Rc; use dioxus_core::ScopeState; use wry::application::event_loop::EventLoopProxy; -use crate::UserWindowEvent; +use crate::user_window_events::UserWindowEvent; +use UserWindowEvent::*; type ProxyType = EventLoopProxy; @@ -35,75 +36,77 @@ impl DesktopContext { /// onmousedown: move |_| { desktop.drag_window(); } /// ``` pub fn drag(&self) { - let _ = self.proxy.send_event(UserWindowEvent::DragWindow); + let _ = self.proxy.send_event(DragWindow); } /// set window minimize state pub fn set_minimized(&self, minimized: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized)); + let _ = self.proxy.send_event(Minimize(minimized)); } /// set window maximize state pub fn set_maximized(&self, maximized: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); + let _ = self.proxy.send_event(Maximize(maximized)); + } + + /// toggle window maximize state + pub fn toggle_maximized(&self) { + let _ = self.proxy.send_event(MaximizeToggle); } /// set window visible or not pub fn set_visible(&self, visible: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Visible(visible)); + let _ = self.proxy.send_event(Visible(visible)); } /// close window pub fn close(&self) { - let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); + let _ = self.proxy.send_event(CloseWindow); } /// set window to focus pub fn focus(&self) { - let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); + let _ = self.proxy.send_event(FocusWindow); } /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { - let _ = self - .proxy - .send_event(UserWindowEvent::Fullscreen(fullscreen)); + let _ = self.proxy.send_event(Fullscreen(fullscreen)); } /// set resizable state pub fn set_resizable(&self, resizable: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); + let _ = self.proxy.send_event(Resizable(resizable)); } /// set the window always on top pub fn set_always_on_top(&self, top: bool) { - let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top)); + let _ = self.proxy.send_event(AlwaysOnTop(top)); } // set cursor visible or not pub fn set_cursor_visible(&self, visible: bool) { - let _ = self - .proxy - .send_event(UserWindowEvent::CursorVisible(visible)); + let _ = self.proxy.send_event(CursorVisible(visible)); } // set cursor grab pub fn set_cursor_grab(&self, grab: bool) { - let _ = self.proxy.send_event(UserWindowEvent::CursorGrab(grab)); + let _ = self.proxy.send_event(CursorGrab(grab)); } /// set window title pub fn set_title(&self, title: &str) { - let _ = self - .proxy - .send_event(UserWindowEvent::SetTitle(String::from(title))); + let _ = self.proxy.send_event(SetTitle(String::from(title))); } /// change window to borderless pub fn set_decorations(&self, decoration: bool) { - let _ = self - .proxy - .send_event(UserWindowEvent::SetDecorations(decoration)); + let _ = self.proxy.send_event(SetDecorations(decoration)); + } + + /// opens DevTool window + pub fn devtool(&self) { + let _ = self.proxy.send_event(DevTool); } } From 932ad0164454c65335f7c62078d3a7250a8608c3 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 20:57:30 +0100 Subject: [PATCH 216/256] Make clippy happy --- packages/desktop/src/user_window_events.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs index 93eeaccf1..aab14561b 100644 --- a/packages/desktop/src/user_window_events.rs +++ b/packages/desktop/src/user_window_events.rs @@ -51,9 +51,9 @@ pub(super) fn handler( Maximize(state) => window.set_maximized(state), MaximizeToggle => window.set_maximized(!window.is_maximized()), Fullscreen(state) => { - window.current_monitor().map(|handle| { + if let Some(handle) = window.current_monitor() { window.set_fullscreen(state.then(|| WryFullscreen::Borderless(Some(handle)))); - }); + } } FocusWindow => window.set_focus(), Resizable(state) => window.set_resizable(state), From dd9f0f362ec8e7193a4920c2ee46246f1cbe67c9 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 14 Feb 2022 16:52:16 +0800 Subject: [PATCH 217/256] fix: statement problem --- packages/interpreter/src/interpreter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index f0acd0802..a631d0368 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -102,10 +102,11 @@ export class Interpreter { SetAttribute(root, field, value, ns) { const name = field; const node = this.nodes[root]; + console.log(ns); if (ns === "style") { // @ts-ignore node.style[name] = value; - } else if (ns != null || ns !== undefined) { + } else if (ns != null || ns != undefined) { node.setAttributeNS(ns, name, value); } else { switch (name) { From e3e5f22bc7605b7019d5f92eeff719abcbf512c5 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 14 Feb 2022 16:53:35 +0800 Subject: [PATCH 218/256] feat: move `default_icon` to assets --- packages/desktop/src/{ => assets}/default_icon.bin | Bin packages/desktop/src/{ => assets}/default_icon.png | Bin packages/desktop/src/cfg.rs | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/desktop/src/{ => assets}/default_icon.bin (100%) rename packages/desktop/src/{ => assets}/default_icon.png (100%) diff --git a/packages/desktop/src/default_icon.bin b/packages/desktop/src/assets/default_icon.bin similarity index 100% rename from packages/desktop/src/default_icon.bin rename to packages/desktop/src/assets/default_icon.bin diff --git a/packages/desktop/src/default_icon.png b/packages/desktop/src/assets/default_icon.png similarity index 100% rename from packages/desktop/src/default_icon.png rename to packages/desktop/src/assets/default_icon.png diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 49a26ae78..2a1067ded 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -88,7 +88,7 @@ impl DesktopConfig { impl DesktopConfig { pub(crate) fn with_default_icon(mut self) -> Self { - let bin: &[u8] = include_bytes!("default_icon.bin"); + let bin: &[u8] = include_bytes!("./assets/default_icon.bin"); let rgba = Icon::from_rgba(bin.to_owned(), 460, 460).expect("image parse failed"); self.window.window.window_icon = Some(rgba); self From 3317b3aac51f2e1225ffc873cfa0ed25b3096f3d Mon Sep 17 00:00:00 2001 From: dazmaks Date: Mon, 14 Feb 2022 17:13:09 +0300 Subject: [PATCH 219/256] fix: typo --- docs/reference/src/mobile/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/src/mobile/index.md b/docs/reference/src/mobile/index.md index 2e8344ed1..aa3470761 100644 --- a/docs/reference/src/mobile/index.md +++ b/docs/reference/src/mobile/index.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo moble init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. From 8ca505b65beba1edf74eddf47d4da5a8e7be7033 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 14 Feb 2022 09:30:08 -0500 Subject: [PATCH 220/256] feat: better link --- packages/router/src/components/link.rs | 48 +++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 18e9fd92e..b2b69108f 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -34,6 +34,17 @@ pub struct LinkProps<'a> { #[props(default, strip_option)] title: Option<&'a str>, + #[props(default = true)] + autodetect: bool, + + /// Is this link an external link? + #[props(default = false)] + external: bool, + + /// New tab? + #[props(default = false)] + new_tab: bool, + children: Element<'a>, #[props(default)] @@ -41,17 +52,38 @@ pub struct LinkProps<'a> { } pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - // log::trace!("render Link to {}", cx.props.to); if let Some(service) = cx.consume_context::() { + let LinkProps { + to, + href, + class, + id, + title, + autodetect, + external, + new_tab, + children, + .. + } = cx.props; + + let is_http = to.starts_with("http") || to.starts_with("https"); + let outerlink = (*autodetect && is_http) || *external; + + let prevent_default = if outerlink { "" } else { "onclick" }; + return cx.render(rsx! { a { - href: "{cx.props.to}", - class: format_args!("{}", cx.props.class.unwrap_or("")), - id: format_args!("{}", cx.props.id.unwrap_or("")), - title: format_args!("{}", cx.props.title.unwrap_or("")), - - prevent_default: "onclick", - onclick: move |_| service.push_route(cx.props.to), + href: "{to}", + class: format_args!("{}", class.unwrap_or("")), + id: format_args!("{}", id.unwrap_or("")), + title: format_args!("{}", title.unwrap_or("")), + prevent_default: "{prevent_default}", + target: format_args!("{}", if *new_tab { "_blank" } else { "" }), + onclick: move |_| { + if !outerlink { + service.push_route(to); + } + }, &cx.props.children } From 5a908d1e8b92d712c35c7f05af5956926acec29e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 14 Feb 2022 12:23:30 -0500 Subject: [PATCH 221/256] fix: remove console log --- packages/interpreter/src/interpreter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index a631d0368..5b81d3a9f 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -102,7 +102,6 @@ export class Interpreter { SetAttribute(root, field, value, ns) { const name = field; const node = this.nodes[root]; - console.log(ns); if (ns === "style") { // @ts-ignore node.style[name] = value; From 1413631d306065b76a9d2dce1c6e709c0def0bd4 Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Mon, 14 Feb 2022 19:08:10 -0500 Subject: [PATCH 222/256] update: match DioxusLabs/docs#3 --- README.md | 8 ++++---- docs/guide/src/README.md | 12 ++++++------ docs/guide/src/setup.md | 2 +- docs/reference/src/SUMMARY.md | 11 ++++++----- .../src/{desktop/index.md => platforms/desktop.md} | 2 +- docs/reference/src/platforms/index.md | 10 ++++++++++ .../src/{mobile/index.md => platforms/mobile.md} | 2 +- .../reference/src/{ssr/index.md => platforms/ssr.md} | 0 .../reference/src/{tui/index.md => platforms/tui.md} | 2 +- .../reference/src/{web/index.md => platforms/web.md} | 2 +- 10 files changed, 31 insertions(+), 20 deletions(-) rename docs/reference/src/{desktop/index.md => platforms/desktop.md} (98%) create mode 100644 docs/reference/src/platforms/index.md rename docs/reference/src/{mobile/index.md => platforms/mobile.md} (99%) rename docs/reference/src/{ssr/index.md => platforms/ssr.md} (100%) rename docs/reference/src/{tui/index.md => platforms/tui.md} (98%) rename docs/reference/src/{web/index.md => platforms/web.md} (98%) diff --git a/README.md b/README.md index 02857ed18..23cd9c516 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,10 @@ cargo run --example EXAMPLE - - - - + + + +
TutorialWebDesktopSSRMobileWebDesktopSSRMobile State
diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index bbbd9f85f..da6c0156f 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -18,7 +18,7 @@ fn app(cx: Scope) -> 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 an introduction book! For advanced topics, check out the [Reference](https://dioxuslabs.com/reference) instead. +> This is an introduction book! For advanced topics, check out the [Reference](/reference) instead. ## Multiplatform @@ -37,7 +37,7 @@ The Web is the best-supported target platform for Dioxus. To run on the Web, you Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features. -[Jump to the getting started guide for the web.]() +[Jump to the getting started guide for the web.](/reference/platforms/web) Examples: - [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc) @@ -55,7 +55,7 @@ For rendering statically to an `.html` file or from a WebServer, then you'll wan let contents = dioxus::ssr::render_vdom(&dom); ``` -[Jump to the getting started guide for SSR.]() +[Jump to the getting started guide for SSR.](/reference/platforms/ssr) Examples: - [Example DocSite](https://github.com/dioxusLabs/docsite) @@ -68,13 +68,13 @@ The desktop is a powerful target for Dioxus, but is currently limited in capabil Desktop APIs will likely be in flux as we figure out better patterns than our ElectronJS counterpart. -[Jump to the getting started guide for Desktop.]() +[Jump to the getting started guide for Desktop.](/reference/platforms/desktop) Examples: - [File explorer](https://github.com/dioxusLabs/file-explorer/) - [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) -[![File ExplorerExample](https://github.com/DioxusLabs/file-explorer-example/raw/master/image.png)](https://github.com/dioxusLabs/file-explorer/) +[![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer) ### Mobile Support --- @@ -82,7 +82,7 @@ Mobile is currently the least-supported renderer target for Dioxus. Mobile apps Mobile support is currently best suited for CRUD-style apps, ideally for internal teams who need to develop quickly but don't care much about animations or native widgets. -[Jump to the getting started guide for Mobile.]() +[Jump to the getting started guide for Mobile.](/reference/platforms/mobile) Examples: - [Todo App](https://github.com/DioxusLabs/example-projects/blob/master/ios_demo) diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 1dc9434c5..75cfc6606 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -8,7 +8,7 @@ We'll learn about: - Suggested cargo extensions -For platform-specific guides, check out the [Platform Specific Guides](../platforms/00-index.md). +For platform-specific guides, check out the [Platform Specific Guides](/reference/platforms/index.md). # Setting up Dioxus diff --git a/docs/reference/src/SUMMARY.md b/docs/reference/src/SUMMARY.md index 3feb9fe36..18f351bfa 100644 --- a/docs/reference/src/SUMMARY.md +++ b/docs/reference/src/SUMMARY.md @@ -2,11 +2,12 @@ - [Introduction](README.md) -- [Web](web/index.md) -- [Desktop](desktop/index.md) -- [Mobile](mobile/index.md) -- [SSR](ssr/index.md) -- [TUI](tui/index.md) +- [Platforms](platforms/index.md) + - [Web](platforms/web.md) + - [Server Side Rendering](platforms/ssr.md) + - [Desktop](platforms/desktop.md) + - [Mobile](platforms/mobile.md) + - [TUI](platforms/tui.md) - [Advanced Guides](guide/index.md) - [RSX in Depth](guide/rsx_in_depth.md) diff --git a/docs/reference/src/desktop/index.md b/docs/reference/src/platforms/desktop.md similarity index 98% rename from docs/reference/src/desktop/index.md rename to docs/reference/src/platforms/desktop.md index 8011f1eed..18393f132 100644 --- a/docs/reference/src/desktop/index.md +++ b/docs/reference/src/platforms/desktop.md @@ -1,4 +1,4 @@ -# Desktop +# Getting Started: Desktop One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. diff --git a/docs/reference/src/platforms/index.md b/docs/reference/src/platforms/index.md new file mode 100644 index 000000000..fbbabd35d --- /dev/null +++ b/docs/reference/src/platforms/index.md @@ -0,0 +1,10 @@ +# Platforms + +Dioxus supports many different platforms. Below are a list of guides that walk you through setting up Dioxus for a specific platform. + +### Setup Guides +- [Web](web.md) +- [Server Side Rendering](ssr.md) +- [Desktop](desktop.md) +- [Mobile](mobile.md) +- [TUI](tui.md) \ No newline at end of file diff --git a/docs/reference/src/mobile/index.md b/docs/reference/src/platforms/mobile.md similarity index 99% rename from docs/reference/src/mobile/index.md rename to docs/reference/src/platforms/mobile.md index 2e8344ed1..5a41ffe60 100644 --- a/docs/reference/src/mobile/index.md +++ b/docs/reference/src/platforms/mobile.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo moble init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. diff --git a/docs/reference/src/ssr/index.md b/docs/reference/src/platforms/ssr.md similarity index 100% rename from docs/reference/src/ssr/index.md rename to docs/reference/src/platforms/ssr.md diff --git a/docs/reference/src/tui/index.md b/docs/reference/src/platforms/tui.md similarity index 98% rename from docs/reference/src/tui/index.md rename to docs/reference/src/platforms/tui.md index 3947883d4..e646bd4ba 100644 --- a/docs/reference/src/tui/index.md +++ b/docs/reference/src/platforms/tui.md @@ -1,4 +1,4 @@ -# TUI +# Getting Started: TUI TUI support is currently quite experimental. Even the project name will change. But, if you're willing to venture into the realm of the unknown, this guide will get you started. diff --git a/docs/reference/src/web/index.md b/docs/reference/src/platforms/web.md similarity index 98% rename from docs/reference/src/web/index.md rename to docs/reference/src/platforms/web.md index 062cba177..2ff5f621e 100644 --- a/docs/reference/src/web/index.md +++ b/docs/reference/src/platforms/web.md @@ -1,4 +1,4 @@ -# Getting Started: Dioxus for Web +# Getting Started: Web [*"Pack your things, we're going on an adventure!"*](https://trunkrs.dev) From 7c2d911fbf1abb23a216f3ef5c235bc75766ee4a Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 15 Feb 2022 09:54:32 -0600 Subject: [PATCH 223/256] Fix closure signature for use_ref example in interactivity docs The example showed the closure accepting a single argument, but it accepts none. --- docs/guide/src/interactivity/hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/interactivity/hooks.md b/docs/guide/src/interactivity/hooks.md index a90afe2dc..a84a0b129 100644 --- a/docs/guide/src/interactivity/hooks.md +++ b/docs/guide/src/interactivity/hooks.md @@ -140,7 +140,7 @@ fn Child(cx: Scope, name: String) -> Element { // ✅ Or, use a hashmap with use_ref ```rust -let ages = use_ref(&cx, |_| HashMap::new()); +let ages = use_ref(&cx, || HashMap::new()); names.iter().map(|name| { let age = ages.get(name).unwrap(); From e43a8a9b6a68bd2344bc86c6613ab046d89da40b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 11:03:13 -0500 Subject: [PATCH 224/256] feat: remove old async channel for new channel in ric raf web code --- packages/web/Cargo.toml | 2 +- packages/web/src/dom.rs | 7 ++----- packages/web/src/lib.rs | 2 +- packages/web/src/ric_raf.rs | 21 +++++++++++---------- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7a3baa955..31a7ff6be 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -23,13 +23,13 @@ wasm-logger = "0.2.0" console_error_panic_hook = { version = "0.1.7", optional = true } wasm-bindgen-test = "0.3.29" once_cell = "1.9.0" -async-channel = "1.6.1" anyhow = "1.0.53" gloo-timers = { version = "0.2.3", features = ["futures"] } futures-util = "0.3.19" smallstr = "0.2.0" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0", features = ["web"] } serde-wasm-bindgen = "0.4.2" +futures-channel = "0.3.21" [dependencies.web-sys] version = "0.3.56" diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 48fb25e6e..b2eb65d08 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -87,14 +87,11 @@ impl WebsysDom { } }); + // a match here in order to avoid some error during runtime browser test let document = load_document(); let root = match document.get_element_by_id(&cfg.rootname) { Some(root) => root, - // a match here in order to avoid some error during runtime browser test - None => { - let body = document.create_element("body").ok().unwrap(); - body - } + None => document.create_element("body").ok().unwrap(), }; Self { diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index cef1bc14c..90fe84d04 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -211,7 +211,7 @@ pub async fn run_with_props(root: Component, root_props: T websys_dom.apply_edits(edits.edits); } - let work_loop = ric_raf::RafLoop::new(); + let mut work_loop = ric_raf::RafLoop::new(); loop { log::trace!("waiting for work"); diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 2ac0668b9..89819fff0 100644 --- a/packages/web/src/ric_raf.rs +++ b/packages/web/src/ric_raf.rs @@ -7,6 +7,7 @@ //! Because RIC doesn't work on Safari, we polyfill using the "ricpolyfill.js" file and use some basic detection to see //! if RIC is available. +use futures_util::StreamExt; use gloo_timers::future::TimeoutFuture; use js_sys::Function; use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; @@ -14,21 +15,21 @@ use web_sys::{window, Window}; pub(crate) struct RafLoop { window: Window, - ric_receiver: async_channel::Receiver, - raf_receiver: async_channel::Receiver<()>, + ric_receiver: futures_channel::mpsc::UnboundedReceiver, + raf_receiver: futures_channel::mpsc::UnboundedReceiver<()>, ric_closure: Closure, raf_closure: Closure, } impl RafLoop { pub fn new() -> Self { - let (raf_sender, raf_receiver) = async_channel::unbounded(); + let (raf_sender, raf_receiver) = futures_channel::mpsc::unbounded(); let raf_closure: Closure = Closure::wrap(Box::new(move |_v: JsValue| { - raf_sender.try_send(()).unwrap() + raf_sender.unbounded_send(()).unwrap() })); - let (ric_sender, ric_receiver) = async_channel::unbounded(); + let (ric_sender, ric_receiver) = futures_channel::mpsc::unbounded(); let has_idle_callback = { let bo = window().unwrap().dyn_into::().unwrap(); @@ -45,7 +46,7 @@ impl RafLoop { 10 }; - ric_sender.try_send(time_remaining).unwrap() + ric_sender.unbounded_send(time_remaining).unwrap() })); // execute the polyfill for safari @@ -64,16 +65,16 @@ impl RafLoop { } } /// waits for some idle time and returns a timeout future that expires after the idle time has passed - pub async fn wait_for_idle_time(&self) -> TimeoutFuture { + pub async fn wait_for_idle_time(&mut self) -> TimeoutFuture { let ric_fn = self.ric_closure.as_ref().dyn_ref::().unwrap(); let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap(); - let deadline = self.ric_receiver.recv().await.unwrap(); + let deadline = self.ric_receiver.next().await.unwrap(); TimeoutFuture::new(deadline) } - pub async fn wait_for_raf(&self) { + pub async fn wait_for_raf(&mut self) { let raf_fn = self.raf_closure.as_ref().dyn_ref::().unwrap(); let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap(); - self.raf_receiver.recv().await.unwrap(); + self.raf_receiver.next().await.unwrap(); } } From 06ea624eecc132dc2a27fa8f6b2e2605126caeb4 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 15 Feb 2022 10:27:11 -0600 Subject: [PATCH 225/256] Make all packages which require futures-channel ask for the same version If they aren't the same then Cargo cannot resolve a working version for some reason. --- packages/core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 03b378e88..4126eb47f 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -32,7 +32,7 @@ smallvec = "1.6" slab = "0.4" -futures-channel = "0.3" +futures-channel = "0.3.21" # used for noderefs once_cell = "1.8" From a6cbe233b7f40763faa8e247b4264fba07f09c08 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 13:38:06 -0500 Subject: [PATCH 226/256] fix: empty values on desktop --- packages/desktop/src/controller.rs | 2 -- packages/desktop/src/events.rs | 3 +-- packages/html/src/events.rs | 2 ++ 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 7d9a2f432..8497a42e9 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -99,8 +99,6 @@ impl DesktopController { view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) .unwrap(); } - } else { - println!("waiting for ready"); } } } diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index a991ec788..7be4273e9 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -23,8 +23,7 @@ impl IpcMessage { } pub(crate) fn parse_ipc_message(payload: &str) -> Option { - let mm = serde_json::from_str(payload); - match mm { + match serde_json::from_str(payload) { Ok(message) => Some(message), Err(e) => { log::error!("could not parse IPC message, error: {e}"); diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 8067e6314..4336d2476 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -486,6 +486,8 @@ pub mod on { #[derive(Debug)] pub struct FormData { pub value: String, + + #[serde(default)] pub values: HashMap, /* DOMEvent: Send + SyncTarget relatedTarget */ } From ba17b57cdd244946ca780e3183cd7c9a43648380 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 13:44:05 -0500 Subject: [PATCH 227/256] fix: also include values in onchange --- packages/interpreter/src/interpreter.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index b1271b303..76da73752 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -2,7 +2,7 @@ export function main() { let root = window.document.getElementById("main"); if (root != null) { window.interpreter = new Interpreter(root); - window.ipc.postMessage(serializeIpcMessage("initialize")) + window.ipc.postMessage(serializeIpcMessage("initialize")); } } export class Interpreter { @@ -207,7 +207,9 @@ export class Interpreter { event.preventDefault(); const href = target.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage(serializeIpcMessage("browser_open", { href })) + window.ipc.postMessage( + serializeIpcMessage("browser_open", { href }) + ); } } } @@ -261,12 +263,13 @@ export class Interpreter { if (realId == null) { return; } - window.ipc.postMessage(serializeIpcMessage( - "user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - })); + window.ipc.postMessage( + serializeIpcMessage("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }) + ); } }; this.NewEventListener(edit.event_name, edit.root, handler); @@ -342,6 +345,7 @@ export function serialize_event(event) { } return { value: value, + values: {}, }; } case "input": From 61f9b9dd83a3151b3ee4054792e7b7bc5eb2af95 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 13:46:14 -0500 Subject: [PATCH 228/256] fix: remove serde attr --- packages/html/src/events.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 4336d2476..8067e6314 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -486,8 +486,6 @@ pub mod on { #[derive(Debug)] pub struct FormData { pub value: String, - - #[serde(default)] pub values: HashMap, /* DOMEvent: Send + SyncTarget relatedTarget */ } From ee2e986a307b385e88ad82ca604dd2d34e06ec29 Mon Sep 17 00:00:00 2001 From: Denis Richartz Date: Wed, 16 Feb 2022 15:34:43 +0100 Subject: [PATCH 229/256] fix unnecessary div --- packages/router/src/components/router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index bf23645f5..df1daab78 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -33,6 +33,6 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { } cx.render(rsx!( - div { &cx.props.children } + &cx.props.children )) } From b71cf6ed4ad4367da264827b55fbec537f631d41 Mon Sep 17 00:00:00 2001 From: Denis Richartz Date: Wed, 16 Feb 2022 16:11:57 +0100 Subject: [PATCH 230/256] cargo fmt --- packages/router/src/components/router.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index df1daab78..c117c6650 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -32,7 +32,5 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { cx.props.onchange.call(path.to_string()); } - cx.render(rsx!( - &cx.props.children - )) + cx.render(rsx!(&cx.props.children)) } From aa60971c5c9209d42b3f39b636e757c910a6f61e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:03:53 -0500 Subject: [PATCH 231/256] fix: remove preventdefault on form --- packages/html/src/elements.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 3d160211e..c6b1cb7e0 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1101,12 +1101,6 @@ impl label { } } -impl form { - pub fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { - cx.attr("dioxus-prevent-default", val, None, false) - } -} - builder_constructors! { // SVG components /// Build a From c6bdb5db76629e7fae77c110a21509d07ece2e89 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:08:57 -0500 Subject: [PATCH 232/256] fix: login form --- examples/login_form.rs | 68 +++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index fdd1843ce..62c6f982b 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -3,14 +3,14 @@ use dioxus::events::*; use dioxus::prelude::*; -use dioxus::router::{Link, Router, Route, RouterService}; +use dioxus::router::{Link, Route, Router, RouterService}; fn main() { dioxus::desktop::launch(APP); } static APP: Component = |cx| { - cx.render(rsx!{ + cx.render(rsx! { Router { Route { to: "/", home() } Route { to: "/login", login() } @@ -25,65 +25,45 @@ fn home(cx: Scope) -> Element { }) } - fn login(cx: Scope) -> Element { - let username = use_state(&cx, String::new); - let password = use_state(&cx, String::new); - let service = cx.consume_context::()?; - let onsubmit = move |_| { - cx.push_future({ - let (username, password) = (username.get().clone(), password.get().clone()); - let service = service.clone(); + let onsubmit = move |evt: FormEvent| { + to_owned![service]; + let username = evt.values["username"].clone(); + let password = evt.values["password"].clone(); - async move { - let params = [ - ("username", username.to_string()), - ("password", password.to_string()) - ]; + cx.spawn(async move { + let resp = reqwest::Client::new() + .post("http://localhost/login") + .form(&[("username", username), ("password", password)]) + .send() + .await; - let resp = reqwest::Client::new() - .post("http://localhost/login") - .form(¶ms) - .send() - .await; + match resp { + // Parse data from here, such as storing a response token + Ok(data) => service.push_route("/"), - match resp { - Ok(data) => { - // Parse data from here, such as storing a response token - service.push_route("/"); - } - Err(err) => {} //Handle any errors from the fetch here - } + //Handle any errors from the fetch here + Err(err) => {} } }); }; - cx.render(rsx!{ + cx.render(rsx! { h1 { "Login" } form { onsubmit: onsubmit, - // Prevent the default behavior of to post - prevent_default: "onsubmit", - input { - oninput: move |evt| username.set(evt.value.clone()) - } - label { - "Username" - } + prevent_default: "onsubmit", // Prevent the default behavior of to post + input { r#type: "text" } + label { "Username" } br {} - input { - oninput: move |evt| password.set(evt.value.clone()), - r#type: "password" - } + input { r#type: "password" } label { "Password" } br {} - button { - "Login" - } + button { "Login" } } }) -} \ No newline at end of file +} From babe8627394c47491348b7e3cec54ce02a31e3b2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:11:31 -0500 Subject: [PATCH 233/256] fix: login example to use proper methods --- examples/login_form.rs | 23 +++++++++++------------ packages/router/src/hooks/use_route.rs | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index 62c6f982b..cdf05d7c3 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -3,20 +3,20 @@ use dioxus::events::*; use dioxus::prelude::*; -use dioxus::router::{Link, Route, Router, RouterService}; +use dioxus::router::{use_router, Link, Route, Router}; fn main() { - dioxus::desktop::launch(APP); + dioxus::desktop::launch(app); } -static APP: Component = |cx| { +fn app(cx: Scope) -> Element { cx.render(rsx! { Router { Route { to: "/", home() } Route { to: "/login", login() } } }) -}; +} fn home(cx: Scope) -> Element { cx.render(rsx! { @@ -26,7 +26,7 @@ fn home(cx: Scope) -> Element { } fn login(cx: Scope) -> Element { - let service = cx.consume_context::()?; + let service = use_router(&cx); let onsubmit = move |evt: FormEvent| { to_owned![service]; @@ -42,10 +42,10 @@ fn login(cx: Scope) -> Element { match resp { // Parse data from here, such as storing a response token - Ok(data) => service.push_route("/"), + Ok(_data) => service.push_route("/"), //Handle any errors from the fetch here - Err(err) => {} + Err(_err) => {} } }); }; @@ -55,13 +55,12 @@ fn login(cx: Scope) -> Element { form { onsubmit: onsubmit, prevent_default: "onsubmit", // Prevent the default behavior of to post - input { r#type: "text" } + + input { "type": "text" } label { "Username" } br {} - input { r#type: "password" } - label { - "Password" - } + input { "type": "password" } + label { "Password" } br {} button { "Login" } } diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 31a157a8f..4de6dff12 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -106,7 +106,7 @@ impl Drop for UseRouteListener { } /// This hook provides access to the `RouterService` for the app. -pub fn use_router(cx: &ScopeState) -> &RouterService { +pub fn use_router(cx: &ScopeState) -> &Rc { cx.use_hook(|_| { cx.consume_context::() .expect("Cannot call use_route outside the scope of a Router component") From 4d965c8571774c59c5c6d6a3223a747301da4949 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:16:03 -0500 Subject: [PATCH 234/256] feat: simplify login example --- examples/login_form.rs | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index cdf05d7c3..1613cdc63 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -10,39 +10,22 @@ fn main() { } fn app(cx: Scope) -> Element { - cx.render(rsx! { - Router { - Route { to: "/", home() } - Route { to: "/login", login() } - } - }) -} - -fn home(cx: Scope) -> Element { - cx.render(rsx! { - h1 { "Welcome Home" } - Link { to: "/login", "Login" } - }) -} - -fn login(cx: Scope) -> Element { - let service = use_router(&cx); - let onsubmit = move |evt: FormEvent| { - to_owned![service]; - let username = evt.values["username"].clone(); - let password = evt.values["password"].clone(); - cx.spawn(async move { let resp = reqwest::Client::new() .post("http://localhost/login") - .form(&[("username", username), ("password", password)]) + .form(&[ + ("username", &evt.values["username"]), + ("password", &evt.values["password"]), + ]) .send() .await; match resp { // Parse data from here, such as storing a response token - Ok(_data) => service.push_route("/"), + Ok(_data) => { + println!("Login successful"); + } //Handle any errors from the fetch here Err(_err) => {} From b57987cfb1964d761eeb85f4019cabfcd7625f2c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:17:47 -0500 Subject: [PATCH 235/256] chore: clean up example a bit --- examples/login_form.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index 1613cdc63..7fd91262d 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -3,7 +3,6 @@ use dioxus::events::*; use dioxus::prelude::*; -use dioxus::router::{use_router, Link, Route, Router}; fn main() { dioxus::desktop::launch(app); @@ -23,12 +22,10 @@ fn app(cx: Scope) -> Element { match resp { // Parse data from here, such as storing a response token - Ok(_data) => { - println!("Login successful"); - } + Ok(_data) => println!("Login successful"), //Handle any errors from the fetch here - Err(_err) => {} + Err(_err) => println!("Login failed"), } }); }; From 3873cd1a60e6bc6371e73fe7c378cc2fb2c60407 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:40:45 -0500 Subject: [PATCH 236/256] fix: remove unused depds --- packages/core-macro/Cargo.toml | 1 - packages/desktop/Cargo.toml | 4 +--- packages/router/Cargo.toml | 2 +- packages/web/Cargo.toml | 5 ++--- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index eb8210923..8026bcdaf 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,7 +15,6 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] -once_cell = "1.8" proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } quote = "1.0" diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 2622355eb..6fd277ba4 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -13,12 +13,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } -argh = "0.1.7" serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" log = "0.4.14" -html-escape = "0.2.9" wry = { version = "0.13.1" } futures-channel = "0.3.21" tokio = { version = "1.16.1", features = [ @@ -27,7 +25,6 @@ tokio = { version = "1.16.1", features = [ "rt", "time", ], optional = true, default-features = false } -dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" @@ -47,5 +44,6 @@ ayatana = ["wry/ayatana"] [dev-dependencies] +dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } dioxus-hooks = { path = "../hooks" } # image = "0.24.0" # enable this when generating a new desktop image diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 239a3c5d8..221372b50 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -16,8 +16,8 @@ dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } serde = "1" -url = "2.2.2" serde_urlencoded = "0.7" +# url = "2.2.2" # for wasm web-sys = { version = "0.3", features = [ diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 31a7ff6be..333838398 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -15,13 +15,10 @@ dioxus-core = { path = "../core", version = "^0.1.9" } dioxus-html = { path = "../html", version = "^0.1.6" } js-sys = "0.3.56" wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] } -lazy_static = "1.4.0" wasm-bindgen-futures = "0.4.29" log = { version = "0.4.14", features = ["release_max_level_off"] } fxhash = "0.2.1" -wasm-logger = "0.2.0" console_error_panic_hook = { version = "0.1.7", optional = true } -wasm-bindgen-test = "0.3.29" once_cell = "1.9.0" anyhow = "1.0.53" gloo-timers = { version = "0.2.3", features = ["futures"] } @@ -80,3 +77,5 @@ panic_hook = ["console_error_panic_hook"] dioxus-core-macro = { path = "../core-macro" } wasm-bindgen-test = "0.3.29" dioxus-ssr = { path = "../ssr" } +wasm-logger = "0.2.0" + From d461ffc01131e76e8dc8044f3476db056ee127e7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 10:38:51 -0500 Subject: [PATCH 237/256] feat: integrate fermi --- Cargo.toml | 18 +--- docs/fermi/.gitignore | 1 + docs/fermi/book.toml | 6 ++ docs/fermi/src/SUMMARY.md | 3 + docs/fermi/src/chapter_1.md | 1 + examples/fermi.rs | 30 ++++++ packages/fermi/Cargo.toml | 14 +++ packages/fermi/README.md | 92 ++++++++++++++++++ packages/fermi/src/atoms/atom.rs | 28 ++++++ packages/fermi/src/atoms/atomfamily.rs | 25 +++++ packages/fermi/src/atoms/atomref.rs | 25 +++++ packages/fermi/src/atoms/selector.rs | 0 packages/fermi/src/atoms/selectorfamily.rs | 0 packages/fermi/src/callback.rs | 51 ++++++++++ packages/fermi/src/hooks/atom_ref.rs | 62 +++++++++++++ packages/fermi/src/hooks/atom_root.rs | 11 +++ packages/fermi/src/hooks/init_atom_root.rs | 11 +++ packages/fermi/src/hooks/read.rs | 36 +++++++ packages/fermi/src/hooks/set.rs | 13 +++ packages/fermi/src/lib.rs | 59 ++++++++++++ packages/fermi/src/root.rs | 103 +++++++++++++++++++++ src/lib.rs | 3 + tests/fermi.rs | 53 +++++++++++ 23 files changed, 630 insertions(+), 15 deletions(-) create mode 100644 docs/fermi/.gitignore create mode 100644 docs/fermi/book.toml create mode 100644 docs/fermi/src/SUMMARY.md create mode 100644 docs/fermi/src/chapter_1.md create mode 100644 examples/fermi.rs create mode 100644 packages/fermi/Cargo.toml create mode 100644 packages/fermi/README.md create mode 100644 packages/fermi/src/atoms/atom.rs create mode 100644 packages/fermi/src/atoms/atomfamily.rs create mode 100644 packages/fermi/src/atoms/atomref.rs create mode 100644 packages/fermi/src/atoms/selector.rs create mode 100644 packages/fermi/src/atoms/selectorfamily.rs create mode 100644 packages/fermi/src/callback.rs create mode 100644 packages/fermi/src/hooks/atom_ref.rs create mode 100644 packages/fermi/src/hooks/atom_root.rs create mode 100644 packages/fermi/src/hooks/init_atom_root.rs create mode 100644 packages/fermi/src/hooks/read.rs create mode 100644 packages/fermi/src/hooks/set.rs create mode 100644 packages/fermi/src/lib.rs create mode 100644 packages/fermi/src/root.rs create mode 100644 tests/fermi.rs diff --git a/Cargo.toml b/Cargo.toml index 3f0d50187..bd15590ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ dioxus-core = { path = "./packages/core", version = "^0.1.9" } dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } +fermi = { path = "./packages/fermi", version = "^0.1.0", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } dioxus-desktop = { path = "./packages/desktop", version = "^0.1.6", optional = true } @@ -36,20 +37,6 @@ web = ["dioxus-web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] -devtool = ["dioxus-desktop/devtool"] -fullscreen = ["dioxus-desktop/fullscreen"] -transparent = ["dioxus-desktop/transparent"] - -tray = ["dioxus-desktop/tray"] -ayatana = ["dioxus-desktop/ayatana"] - -# "dioxus-router/web" -# "dioxus-router/desktop" -# desktop = ["dioxus-desktop", "dioxus-router/desktop"] -# mobile = ["dioxus-mobile"] -# liveview = ["dioxus-liveview"] - - [workspace] members = [ "packages/core", @@ -61,6 +48,7 @@ members = [ "packages/desktop", "packages/mobile", "packages/interpreter", + "packages/fermi", ] [dev-dependencies] @@ -75,4 +63,4 @@ serde_json = "1.0.79" rand = { version = "0.8.4", features = ["small_rng"] } tokio = { version = "1.16.1", features = ["full"] } reqwest = { version = "0.11.9", features = ["json"] } -dioxus = { path = ".", features = ["desktop", "ssr", "router"] } +dioxus = { path = ".", features = ["desktop", "ssr", "router", "fermi"] } diff --git a/docs/fermi/.gitignore b/docs/fermi/.gitignore new file mode 100644 index 000000000..7585238ef --- /dev/null +++ b/docs/fermi/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/fermi/book.toml b/docs/fermi/book.toml new file mode 100644 index 000000000..dd6d2eacb --- /dev/null +++ b/docs/fermi/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Jonathan Kelley"] +language = "en" +multilingual = false +src = "src" +title = "Fermi Guide" diff --git a/docs/fermi/src/SUMMARY.md b/docs/fermi/src/SUMMARY.md new file mode 100644 index 000000000..7390c8289 --- /dev/null +++ b/docs/fermi/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/docs/fermi/src/chapter_1.md b/docs/fermi/src/chapter_1.md new file mode 100644 index 000000000..b743fda35 --- /dev/null +++ b/docs/fermi/src/chapter_1.md @@ -0,0 +1 @@ +# Chapter 1 diff --git a/examples/fermi.rs b/examples/fermi.rs new file mode 100644 index 000000000..2be01d019 --- /dev/null +++ b/examples/fermi.rs @@ -0,0 +1,30 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use fermi::prelude::*; + +fn main() { + dioxus::desktop::launch(app) +} + +static NAME: Atom = |_| "world".to_string(); + +fn app(cx: Scope) -> Element { + let name = use_read(&cx, NAME); + + cx.render(rsx! { + div { "hello {name}!" } + Child {} + }) +} + +fn Child(cx: Scope) -> Element { + let set_name = use_set(&cx, NAME); + + cx.render(rsx! { + button { + onclick: move |_| set_name("dioxus".to_string()), + "reset name" + } + }) +} diff --git a/packages/fermi/Cargo.toml b/packages/fermi/Cargo.toml new file mode 100644 index 000000000..48fe377c7 --- /dev/null +++ b/packages/fermi/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fermi" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-core = { path = "../core" } +im-rc = { version = "15.0.0", features = ["serde"] } +log = "0.4.14" + +[dev-dependencies] +closure = "0.3.0" diff --git a/packages/fermi/README.md b/packages/fermi/README.md new file mode 100644 index 000000000..0923aa6f6 --- /dev/null +++ b/packages/fermi/README.md @@ -0,0 +1,92 @@ + +

+

Fermi ⚛

+

+ Atom-based global state management solution for Dioxus +

+
+ + +
+ +----- + +Fermi is a global state management solution for Dioxus that's as easy as `use_state`. + +Inspired by atom-based state management solutions, all state in Fermi starts as an `atom`: + +```rust +static NAME: Atom<&str> = |_| "Dioxus"; +``` + +From anywhere in our app, we can read our the value of our atom: + +```rust +fn NameCard(cx: Scope) -> Element { + let name = use_read(&cx, NAME); + cx.render(rsx!{ h1 { "Hello, {name}"} }) +} +``` + +We can also set the value of our atom, also from anywhere in our app: + +```rust +fn NameCard(cx: Scope) -> Element { + let set_name = use_set(&cx, NAME); + cx.render(rsx!{ + button { + onclick: move |_| set_name("Fermi"), + "Set name to fermi" + } + }) +} +``` + +It's that simple! + +## Installation +Fermi is currently under construction, so you have to use the `master` branch to get started. + +```rust +[depdencies] +fermi = { git = "https://github.com/dioxuslabs/fermi" } +``` + + +## Running examples + +The examples here use Dioxus Desktop to showcase their functionality. To run an example, use +``` +$ cargo run --example EXAMPLE +``` + +## Features + +Broadly our feature set to required to be released includes: +- [x] Support for Atoms +- [x] Support for AtomRef (for values that aren't clone) +- [ ] Support for Atom Families +- [ ] Support for memoized Selectors +- [ ] Support for memoized SelectorFamilies +- [ ] Support for UseFermiCallback for access to fermi from async diff --git a/packages/fermi/src/atoms/atom.rs b/packages/fermi/src/atoms/atom.rs new file mode 100644 index 000000000..2b99174b5 --- /dev/null +++ b/packages/fermi/src/atoms/atom.rs @@ -0,0 +1,28 @@ +use crate::{AtomId, AtomRoot, Readable, Writable}; + +pub type Atom = fn(AtomBuilder) -> T; +pub struct AtomBuilder; + +impl Readable for Atom { + fn read(&self, _root: AtomRoot) -> Option { + todo!() + } + fn init(&self) -> V { + (*self)(AtomBuilder) + } + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +impl Writable for Atom { + fn write(&self, _root: AtomRoot, _value: V) { + todo!() + } +} + +#[test] +fn atom_compiles() { + static TEST_ATOM: Atom<&str> = |_| "hello"; + dbg!(TEST_ATOM.init()); +} diff --git a/packages/fermi/src/atoms/atomfamily.rs b/packages/fermi/src/atoms/atomfamily.rs new file mode 100644 index 000000000..60fc22bcb --- /dev/null +++ b/packages/fermi/src/atoms/atomfamily.rs @@ -0,0 +1,25 @@ +use crate::{AtomId, AtomRoot, Readable, Writable}; +use im_rc::HashMap as ImMap; + +pub struct AtomFamilyBuilder; +pub type AtomFamily = fn(AtomFamilyBuilder) -> ImMap; + +impl Readable> for AtomFamily { + fn read(&self, _root: AtomRoot) -> Option> { + todo!() + } + + fn init(&self) -> ImMap { + (*self)(AtomFamilyBuilder) + } + + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +impl Writable> for AtomFamily { + fn write(&self, _root: AtomRoot, _value: ImMap) { + todo!() + } +} diff --git a/packages/fermi/src/atoms/atomref.rs b/packages/fermi/src/atoms/atomref.rs new file mode 100644 index 000000000..59e412031 --- /dev/null +++ b/packages/fermi/src/atoms/atomref.rs @@ -0,0 +1,25 @@ +use crate::{AtomId, AtomRoot, Readable}; +use std::cell::RefCell; + +pub struct AtomRefBuilder; +pub type AtomRef = fn(AtomRefBuilder) -> T; + +impl Readable> for AtomRef { + fn read(&self, _root: AtomRoot) -> Option> { + todo!() + } + + fn init(&self) -> RefCell { + RefCell::new((*self)(AtomRefBuilder)) + } + + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +#[test] +fn atom_compiles() { + static TEST_ATOM: AtomRef> = |_| vec![]; + dbg!(TEST_ATOM.init()); +} diff --git a/packages/fermi/src/atoms/selector.rs b/packages/fermi/src/atoms/selector.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/fermi/src/atoms/selectorfamily.rs b/packages/fermi/src/atoms/selectorfamily.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/fermi/src/callback.rs b/packages/fermi/src/callback.rs new file mode 100644 index 000000000..284fb6439 --- /dev/null +++ b/packages/fermi/src/callback.rs @@ -0,0 +1,51 @@ +use std::{future::Future, rc::Rc}; + +use dioxus_core::prelude::*; + +use crate::{Atom, AtomRoot, Readable, Writable}; + +#[derive(Clone)] +pub struct CallbackApi { + root: Rc, +} + +impl CallbackApi { + // get the current value of the atom + pub fn get(&self, atom: impl Readable) -> &V { + todo!() + } + + // get the current value of the atom in its RC container + pub fn get_rc(&self, atom: impl Readable) -> &Rc { + todo!() + } + + // set the current value of the atom + pub fn set(&self, atom: impl Writable, value: V) { + todo!() + } +} + +pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi { + todo!() +} + +macro_rules! use_callback { + (&$cx:ident, [$($cap:ident),*], move || $body:expr) => { + move || { + $( + #[allow(unused_mut)] + let mut $cap = $cap.to_owned(); + )* + $cx.spawn($body); + } + }; +} + +#[macro_export] +macro_rules! to_owned { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.to_owned(); + )*} +} diff --git a/packages/fermi/src/hooks/atom_ref.rs b/packages/fermi/src/hooks/atom_ref.rs new file mode 100644 index 000000000..da9ad9367 --- /dev/null +++ b/packages/fermi/src/hooks/atom_ref.rs @@ -0,0 +1,62 @@ +use crate::{use_atom_root, AtomId, AtomRef, AtomRoot, Readable}; +use dioxus_core::{ScopeId, ScopeState}; +use std::{ + cell::{Ref, RefCell, RefMut}, + rc::Rc, +}; + +/// +/// +/// +/// +/// +/// +/// +/// +pub fn use_atom_ref(cx: &ScopeState, atom: AtomRef) -> &UseAtomRef { + let root = use_atom_root(cx); + + cx.use_hook(|_| { + root.initialize(atom); + UseAtomRef { + ptr: atom.unique_id(), + root: root.clone(), + scope_id: cx.scope_id(), + value: root.register(atom, cx.scope_id()), + } + }) +} + +pub struct UseAtomRef { + ptr: AtomId, + value: Rc>, + root: Rc, + scope_id: ScopeId, +} + +impl UseAtomRef { + pub fn read(&self) -> Ref { + self.value.borrow() + } + + pub fn write(&self) -> RefMut { + self.root.force_update(self.ptr); + self.value.borrow_mut() + } + + pub fn write_silent(&self) -> RefMut { + self.root.force_update(self.ptr); + self.value.borrow_mut() + } + + pub fn set(&self, new: T) { + self.root.force_update(self.ptr); + self.root.set(self.ptr, new); + } +} + +impl Drop for UseAtomRef { + fn drop(&mut self) { + self.root.unsubscribe(self.ptr, self.scope_id) + } +} diff --git a/packages/fermi/src/hooks/atom_root.rs b/packages/fermi/src/hooks/atom_root.rs new file mode 100644 index 000000000..d2945025b --- /dev/null +++ b/packages/fermi/src/hooks/atom_root.rs @@ -0,0 +1,11 @@ +use crate::AtomRoot; +use dioxus_core::ScopeState; +use std::rc::Rc; + +// Returns the atom root, initiaizing it at the root of the app if it does not exist. +pub fn use_atom_root(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| match cx.consume_context::() { + Some(root) => root, + None => cx.provide_root_context(AtomRoot::new(cx.schedule_update_any())), + }) +} diff --git a/packages/fermi/src/hooks/init_atom_root.rs b/packages/fermi/src/hooks/init_atom_root.rs new file mode 100644 index 000000000..edb8aafb8 --- /dev/null +++ b/packages/fermi/src/hooks/init_atom_root.rs @@ -0,0 +1,11 @@ +use crate::AtomRoot; +use dioxus_core::ScopeState; +use std::rc::Rc; + +// Initializes the atom root and retuns it; +pub fn use_init_atom_root(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| match cx.consume_context::() { + Some(ctx) => ctx, + None => cx.provide_context(AtomRoot::new(cx.schedule_update_any())), + }) +} diff --git a/packages/fermi/src/hooks/read.rs b/packages/fermi/src/hooks/read.rs new file mode 100644 index 000000000..b231c74d3 --- /dev/null +++ b/packages/fermi/src/hooks/read.rs @@ -0,0 +1,36 @@ +use crate::{use_atom_root, AtomId, AtomRoot, Readable}; +use dioxus_core::{ScopeId, ScopeState}; +use std::rc::Rc; + +pub fn use_read<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable) -> &'a V { + use_read_rc(cx, f).as_ref() +} + +pub fn use_read_rc<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable) -> &'a Rc { + let root = use_atom_root(cx); + + struct UseReadInner { + root: Rc, + id: AtomId, + scope_id: ScopeId, + value: Option>, + } + + impl Drop for UseReadInner { + fn drop(&mut self) { + self.root.unsubscribe(self.id, self.scope_id) + } + } + + let inner = cx.use_hook(|_| UseReadInner { + value: None, + root: root.clone(), + scope_id: cx.scope_id(), + id: f.unique_id(), + }); + + let value = inner.root.register(f, cx.scope_id()); + + inner.value = Some(value); + inner.value.as_ref().unwrap() +} diff --git a/packages/fermi/src/hooks/set.rs b/packages/fermi/src/hooks/set.rs new file mode 100644 index 000000000..6d77fbf8c --- /dev/null +++ b/packages/fermi/src/hooks/set.rs @@ -0,0 +1,13 @@ +use crate::{use_atom_root, Writable}; +use dioxus_core::ScopeState; +use std::rc::Rc; + +pub fn use_set<'a, T: 'static>(cx: &'a ScopeState, f: impl Writable) -> &'a Rc { + let root = use_atom_root(cx); + cx.use_hook(|_| { + let id = f.unique_id(); + let root = root.clone(); + root.initialize(f); + Rc::new(move |new| root.set(id, new)) as Rc + }) +} diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs new file mode 100644 index 000000000..023b5d286 --- /dev/null +++ b/packages/fermi/src/lib.rs @@ -0,0 +1,59 @@ +#![doc = include_str!("../README.md")] + +pub mod prelude { + pub use crate::*; +} + +mod root; +mod callback; + + +pub use atoms::*; +pub use hooks::*; +pub use root::*; +pub use callback::*; + +mod atoms { + mod atom; + mod atomfamily; + mod atomref; + mod selector; + mod selectorfamily; + + pub use atom::*; + pub use atomfamily::*; + pub use atomref::*; + pub use selector::*; + pub use selectorfamily::*; +} + +pub mod hooks { + mod atom_ref; + mod atom_root; + mod init_atom_root; + mod read; + mod set; + pub use atom_ref::*; + pub use atom_root::*; + pub use init_atom_root::*; + pub use read::*; + pub use set::*; +} + +/// All Atoms are `Readable` - they support reading their value. +/// +/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors. +/// It is not very useful for your own code, but could be used to build new Atom primitives. +pub trait Readable { + fn read(&self, root: AtomRoot) -> Option; + fn init(&self) -> V; + fn unique_id(&self) -> AtomId; +} + +/// All Atoms are `Writable` - they support writing their value. +/// +/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors. +/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors +pub trait Writable: Readable { + fn write(&self, root: AtomRoot, value: V); +} diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs new file mode 100644 index 000000000..5c07083be --- /dev/null +++ b/packages/fermi/src/root.rs @@ -0,0 +1,103 @@ +use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc}; + +use dioxus_core::ScopeId; +use im_rc::HashSet; + +use crate::Readable; + +pub type AtomId = *const (); + +pub struct AtomRoot { + pub atoms: RefCell>, + pub update_any: Rc, +} + +pub struct Slot { + pub value: Rc, + pub subscribers: HashSet, +} + +impl AtomRoot { + pub fn new(update_any: Rc) -> Self { + Self { + update_any, + atoms: RefCell::new(HashMap::new()), + } + } + + pub fn initialize(&self, f: impl Readable) { + let id = f.unique_id(); + if self.atoms.borrow().get(&id).is_none() { + self.atoms.borrow_mut().insert( + id, + Slot { + value: Rc::new(f.init()), + subscribers: HashSet::new(), + }, + ); + } + } + + pub fn register(&self, f: impl Readable, scope: ScopeId) -> Rc { + log::trace!("registering atom {:?}", f.unique_id()); + + let mut atoms = self.atoms.borrow_mut(); + + // initialize the value if it's not already initialized + if let Some(slot) = atoms.get_mut(&f.unique_id()) { + slot.subscribers.insert(scope); + slot.value.clone().downcast().unwrap() + } else { + let value = Rc::new(f.init()); + let mut subscribers = HashSet::new(); + subscribers.insert(scope); + + atoms.insert( + f.unique_id(), + Slot { + value: value.clone(), + subscribers, + }, + ); + value + } + } + + pub fn set(&self, ptr: AtomId, value: V) { + let mut atoms = self.atoms.borrow_mut(); + + if let Some(slot) = atoms.get_mut(&ptr) { + slot.value = Rc::new(value); + log::trace!("found item with subscribers {:?}", slot.subscribers); + + for scope in &slot.subscribers { + log::trace!("updating subcsriber"); + (self.update_any)(*scope); + } + } else { + log::trace!("no atoms found for {:?}", ptr); + } + } + + pub fn unsubscribe(&self, ptr: AtomId, scope: ScopeId) { + let mut atoms = self.atoms.borrow_mut(); + + if let Some(slot) = atoms.get_mut(&ptr) { + slot.subscribers.remove(&scope); + } + } + + // force update of all subscribers + pub fn force_update(&self, ptr: AtomId) { + if let Some(slot) = self.atoms.borrow_mut().get(&ptr) { + for scope in slot.subscribers.iter() { + log::trace!("updating subcsriber"); + (self.update_any)(*scope); + } + } + } + + pub fn read(&self, _f: impl Readable) -> &V { + todo!() + } +} diff --git a/src/lib.rs b/src/lib.rs index d7b479a07..2db210bee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -340,6 +340,9 @@ pub use dioxus_web as web; #[cfg(feature = "desktop")] pub use dioxus_desktop as desktop; +#[cfg(feature = "fermi")] +pub use fermi; + // #[cfg(feature = "mobile")] // pub use dioxus_mobile as mobile; diff --git a/tests/fermi.rs b/tests/fermi.rs new file mode 100644 index 000000000..efef5c641 --- /dev/null +++ b/tests/fermi.rs @@ -0,0 +1,53 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use fermi::*; + +#[test] +fn test_fermi() { + let mut app = VirtualDom::new(App); + app.rebuild(); +} + +static TITLE: Atom = |_| "".to_string(); +static USERS: AtomFamily = |_| Default::default(); + +fn App(cx: Scope) -> Element { + cx.render(rsx!( + Leaf { id: 0 } + Leaf { id: 1 } + Leaf { id: 2 } + )) +} + +#[derive(Debug, PartialEq, Props)] +struct LeafProps { + id: u32, +} + +fn Leaf(cx: Scope) -> Element { + let user = use_read(&cx, TITLE); + let user = use_read(&cx, USERS); + + use_coroutine(&cx, || { + // + async move { + // + } + }); + + rsx!(cx, div { + button { + onclick: move |_| {}, + "Start" + } + button { + onclick: move |_| {}, + "Stop" + } + button { + onclick: move |_| {}, + "Reverse" + } + }) +} From d095d8c407ed8bf3b57a4944922b61918dd17995 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 10:43:34 -0500 Subject: [PATCH 238/256] fix: clippy is happy --- packages/fermi/src/callback.rs | 6 ++++-- tests/fermi.rs | 11 ++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/fermi/src/callback.rs b/packages/fermi/src/callback.rs index 284fb6439..a7ca9155f 100644 --- a/packages/fermi/src/callback.rs +++ b/packages/fermi/src/callback.rs @@ -1,8 +1,10 @@ -use std::{future::Future, rc::Rc}; +#![allow(clippy::all, unused)] + +use std::rc::Rc; use dioxus_core::prelude::*; -use crate::{Atom, AtomRoot, Readable, Writable}; +use crate::{AtomRoot, Readable, Writable}; #[derive(Clone)] pub struct CallbackApi { diff --git a/tests/fermi.rs b/tests/fermi.rs index efef5c641..1149539f0 100644 --- a/tests/fermi.rs +++ b/tests/fermi.rs @@ -26,15 +26,8 @@ struct LeafProps { } fn Leaf(cx: Scope) -> Element { - let user = use_read(&cx, TITLE); - let user = use_read(&cx, USERS); - - use_coroutine(&cx, || { - // - async move { - // - } - }); + let _user = use_read(&cx, TITLE); + let _user = use_read(&cx, USERS); rsx!(cx, div { button { From a4df07f338cc08a3b71cd5730abf4a31e7d9766a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:16:04 -0500 Subject: [PATCH 239/256] fix: rustmft --- packages/fermi/src/atoms/selector.rs | 1 + packages/fermi/src/atoms/selectorfamily.rs | 1 + packages/fermi/src/lib.rs | 5 ++--- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/fermi/src/atoms/selector.rs b/packages/fermi/src/atoms/selector.rs index e69de29bb..8b1378917 100644 --- a/packages/fermi/src/atoms/selector.rs +++ b/packages/fermi/src/atoms/selector.rs @@ -0,0 +1 @@ + diff --git a/packages/fermi/src/atoms/selectorfamily.rs b/packages/fermi/src/atoms/selectorfamily.rs index e69de29bb..8b1378917 100644 --- a/packages/fermi/src/atoms/selectorfamily.rs +++ b/packages/fermi/src/atoms/selectorfamily.rs @@ -0,0 +1 @@ + diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs index 023b5d286..8ac4f1ccf 100644 --- a/packages/fermi/src/lib.rs +++ b/packages/fermi/src/lib.rs @@ -4,14 +4,13 @@ pub mod prelude { pub use crate::*; } -mod root; mod callback; - +mod root; pub use atoms::*; +pub use callback::*; pub use hooks::*; pub use root::*; -pub use callback::*; mod atoms { mod atom; From f4686150d7e2d62b6607a6a761c72cfe1b451479 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:19:28 -0500 Subject: [PATCH 240/256] ci: make workflows only run on PRs --- .github/workflows/macos.yml | 9 ++++----- .github/workflows/main.yml | 9 ++++----- .github/workflows/windows.yml | 9 ++++----- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d1351712a..718922a57 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,7 +1,10 @@ name: macOS tests on: - push: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master paths: - packages/** - examples/** @@ -9,10 +12,6 @@ on: - .github/** - lib.rs - Cargo.toml - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: - - master jobs: test: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e1ec15c0..9f8e0bac3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,10 @@ name: Rust CI on: - push: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master paths: - packages/** - examples/** @@ -9,10 +12,6 @@ on: - .github/** - lib.rs - Cargo.toml - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: - - master jobs: check: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 518da37b3..75953807a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,7 +1,10 @@ name: windows on: - push: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master paths: - packages/** - examples/** @@ -9,10 +12,6 @@ on: - .github/** - lib.rs - Cargo.toml - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: - - master jobs: test: From 6bc9dd36f9ae1d698282147f1a6fe08a46f8f6e5 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:27:13 -0500 Subject: [PATCH 241/256] fix: coverage --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9f8e0bac3..8521dd176 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -101,8 +101,9 @@ jobs: - name: Generate code coverage run: | apt-get update &&\ + apt-get install build-essential &&\ apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ - cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v2 with: From 7aeed4ccae47d0e28c667dde9779de8e4babdf47 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:30:25 -0500 Subject: [PATCH 242/256] ci: disable coverage --- .github/workflows/main.yml | 39 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8521dd176..128dc5179 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,22 +89,23 @@ jobs: command: clippy args: -- -D warnings - coverage: - name: Coverage - runs-on: ubuntu-latest - container: - image: xd009642/tarpaulin:develop-nightly - options: --security-opt seccomp=unconfined - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Generate code coverage - run: | - apt-get update &&\ - apt-get install build-essential &&\ - apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ - cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml - - name: Upload to codecov.io - uses: codecov/codecov-action@v2 - with: - fail_ci_if_error: false + # Coverage is disabled until we can fix it + # coverage: + # name: Coverage + # runs-on: ubuntu-latest + # container: + # image: xd009642/tarpaulin:develop-nightly + # options: --security-opt seccomp=unconfined + # steps: + # - name: Checkout repository + # uses: actions/checkout@v2 + # - name: Generate code coverage + # run: | + # apt-get update &&\ + # apt-get install build-essential &&\ + # apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ + # cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml + # - name: Upload to codecov.io + # uses: codecov/codecov-action@v2 + # with: + # fail_ci_if_error: false From d897d2148ab9afcfb484f7d1d6e9bc19dc56a4cb Mon Sep 17 00:00:00 2001 From: Malcolm Rebughini <9681621+malcolmrebughini@users.noreply.github.com> Date: Thu, 17 Feb 2022 19:17:32 -0800 Subject: [PATCH 243/256] docs(reference/platforms/ssr): fixing example and commands --- docs/reference/src/platforms/ssr.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/reference/src/platforms/ssr.md b/docs/reference/src/platforms/ssr.md index aef223dfd..09e32ba43 100644 --- a/docs/reference/src/platforms/ssr.md +++ b/docs/reference/src/platforms/ssr.md @@ -2,11 +2,8 @@ The Dioxus VirtualDom can be rendered to a string by traversing the Element Tree. This is implemented in the `ssr` crate where your Dioxus app can be directly rendered to HTML to be served by a web server. - - ## Setup - If you just want to render `rsx!` or a VirtualDom to HTML, check out the API docs. It's pretty simple: ```rust @@ -19,8 +16,7 @@ println!("{}", dioxus::ssr::render_vdom(&vdom)); println!( "{}", dioxus::ssr::render_lazy(rsx! { h1 { "Hello, world!" } } ); ``` - -However, for this guide, we're going to show how to use Dioxus SSR with `Axum`. +However, for this guide, we're going to show how to use Dioxus SSR with `Axum`. Make sure you have Rust and Cargo installed, and then create a new project: @@ -29,15 +25,16 @@ $ cargo new --bin demo $ cd app ``` -Add Dioxus with the `desktop` feature: +Add Dioxus with the `ssr` feature: ```shell $ cargo add dioxus --features ssr ``` Next, add all the Axum dependencies. This will be different if you're using a different Web Framework + ``` -$ cargo add dioxus tokio --features full +$ cargo add tokio --features full $ cargo add axum ``` @@ -45,12 +42,11 @@ Your dependencies should look roughly like this: ```toml [dependencies] -axum = "0.4.3" +axum = "0.4.5" dioxus = { version = "*", features = ["ssr"] } tokio = { version = "1.15.0", features = ["full"] } ``` - Now, setup your Axum app to respond on an endpoint. ```rust @@ -63,7 +59,11 @@ async fn main() { println!("listening on http://{}", addr); axum::Server::bind(&addr) - .serve(Router::new().route("/", get(app_endpoint))) + .serve( + Router::new() + .route("/", get(app_endpoint)) + .into_make_service(), + ) .await .unwrap(); } @@ -88,14 +88,14 @@ async fn app_endpoint() -> Html { } let mut app = VirtualDom::new(app); let _ = app.rebuild(); - + Html(dioxus::ssr::render_vdom(&app)) } ``` And that's it! -> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it *must* remain on the thread it started. We are working on loosening this requirement. +> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it _must_ remain on the thread it started. We are working on loosening this requirement. ## Future Steps From b93487282de4faaac3db5b830215c4b1dbdeb16f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 20:54:30 -0500 Subject: [PATCH 244/256] feat: move files around --- Cargo.toml | 4 +- examples/rsx_autocomplete.rs | 27 -- packages/core-macro/Cargo.toml | 2 +- packages/rsx/src/{rsx => }/component.rs | 0 packages/rsx/src/{rsx => }/element.rs | 0 packages/rsx/src/htm.rs | 443 ------------------------ packages/rsx/src/lib.rs | 101 +++++- packages/rsx/src/{rsx => }/node.rs | 0 packages/rsx/src/pretty.rs | 1 + packages/rsx/src/rsx/mod.rs | 95 ----- 10 files changed, 101 insertions(+), 572 deletions(-) delete mode 100644 examples/rsx_autocomplete.rs rename packages/rsx/src/{rsx => }/component.rs (100%) rename packages/rsx/src/{rsx => }/element.rs (100%) delete mode 100644 packages/rsx/src/htm.rs rename packages/rsx/src/{rsx => }/node.rs (100%) create mode 100644 packages/rsx/src/pretty.rs delete mode 100644 packages/rsx/src/rsx/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 871810a76..63f1c938c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ dioxus-core = { path = "./packages/core", version = "^0.1.9" } dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } -dioxus-macro-inner = { path = "./packages/rsx", optional = true } +dioxus-rsx = { path = "./packages/rsx", optional = true } fermi = { path = "./packages/fermi", version = "^0.1.0", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } @@ -30,7 +30,7 @@ dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.0.0", o [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro", "dioxus-macro-inner"] +macro = ["dioxus-core-macro", "dioxus-rsx"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] diff --git a/examples/rsx_autocomplete.rs b/examples/rsx_autocomplete.rs deleted file mode 100644 index 99afe7521..000000000 --- a/examples/rsx_autocomplete.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! This example shows that autocomplete works in RSX - -use dioxus::prelude::*; - -fn main() { - dioxus::desktop::launch(app); -} - -fn app(cx: Scope) -> Element { - cx.render(rsx! { - div { - onclick: move |_| { - } - // class: "asd", - // style { - // media: "Ad", - // } - // div { - - // } - // { - // let t = String::new(); - // t. - // } - } - }) -} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index 94f8e6e87..ed5d34e53 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,7 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] -dioxus-macro-inner = { path = "../macro-inner" } +dioxus-rsx = { path = "../rsx" } proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } quote = "1.0" diff --git a/packages/rsx/src/rsx/component.rs b/packages/rsx/src/component.rs similarity index 100% rename from packages/rsx/src/rsx/component.rs rename to packages/rsx/src/component.rs diff --git a/packages/rsx/src/rsx/element.rs b/packages/rsx/src/element.rs similarity index 100% rename from packages/rsx/src/rsx/element.rs rename to packages/rsx/src/element.rs diff --git a/packages/rsx/src/htm.rs b/packages/rsx/src/htm.rs deleted file mode 100644 index ca53096f6..000000000 --- a/packages/rsx/src/htm.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! -//! TODO: -//! - [ ] Support for VComponents -//! - [ ] Support for inline format in text -//! - [ ] Support for expressions in attribute positions -//! - [ ] Support for iterators -//! - [ ] support for inline html! -//! -//! -//! -//! -//! -//! -//! - -use { - proc_macro2::TokenStream as TokenStream2, - quote::{quote, ToTokens, TokenStreamExt}, - syn::{ - ext::IdentExt, - parse::{Parse, ParseStream}, - token, Error, Expr, ExprClosure, Ident, LitStr, Result, Token, - }, -}; - -// ============================================== -// Parse any stream coming from the html! macro -// ============================================== -pub struct HtmlRender { - kind: NodeOrList, -} - -impl Parse for HtmlRender { - fn parse(input: ParseStream) -> Result { - if input.peek(LitStr) { - return input.parse::()?.parse::(); - } - - // let __cx: Ident = s.parse()?; - // s.parse::()?; - // if elements are in an array, return a bumpalo::collections::Vec rather than a Node. - let kind = if input.peek(token::Bracket) { - let nodes_toks; - syn::bracketed!(nodes_toks in input); - let mut nodes: Vec> = vec![nodes_toks.parse()?]; - while nodes_toks.peek(Token![,]) { - nodes_toks.parse::()?; - nodes.push(nodes_toks.parse()?); - } - NodeOrList::List(NodeList(nodes)) - } else { - NodeOrList::Node(input.parse()?) - }; - Ok(HtmlRender { kind }) - } -} - -impl ToTokens for HtmlRender { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let new_toks = ToToksCtx::new(&self.kind).to_token_stream(); - - // create a lazy tree that accepts a bump allocator - let final_tokens = quote! { - dioxus::prelude::LazyNodes::new(move |__cx| { - let bump = __cx.bump(); - - #new_toks - }) - }; - - final_tokens.to_tokens(out_tokens); - } -} - -/// ============================================= -/// Parse any child as a node or list of nodes -/// ============================================= -/// - [ ] Allow iterators -/// -/// -enum NodeOrList { - Node(Node), - List(NodeList), -} - -impl ToTokens for ToToksCtx<&NodeOrList> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match self.inner { - NodeOrList::Node(node) => self.recurse(node).to_tokens(tokens), - NodeOrList::List(list) => self.recurse(list).to_tokens(tokens), - } - } -} - -struct NodeList(Vec>); - -impl ToTokens for ToToksCtx<&NodeList> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let nodes = self.inner.0.iter().map(|node| self.recurse(node)); - tokens.append_all(quote! { - dioxus::bumpalo::vec![in bump; - #(#nodes),* - ] - }); - } -} - -enum Node { - Element(Element), - Text(TextNode), -} - -impl ToTokens for ToToksCtx<&Node> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match &self.inner { - Node::Element(el) => self.recurse(el).to_tokens(tokens), - Node::Text(txt) => self.recurse(txt).to_tokens(tokens), - } - } -} - -impl Node { - fn _peek(s: ParseStream) -> bool { - (s.peek(Token![<]) && !s.peek2(Token![/])) || s.peek(token::Brace) || s.peek(LitStr) - } -} - -impl Parse for Node { - fn parse(s: ParseStream) -> Result { - Ok(if s.peek(Token![<]) { - Node::Element(s.parse()?) - } else { - Node::Text(s.parse()?) - }) - } -} - -/// ======================================= -/// Parse the VNode::Element type -/// ======================================= -/// - [ ] Allow VComponent -/// -/// -struct Element { - name: Ident, - attrs: Vec, - children: MaybeExpr>, -} - -impl ToTokens for ToToksCtx<&Element> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - // let __cx = self.__cx; - let name = &self.inner.name; - // let name = &self.inner.name.to_string(); - tokens.append_all(quote! { - __cx.element(dioxus_elements::#name) - // dioxus::builder::ElementBuilder::new( #name) - }); - for attr in self.inner.attrs.iter() { - self.recurse(attr).to_tokens(tokens); - } - - // if is_valid_svg_tag(&name.to_string()) { - // tokens.append_all(quote! { - // .namespace(Some("http://www.w3.org/2000/svg")) - // }); - // } - - match &self.inner.children { - MaybeExpr::Expr(expr) => tokens.append_all(quote! { - .children(#expr) - }), - MaybeExpr::Literal(nodes) => { - let mut children = nodes.iter(); - if let Some(child) = children.next() { - let mut inner_toks = TokenStream2::new(); - self.recurse(child).to_tokens(&mut inner_toks); - for child in children { - quote!(,).to_tokens(&mut inner_toks); - self.recurse(child).to_tokens(&mut inner_toks); - } - tokens.append_all(quote! { - .children([#inner_toks]) - }); - } - } - } - tokens.append_all(quote! { - .finish() - }); - } -} - -impl Parse for Element { - fn parse(s: ParseStream) -> Result { - s.parse::()?; - let name = Ident::parse_any(s)?; - let mut attrs = vec![]; - let _children: Vec = vec![]; - - // keep looking for attributes - while !s.peek(Token![>]) { - // self-closing - if s.peek(Token![/]) { - s.parse::()?; - s.parse::]>()?; - return Ok(Self { - name, - attrs, - children: MaybeExpr::Literal(vec![]), - }); - } - attrs.push(s.parse()?); - } - s.parse::]>()?; - - // Contents of an element can either be a brace (in which case we just copy verbatim), or a - // sequence of nodes. - let children = if s.peek(token::Brace) { - // expr - let content; - syn::braced!(content in s); - MaybeExpr::Expr(content.parse()?) - } else { - // nodes - let mut children = vec![]; - while !(s.peek(Token![<]) && s.peek2(Token![/])) { - children.push(s.parse()?); - } - MaybeExpr::Literal(children) - }; - - // closing element - s.parse::()?; - s.parse::()?; - let close = Ident::parse_any(s)?; - if close != name { - return Err(Error::new_spanned( - close, - "closing element does not match opening", - )); - } - s.parse::]>()?; - - Ok(Self { - name, - attrs, - children, - }) - } -} - -/// ======================================= -/// Parse a VElement's Attributes -/// ======================================= -/// - [ ] Allow expressions as attribute -/// -/// -struct Attr { - name: Ident, - ty: AttrType, -} - -impl Parse for Attr { - fn parse(s: ParseStream) -> Result { - let mut name = Ident::parse_any(s)?; - let name_str = name.to_string(); - s.parse::()?; - - // Check if this is an event handler - // If so, parse into literal tokens - let ty = if name_str.starts_with("on") { - // remove the "on" bit - name = Ident::new(name_str.trim_start_matches("on"), name.span()); - let content; - syn::braced!(content in s); - // AttrType::Value(content.parse()?) - AttrType::Event(content.parse()?) - // AttrType::Event(content.parse()?) - } else { - let lit_str = if name_str == "style" && s.peek(token::Brace) { - // special-case to deal with literal styles. - let outer; - syn::braced!(outer in s); - // double brace for inline style. - // todo!("Style support not ready yet"); - - // if outer.peek(token::Brace) { - // let inner; - // syn::braced!(inner in outer); - // let styles: Styles = inner.parse()?; - // MaybeExpr::Literal(LitStr::new(&styles.to_string(), Span::call_site())) - // } else { - // just parse as an expression - MaybeExpr::Expr(outer.parse()?) - // } - } else { - s.parse()? - }; - AttrType::Value(lit_str) - }; - Ok(Attr { name, ty }) - } -} - -impl ToTokens for ToToksCtx<&Attr> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let name = self.inner.name.to_string(); - let _attr_stream = TokenStream2::new(); - match &self.inner.ty { - AttrType::Value(value) => { - let value = self.recurse(value); - if name == "xmlns" { - tokens.append_all(quote! { - .namespace(Some(#value)) - }); - } else { - tokens.append_all(quote! { - .attr(#name, format_args_f!(#value)) - }); - } - } - AttrType::Event(event) => { - tokens.append_all(quote! { - .on(#name, #event) - }); - } - } - } -} - -enum AttrType { - Value(MaybeExpr), - Event(ExprClosure), - // todo Bool(MaybeExpr) -} - -/// ======================================= -/// Parse just plain text -/// ======================================= -/// - [ ] Perform formatting automatically -/// -/// -struct TextNode(MaybeExpr); - -impl Parse for TextNode { - fn parse(s: ParseStream) -> Result { - Ok(Self(s.parse()?)) - } -} - -impl ToTokens for ToToksCtx<&TextNode> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let mut token_stream = TokenStream2::new(); - self.recurse(&self.inner.0).to_tokens(&mut token_stream); - tokens.append_all(quote! { - __cx.text(format_args_f!(#token_stream)) - }); - } -} - -#[allow(clippy::large_enum_variant)] -enum MaybeExpr { - Literal(T), - Expr(Expr), -} - -impl Parse for MaybeExpr { - fn parse(s: ParseStream) -> Result { - if s.peek(token::Brace) { - let content; - syn::braced!(content in s); - Ok(MaybeExpr::Expr(content.parse()?)) - } else { - Ok(MaybeExpr::Literal(s.parse()?)) - } - } -} - -impl<'a, T> ToTokens for ToToksCtx<&'a MaybeExpr> -where - T: 'a, - ToToksCtx<&'a T>: ToTokens, -{ - fn to_tokens(&self, tokens: &mut TokenStream2) { - match &self.inner { - MaybeExpr::Literal(v) => self.recurse(v).to_tokens(tokens), - MaybeExpr::Expr(expr) => expr.to_tokens(tokens), - } - } -} - -/// ToTokens context -struct ToToksCtx { - inner: T, -} - -impl<'a, T> ToToksCtx { - fn new(inner: T) -> Self { - ToToksCtx { inner } - } - - fn recurse(&self, inner: U) -> ToToksCtx { - ToToksCtx { inner } - } -} - -impl ToTokens for ToToksCtx<&LitStr> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - self.inner.to_tokens(tokens) - } -} - -#[cfg(test)] -mod test { - fn parse(input: &str) -> super::Result { - syn::parse_str(input) - } - - #[test] - fn div() { - parse("bump,
").unwrap(); - } - - #[test] - fn nested() { - parse("bump,
\"text\"
").unwrap(); - } - - #[test] - fn complex() { - parse( - "bump, -
{contact_details}
- ", - ) - .unwrap(); - } -} diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index b9911dedc..e20d80dec 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -1,4 +1,97 @@ -pub mod ifmt; -pub mod inlineprops; -pub mod props; -pub mod rsx; +//! Parse the root tokens in the rsx!{} macro +//! ========================================= +//! +//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing. +//! This feature must support: +//! - [x] Optionally rendering if the `in XYZ` pattern is present +//! - [x] Fragments as top-level element (through ambiguous) +//! - [x] Components as top-level element (through ambiguous) +//! - [x] Tags as top-level elements (through ambiguous) +//! - [x] Good errors if parsing fails +//! +//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. + +mod component; +mod element; +mod node; + +pub mod pretty; + +// Re-export the namespaces into each other +pub use component::*; +pub use element::*; +pub use node::*; + +// imports +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + Ident, Result, Token, +}; + +pub struct CallBody { + pub custom_context: Option, + pub roots: Vec, +} + +impl Parse for CallBody { + fn parse(input: ParseStream) -> Result { + let custom_context = if input.peek(Ident) && input.peek2(Token![,]) { + let name = input.parse::()?; + input.parse::()?; + + Some(name) + } else { + None + }; + + let mut roots = Vec::new(); + + while !input.is_empty() { + let node = input.parse::()?; + + if input.peek(Token![,]) { + let _ = input.parse::(); + } + + roots.push(node); + } + + Ok(Self { + custom_context, + roots, + }) + } +} + +/// Serialize the same way, regardless of flavor +impl ToTokens for CallBody { + fn to_tokens(&self, out_tokens: &mut TokenStream2) { + let inner = if self.roots.len() == 1 { + let inner = &self.roots[0]; + quote! { #inner } + } else { + let childs = &self.roots; + quote! { __cx.fragment_root([ #(#childs),* ]) } + }; + + match &self.custom_context { + // The `in cx` pattern allows directly rendering + Some(ident) => out_tokens.append_all(quote! { + #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + #inner + })) + }), + + // Otherwise we just build the LazyNode wrapper + None => out_tokens.append_all(quote! { + LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + #inner + }) + }), + }; + } +} diff --git a/packages/rsx/src/rsx/node.rs b/packages/rsx/src/node.rs similarity index 100% rename from packages/rsx/src/rsx/node.rs rename to packages/rsx/src/node.rs diff --git a/packages/rsx/src/pretty.rs b/packages/rsx/src/pretty.rs new file mode 100644 index 000000000..26ad54006 --- /dev/null +++ b/packages/rsx/src/pretty.rs @@ -0,0 +1 @@ +//! pretty printer for rsx! diff --git a/packages/rsx/src/rsx/mod.rs b/packages/rsx/src/rsx/mod.rs deleted file mode 100644 index 87b324dfc..000000000 --- a/packages/rsx/src/rsx/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Parse the root tokens in the rsx!{} macro -//! ========================================= -//! -//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing. -//! This feature must support: -//! - [x] Optionally rendering if the `in XYZ` pattern is present -//! - [x] Fragments as top-level element (through ambiguous) -//! - [x] Components as top-level element (through ambiguous) -//! - [x] Tags as top-level elements (through ambiguous) -//! - [x] Good errors if parsing fails -//! -//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. - -mod component; -mod element; -mod node; - -// Re-export the namespaces into each other -pub use component::*; -pub use element::*; -pub use node::*; - -// imports -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseStream}, - Ident, Result, Token, -}; - -pub struct CallBody { - pub custom_context: Option, - pub roots: Vec, -} - -impl Parse for CallBody { - fn parse(input: ParseStream) -> Result { - let custom_context = if input.peek(Ident) && input.peek2(Token![,]) { - let name = input.parse::()?; - input.parse::()?; - - Some(name) - } else { - None - }; - - let mut roots = Vec::new(); - - while !input.is_empty() { - let node = input.parse::()?; - - if input.peek(Token![,]) { - let _ = input.parse::(); - } - - roots.push(node); - } - - Ok(Self { - custom_context, - roots, - }) - } -} - -/// Serialize the same way, regardless of flavor -impl ToTokens for CallBody { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let inner = if self.roots.len() == 1 { - let inner = &self.roots[0]; - quote! { #inner } - } else { - let childs = &self.roots; - quote! { __cx.fragment_root([ #(#childs),* ]) } - }; - - match &self.custom_context { - // The `in cx` pattern allows directly rendering - Some(ident) => out_tokens.append_all(quote! { - #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner - })) - }), - - // Otherwise we just build the LazyNode wrapper - None => out_tokens.append_all(quote! { - LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner - }) - }), - }; - } -} From 185902f935b545b6af70fcff53f130031d98fa50 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:14:17 -0500 Subject: [PATCH 245/256] fix: macro compiles --- packages/core-macro/src/lib.rs | 37 +++++----------------------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index b2e4ffbb1..96ea0ce87 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -1,8 +1,11 @@ -use dioxus_macro_inner::*; use proc_macro::TokenStream; use quote::ToTokens; use syn::parse_macro_input; +mod ifmt; +mod inlineprops; +mod props; + #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { use ifmt::*; @@ -175,42 +178,12 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token #[proc_macro_error::proc_macro_error] #[proc_macro] pub fn rsx(s: TokenStream) -> TokenStream { - match syn::parse::(s) { + match syn::parse::(s) { Err(err) => err.to_compile_error().into(), Ok(stream) => stream.to_token_stream().into(), } } -/// Derive macro used to mark an enum as Routable. -/// -/// This macro can only be used on enums. Every varient of the macro needs to be marked -/// with the `at` attribute to specify the URL of the route. It generates an implementation of -/// `yew_router::Routable` trait and `const`s for the routes passed which are used with `Route` -/// component. -/// -/// # Example -/// -/// ``` -/// # use yew_router::Routable; -/// #[derive(Debug, Clone, Copy, PartialEq, Routable)] -/// enum Routes { -/// #[at("/")] -/// Home, -/// #[at("/secure")] -/// Secure, -/// #[at("/profile/{id}")] -/// Profile(u32), -/// #[at("/404")] -/// NotFound, -/// } -/// ``` -#[proc_macro_derive(Routable, attributes(at, not_found))] -pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - use router::{routable_derive_impl, Routable}; - 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

`, From 9438cc14bc8c2dd8781908a598684e723d45ec45 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:17:33 -0500 Subject: [PATCH 246/256] fix: make listeners up to date --- packages/rsx/src/element.rs | 84 ++++++++++++++++++------------------- src/lib.rs | 2 +- 2 files changed, 42 insertions(+), 44 deletions(-) diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index c612e9f1b..2c295d9a0 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, - Expr, ExprClosure, Ident, LitStr, Result, Token, + Expr, Ident, LitStr, Result, Token, }; // ======================================= @@ -81,24 +81,31 @@ impl Parse for Element { content.parse::()?; if name_str.starts_with("on") { - if content.fork().parse::().is_ok() { - // - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventClosure { - name, - closure: content.parse()?, - }, - }); - } else { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: content.parse()?, - }, - }); - } + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventTokens { + name, + tokens: content.parse()?, + }, + }); + // if content.fork().parse::().is_ok() { + // // + // attributes.push(ElementAttrNamed { + // el_name: el_name.clone(), + // attr: ElementAttr::EventClosure { + // name, + // closure: content.parse()?, + // }, + // }); + // } else { + // attributes.push(ElementAttrNamed { + // el_name: el_name.clone(), + // attr: ElementAttr::EventTokens { + // name, + // tokens: content.parse()?, + // }, + // }); + // } } else { match name_str.as_str() { "key" => { @@ -203,28 +210,20 @@ impl ToTokens for Element { let name = &self.name; let children = &self.children; - // let listeners = &self.listeners; - let key = match &self.key { Some(ty) => quote! { Some(format_args_f!(#ty)) }, None => quote! { None }, }; - let listeners = self.attributes.iter().filter(|f| { - if let ElementAttr::EventTokens { .. } = f.attr { - true - } else { - false - } - }); + let listeners = self + .attributes + .iter() + .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); - let attr = self.attributes.iter().filter(|f| { - if let ElementAttr::EventTokens { .. } = f.attr { - false - } else { - true - } - }); + let attr = self + .attributes + .iter() + .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); tokens.append_all(quote! { __cx.element( @@ -251,9 +250,8 @@ pub enum ElementAttr { /// "attribute": true, CustomAttrExpression { name: LitStr, value: Expr }, - /// onclick: move |_| {} - EventClosure { name: Ident, closure: ExprClosure }, - + // /// onclick: move |_| {} + // EventClosure { name: Ident, closure: ExprClosure }, /// onclick: {} EventTokens { name: Ident, tokens: Expr }, } @@ -288,11 +286,11 @@ impl ToTokens for ElementAttrNamed { __cx.attr( #name, format_args_f!(#value), None, false ) } } - ElementAttr::EventClosure { name, closure } => { - quote! { - dioxus_elements::on::#name(__cx, #closure) - } - } + // ElementAttr::EventClosure { name, closure } => { + // quote! { + // dioxus_elements::on::#name(__cx, #closure) + // } + // } ElementAttr::EventTokens { name, tokens } => { quote! { dioxus_elements::on::#name(__cx, #tokens) diff --git a/src/lib.rs b/src/lib.rs index 2db210bee..488b1ee9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -353,7 +353,7 @@ pub mod events { pub mod prelude { pub use dioxus_core::prelude::*; - pub use dioxus_core_macro::{format_args_f, inline_props, rsx, Props, Routable}; + pub use dioxus_core_macro::{format_args_f, inline_props, rsx, Props}; pub use dioxus_elements::{GlobalAttributes, SvgAttributes}; pub use dioxus_hooks::*; pub use dioxus_html as dioxus_elements; From 5d56326f74e07dbc70141100bb71826ed7033152 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:18:52 -0500 Subject: [PATCH 247/256] feat: collapse rsx --- packages/rsx/src/element.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 2c295d9a0..fa8fc9f51 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -88,24 +88,6 @@ impl Parse for Element { tokens: content.parse()?, }, }); - // if content.fork().parse::().is_ok() { - // // - // attributes.push(ElementAttrNamed { - // el_name: el_name.clone(), - // attr: ElementAttr::EventClosure { - // name, - // closure: content.parse()?, - // }, - // }); - // } else { - // attributes.push(ElementAttrNamed { - // el_name: el_name.clone(), - // attr: ElementAttr::EventTokens { - // name, - // tokens: content.parse()?, - // }, - // }); - // } } else { match name_str.as_str() { "key" => { @@ -199,7 +181,6 @@ impl Parse for Element { name: el_name, attributes, children, - // listeners, _is_static: false, }) } From 7ce34ad97ed926c4fa313d4a881ceb78a5800485 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:22:35 -0500 Subject: [PATCH 248/256] fix: enable desktop devtol --- packages/desktop/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 371adced5..197735ddc 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -173,6 +173,10 @@ pub fn launch_with_props( webview = webview.with_custom_protocol(name, handler) } + if cfg!(debug_assertions) { + webview = webview.with_dev_tool(true); + } + desktop.webviews.insert(window_id, webview.build().unwrap()); } From 237f036a801244f5ae3dc664e58f244c2cf1d561 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:26:47 -0500 Subject: [PATCH 249/256] wip: add meta to rsx --- packages/rsx/src/node.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index d5d701767..eeee421b0 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseStream}, - token, Expr, LitStr, Result, Token, + token, Attribute, Expr, LitStr, Result, Token, }; /* @@ -20,6 +20,7 @@ pub enum BodyNode { Component(Component), Text(LitStr), RawExpr(Expr), + Meta(Attribute), } impl Parse for BodyNode { @@ -79,6 +80,7 @@ impl ToTokens for BodyNode { BodyNode::RawExpr(exp) => tokens.append_all(quote! { __cx.fragment_from_iter(#exp) }), + BodyNode::Meta(_) => {} } } } From 5a6d1c2a310aff3ec996f0f02d2ef09d5e3a54a3 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:30:09 -0500 Subject: [PATCH 250/256] fix: compile for rust 1.57 --- packages/desktop/src/events.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 7be4273e9..44126dd63 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -26,7 +26,7 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option { match serde_json::from_str(payload) { Ok(message) => Some(message), Err(e) => { - log::error!("could not parse IPC message, error: {e}"); + log::error!("could not parse IPC message, error: {}", e); None } } From c1d692dd974d9122120e20b69e56b1c36b9d1e26 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 18 Feb 2022 23:33:48 -0500 Subject: [PATCH 251/256] fix: dioxus must specify rust versionand edition --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bd15590ac..2ba3b55fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,13 +2,14 @@ name = "dioxus" version = "0.1.8" authors = ["Jonathan Kelley"] -edition = "2018" +edition = "2021" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" license = "MIT OR Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] +rust-version = "1.56.0" [dependencies] dioxus-core = { path = "./packages/core", version = "^0.1.9" } From 28716248c5de9c9ebc9c8dbc1dd32ec7be9eb99b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Feb 2022 17:34:44 -0500 Subject: [PATCH 252/256] fix: ping window after virtualdom is ready --- examples/heavy_compute.rs | 28 ++++++++++++++++++++++ packages/desktop/src/controller.rs | 3 +++ packages/desktop/src/lib.rs | 1 - packages/desktop/src/user_window_events.rs | 1 + 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 examples/heavy_compute.rs diff --git a/examples/heavy_compute.rs b/examples/heavy_compute.rs new file mode 100644 index 000000000..6e10a2f1f --- /dev/null +++ b/examples/heavy_compute.rs @@ -0,0 +1,28 @@ +//! This example shows that you can place heavy work on the main thread, and then +//! +//! You *should* be using `tokio::spawn_blocking` instead. +//! +//! Your app runs in an async runtime (Tokio), so you should avoid blocking +//! the rendering of the VirtualDom. +//! +//! + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + // This is discouraged + std::thread::sleep(std::time::Duration::from_millis(2_000)); + + // This is suggested + tokio::task::spawn_blocking(move || { + std::thread::sleep(std::time::Duration::from_millis(2_000)); + }); + + cx.render(rsx! { + div { "Hello, world!" } + }) +} diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 8497a42e9..8715c74f0 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -57,6 +57,9 @@ impl DesktopController { .unwrap() .push_front(serde_json::to_string(&edits.edits).unwrap()); + // Make sure the window is ready for any new updates + proxy.send_event(UserWindowEvent::Update).unwrap(); + loop { dom.wait_for_work().await; let mut muts = dom.work_with_deadline(|| false); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 197735ddc..934c82f1a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -143,7 +143,6 @@ pub fn launch_with_props( .send_event(user_window_events::UserWindowEvent::Update); } "browser_open" => { - println!("browser_open"); let data = message.params(); log::trace!("Open browser: {:?}", data); if let Some(temp) = data.as_object() { diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs index aab14561b..bde215d48 100644 --- a/packages/desktop/src/user_window_events.rs +++ b/packages/desktop/src/user_window_events.rs @@ -3,6 +3,7 @@ use wry::application::window::Fullscreen as WryFullscreen; use crate::controller::DesktopController; +#[derive(Debug)] pub(crate) enum UserWindowEvent { Update, From 45473ece8c2fb9ac076b4e881f071d0efd7b9a1e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Feb 2022 18:00:03 -0500 Subject: [PATCH 253/256] feat: add some helpers to use_ref --- examples/use_ref.rs | 21 +++++++++++++++++++++ packages/hooks/src/useref.rs | 11 +++++++++++ 2 files changed, 32 insertions(+) create mode 100644 examples/use_ref.rs diff --git a/examples/use_ref.rs b/examples/use_ref.rs new file mode 100644 index 000000000..358b9c266 --- /dev/null +++ b/examples/use_ref.rs @@ -0,0 +1,21 @@ +use std::collections::HashMap; + +use dioxus::prelude::*; +fn main() {} + +fn app(cx: Scope) -> Element { + let val = use_ref(&cx, || HashMap::::new()); + + // Pull the value out locally + let p = val.read(); + + // to get an &HashMap we have to "reborrow" through the RefCell + // Be careful: passing this into children might cause a double borrow of the RefCell and a panic + let g = &*p; + + cx.render(rsx! { + div { + "hi" + } + }) +} diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index 3ddf86e8c..a0b9189d0 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -42,6 +42,17 @@ impl UseRef { self.value.borrow_mut() } + /// Take a reference to the inner value termporarily and produce a new value + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + f(&*self.read()) + } + + /// Take a reference to the inner value termporarily and produce a new value, + /// modifying the original in place. + pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { + f(&mut *self.write()) + } + pub fn needs_update(&self) { (self.update_callback)(); } From e7022dfccf64b378ac08a93c02758690934821fc Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Feb 2022 21:51:26 -0500 Subject: [PATCH 254/256] docs: more use_ref docs --- examples/use_ref.rs | 14 +- packages/hooks/src/useref.rs | 255 ++++++++++++++++++++++++++++------- 2 files changed, 216 insertions(+), 53 deletions(-) diff --git a/examples/use_ref.rs b/examples/use_ref.rs index 358b9c266..d0b85a66f 100644 --- a/examples/use_ref.rs +++ b/examples/use_ref.rs @@ -6,12 +6,16 @@ fn main() {} fn app(cx: Scope) -> Element { let val = use_ref(&cx, || HashMap::::new()); - // Pull the value out locally - let p = val.read(); + { + // Pull the value out locally + let p = val.read(); - // to get an &HashMap we have to "reborrow" through the RefCell - // Be careful: passing this into children might cause a double borrow of the RefCell and a panic - let g = &*p; + // to get an &HashMap we have to "reborrow" through the RefCell + // Be careful: passing this into children might cause a double borrow of the RefCell and a panic + let g = &*p; + + dbg!(g); + } cx.render(rsx! { div { diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index a0b9189d0..c2537e079 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -1,68 +1,227 @@ +use dioxus_core::ScopeState; use std::{ cell::{Ref, RefCell, RefMut}, rc::Rc, }; -use dioxus_core::ScopeState; - -pub fn use_ref<'a, T: 'static>(cx: &'a ScopeState, f: impl FnOnce() -> T) -> &'a UseRef { +/// `use_ref` is a key foundational hook for storing state in Dioxus. +/// +/// It is different that `use_state` in that the value stored is not "immutable". +/// Instead, UseRef is designed to store larger values that will be mutated at will. +/// +/// ## Writing Values +/// +/// Generally, `use_ref` is just a wrapper around a RefCell that tracks mutable +/// writes through the `write` method. Whenever `write` is called, the component +/// that initialized the hook will be marked as "dirty". +/// +/// ```rust +/// let val = use_ref(|| HashMap::::new()); +/// +/// // using `write` will give us a `RefMut` to the inner value, which we can call methods on +/// // This marks the component as "dirty" +/// val.write().insert(1, "hello".to_string()); +/// ``` +/// +/// You can avoid this defualt behavior with `write_silent` +/// +/// ``` +/// // with `write_silent`, the component will not be re-rendered +/// val.write_silent().insert(2, "goodbye".to_string()); +/// ``` +/// +/// ## Reading Values +/// +/// To read values out of the refcell, you can use the `read` method which will retrun a `Ref`. +/// +/// ```rust +/// let map: Ref<_> = val.read(); +/// +/// let item = map.get(&1); +/// ``` +/// +/// To get an &T out of the RefCell, you need to "reborrow" through the Ref: +/// +/// ```rust +/// let read = val.read(); +/// let map = &*read; +/// ``` +/// +/// ## Collections and iteration +/// +/// A common usecase for `use_ref` is to store a large amount of data in a component. +/// Typically this will be a collection like a HashMap or a Vec. To create new +/// elements from the collection, we can use `read()` directly in our rsx!. +/// +/// ```rust +/// rsx!{ +/// val.read().iter().map(|(k, v)| { +/// rsx!{ key: "{k}", value: "{v}" } +/// }) +/// } +/// ``` +/// +/// If you are generating elements outside of `rsx!` then you might need to call +/// "render" inside the iterator. For some cases you might need to collect into +/// a temporary Vec. +/// +/// ```rust +/// let items = val.read().iter().map(|(k, v)| { +/// cx.render(rsx!{ key: "{k}", value: "{v}" }) +/// }); +/// +/// // collect into a Vec +/// +/// let items: Vec = items.collect(); +/// ``` +/// +/// ## Use in Async +/// +/// To access values from a `UseRef` in an async context, you need to detach it +/// from the current scope's lifetime, making it a `'static` value. This is done +/// by simply calling `ToOnwed` or `Clone`. +/// +/// ```rust +/// let val = use_ref(|| HashMap::::new()); +/// +/// cx.spawn({ +/// let val = val.clone(); +/// async move { +/// some_work().await; +/// val.write().insert(1, "hello".to_string()); +/// } +/// }) +/// ``` +/// +/// If you're working with lots of values like UseState and UseRef, you can use the +/// `clone!` macro to make it easier to write the above code. +/// +/// ```rust +/// let val1 = use_ref(|| HashMap::::new()); +/// let val2 = use_ref(|| HashMap::::new()); +/// let val3 = use_ref(|| HashMap::::new()); +/// +/// cx.spawn({ +/// clone![val1, val2, val3]; +/// async move { +/// some_work().await; +/// val.write().insert(1, "hello".to_string()); +/// } +/// }) +/// ``` +pub fn use_ref<'a, T: 'static>( + cx: &'a ScopeState, + initialize_refcell: impl FnOnce() -> T, +) -> &'a UseRef { cx.use_hook(|_| UseRef { - update_callback: cx.schedule_update(), - value: Rc::new(RefCell::new(f())), + update: cx.schedule_update(), + value: Rc::new(RefCell::new(initialize_refcell())), }) } +/// A type created by the [`use_ref`] hook. See its documentation for more details. pub struct UseRef { - update_callback: Rc, + update: Rc, value: Rc>, } -impl UseRef { - pub fn read(&self) -> Ref<'_, T> { - self.value.borrow() - } - - pub fn set(&self, new: T) { - *self.value.borrow_mut() = new; - self.needs_update(); - } - - pub fn read_write(&self) -> (Ref<'_, T>, &Self) { - (self.read(), self) - } - - /// Calling "write" will force the component to re-render - pub fn write(&self) -> RefMut<'_, T> { - self.needs_update(); - self.value.borrow_mut() - } - - /// Allows the ability to write the value without forcing a re-render - pub fn write_silent(&self) -> RefMut<'_, T> { - self.value.borrow_mut() - } - - /// Take a reference to the inner value termporarily and produce a new value - pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { - f(&*self.read()) - } - - /// Take a reference to the inner value termporarily and produce a new value, - /// modifying the original in place. - pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { - f(&mut *self.write()) - } - - pub fn needs_update(&self) { - (self.update_callback)(); - } -} - impl Clone for UseRef { fn clone(&self) -> Self { Self { - update_callback: self.update_callback.clone(), + update: self.update.clone(), value: self.value.clone(), } } } + +impl UseRef { + /// Read the value in the RefCell into a `Ref`. If this method is called + /// while other values are still being `read` or `write`, then your app will crash. + /// + /// Be very careful when working with this method. If you can, consider using + /// the `with` and `with_mut` methods instead, choosing to render Elements + /// during the read calls. + pub fn read(&self) -> Ref<'_, T> { + self.value.borrow() + } + + /// Set the curernt value to `new_value`. This will mark the component as "dirty" + /// + /// This change will propogate immediately, so any other contexts that are + /// using this RefCell will also be affected. If called during an async context, + /// the component will not be re-rendered until the next `.await` call. + pub fn set(&self, new: T) { + *self.value.borrow_mut() = new; + self.needs_update(); + } + + /// Mutably unlock the value in the RefCell. This will mark the component as "dirty" + /// + /// Uses to `write` should be as short as possible. + /// + /// Be very careful when working with this method. If you can, consider using + /// the `with` and `with_mut` methods instead, choosing to render Elements + /// during the read and write calls. + pub fn write(&self) -> RefMut<'_, T> { + self.needs_update(); + self.value.borrow_mut() + } + + /// Mutably unlock the value in the RefCell. This will not mark the component as dirty. + /// This is useful if you want to do some work without causing the component to re-render. + /// + /// Uses to `write` should be as short as possible. + /// + /// Be very careful when working with this method. If you can, consider using + /// the `with` and `with_mut` methods instead, choosing to render Elements + pub fn write_silent(&self) -> RefMut<'_, T> { + self.value.borrow_mut() + } + + /// Take a reference to the inner value termporarily and produce a new value + /// + /// Note: You can always "reborrow" the value through the RefCell. + /// This method just does it for you automatically. + /// + /// ```rust + /// let val = use_ref(|| HashMap::::new()); + /// + /// + /// // use reborrowing + /// let inner = &*val.read(); + /// + /// // or, be safer and use `with` + /// val.with(|i| println!("{:?}", i)); + /// ``` + pub fn with(&self, immutable_callback: impl FnOnce(&T) -> O) -> O { + immutable_callback(&*self.read()) + } + + /// Take a reference to the inner value termporarily and produce a new value, + /// modifying the original in place. + /// + /// Note: You can always "reborrow" the value through the RefCell. + /// This method just does it for you automatically. + /// + /// ```rust + /// let val = use_ref(|| HashMap::::new()); + /// + /// + /// // use reborrowing + /// let inner = &mut *val.write(); + /// + /// // or, be safer and use `with` + /// val.with_mut(|i| i.insert(1, "hi")); + /// ``` + pub fn with_mut(&self, mutable_callback: impl FnOnce(&mut T) -> O) -> O { + mutable_callback(&mut *self.write()) + } + + /// Call the inner callback to mark the originator component as dirty. + /// + /// This will cause the component to be re-rendered after the current scope + /// has ended or the current async task has been yielded through await. + pub fn needs_update(&self) { + (self.update)(); + } +} From 98076c52e88be4a05ffb854e0dfac9cae8f52c02 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Feb 2022 21:53:00 -0500 Subject: [PATCH 255/256] docs: remove use_ref example --- examples/use_ref.rs | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 examples/use_ref.rs diff --git a/examples/use_ref.rs b/examples/use_ref.rs deleted file mode 100644 index d0b85a66f..000000000 --- a/examples/use_ref.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::collections::HashMap; - -use dioxus::prelude::*; -fn main() {} - -fn app(cx: Scope) -> Element { - let val = use_ref(&cx, || HashMap::::new()); - - { - // Pull the value out locally - let p = val.read(); - - // to get an &HashMap we have to "reborrow" through the RefCell - // Be careful: passing this into children might cause a double borrow of the RefCell and a panic - let g = &*p; - - dbg!(g); - } - - cx.render(rsx! { - div { - "hi" - } - }) -} From 5954bba975691b9b080f73637b95e56e46a0d95f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Feb 2022 22:36:55 -0500 Subject: [PATCH 256/256] wip: disable reload in production --- packages/desktop/src/index.html | 24 ++++++++++++------------ packages/desktop/src/lib.rs | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index e244b6faa..292a1ae6a 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -1,15 +1,15 @@ - - Dioxus app - - - -

- - + + Dioxus app + + + +
+ + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 197735ddc..4d204a378 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -174,7 +174,25 @@ pub fn launch_with_props( } if cfg!(debug_assertions) { + // in debug, we are okay with the reload menu showing and dev tool webview = webview.with_dev_tool(true); + } else { + // in release mode, we don't want to show the dev tool or reload menus + webview = webview.with_initialization_script( + r#" + if (document.addEventListener) { + document.addEventListener('contextmenu', function(e) { + alert("You've tried to open context menu"); + e.preventDefault(); + }, false); + } else { + document.attachEvent('oncontextmenu', function() { + alert("You've tried to open context menu"); + window.event.returnValue = false; + }); + } + "#, + ) } desktop.webviews.insert(window_id, webview.build().unwrap());